Course 3’s multi-step workflows relied on the connected client’s own model to plan a
sequence of tool calls, Claude or ChatGPT read your ability descriptions and decided the
order itself, entirely outside your server. That’s the right default. Sometimes, though,
you want planning to happen inside WordPress, as a single ability an outer agent calls once,
that itself decides which of several other abilities to run and in what order. That’s a
planner/executor pattern, and it’s built from two functions you already know:
wp_ai_client_prompt() from the PHP AI Client SDK course, and wp_get_ability( $name )->execute()
from the Abilities API.
wp_ai_client_prompt() and as_json_response() from the PHP AI Client SDK course, and
wp_get_ability( $name )->execute( $input ) from the Abilities API. This lesson assumes
you already have the order-ops/get-order-status, order-ops/open-escalation,
order-ops/process-return, and order-ops/reship-order abilities from Lesson 1 registered.
Step 1: Why plan inside WordPress at all
An outer client model is still the one deciding to call your planner ability in the first place, planning doesn’t disappear, it moves one level down. The planner pattern is worth it when a task has a known, bounded set of internal abilities that almost always need to run together in some order, and you’d rather encapsulate that decision-making behind one ability than expose four or five raw abilities and hope every client’s model sequences them the same sensible way every time. A “resolve this order issue” planner is a good candidate: given a description of the problem, it decides whether that means checking status, processing a return, reshipping, or opening an escalation for a human, and calls the right one.
Step 2: The planner ability, using wp_ai_client_prompt() to decide
// File: order-ops/class-plan-resolution.php
wp_register_ability( 'order-ops/plan-resolution', array(
'label' => 'Plan and execute an order issue resolution',
'description' => 'Given a plain-language description of a customer\'s order issue and '
. 'the order ID, decides which internal action is appropriate (check status, open an '
. 'escalation, process a return, or reship) and executes it. Returns the action taken '
. 'and its result.',
'input_schema' => array(
'type' => 'object',
'properties' => array(
'order_id' => array( 'type' => 'integer' ),
'description' => array( 'type' => 'string' ),
),
'required' => array( 'order_id', 'description' ),
),
'execute_callback' => 'order_ops_plan_resolution',
'permission_callback' => 'order_ops_can_resolve',
'meta' => array( 'mcp' => array( 'public' => true ) ),
) );
function order_ops_plan_resolution( $input ) {
$schema = array(
'type' => 'object',
'properties' => array(
'action' => array(
'type' => 'string',
'enum' => array( 'check_status', 'open_escalation', 'process_return', 'reship' ),
),
'reasoning' => array( 'type' => 'string' ),
),
'required' => array( 'action', 'reasoning' ),
);
$raw = wp_ai_client_prompt()
->with_text( 'Order ID: ' . $input['order_id'] . '. Customer issue: ' . $input['description'] )
->using_system_instruction(
'You decide which single action resolves a customer order issue. Choose ' .
'check_status for questions with no clear problem, open_escalation for anything ' .
'ambiguous or high-value, process_return only for explicit return requests, and ' .
'reship only for confirmed lost-or-damaged shipments. When unsure, choose ' .
'open_escalation, never guess toward a destructive action.'
)
->as_json_response( $schema )
->generate_text();
$plan = json_decode( $raw, true );
if ( ! is_array( $plan ) || empty( $plan['action'] ) ) {
return new WP_Error( 'planning_failed', 'Could not determine a resolution action.' );
}
return order_ops_execute_plan( $plan['action'], $input['order_id'], $plan['reasoning'] );
}
Step 3: Executing the plan with wp_get_ability()
The planner never calls the underlying logic directly. It looks the ability up by name and executes it exactly the way an external MCP client would, just from inside PHP:
// File: order-ops/class-plan-resolution.php (continued)
function order_ops_execute_plan( $action, $order_id, $reasoning ) {
$ability_map = array(
'check_status' => 'order-ops/get-order-status',
'open_escalation' => 'order-ops/open-escalation',
'process_return' => 'order-ops/process-return',
'reship' => 'order-ops/reship-order',
);
$ability_name = $ability_map[ $action ] ?? null;
$ability = $ability_name ? wp_get_ability( $ability_name ) : null;
if ( ! $ability ) {
return new WP_Error( 'unknown_action', 'Planner chose an action with no matching ability.' );
}
$result = $ability->execute( array( 'order_id' => $order_id ) );
return array(
'action_taken' => $action,
'reasoning' => $reasoning,
'result' => $result,
);
}
wp_get_ability() returns null if the name doesn’t exist, which is why the lookup is
checked before calling execute(), a planner that hallucinates an ability name should fail
loudly, not fatal.
Step 4: The planner still needs the same guardrails as any agent
A planner ability that can choose process_return or reship is, functionally, an agent
making a decision with real consequences. The confirmation-gate pattern from Course 3 still
applies here, don’t let the planner call a destructive underlying ability directly. Instead,
route process_return and reship through the propose/confirm pattern from Course 3, so
the planner’s output is a proposal a human still approves, not an executed action:
// File: order-ops/class-plan-resolution.php (continued)
$ability_map = array(
'check_status' => 'order-ops/get-order-status', // safe to run directly
'open_escalation' => 'order-ops/open-escalation', // safe to run directly
'process_return' => 'order-ops/propose-process-return', // proposes only, needs confirm
'reship' => 'order-ops/propose-reship-order', // proposes only, needs confirm
);
Constraining the model’s choice to a fixed enum in the schema, as Step 2 does, is what
keeps this pattern safe enough to build on: the model is picking among four known,
pre-vetted actions, not writing arbitrary PHP or choosing an ability name freeform.
Test it
# File: terminal
wp-env run cli wp eval '
$result = wp_get_ability( "order-ops/plan-resolution" )->execute( array(
"order_id" => 4821,
"description" => "Customer says the package never arrived, tracking shows delivered.",
) );
var_dump( $result );
'
Confirm the action_taken matches what a human reading that description would expect
(likely open_escalation given the ambiguity), and that the schema’s enum constraint
actually rejected any attempt to return an action outside the four listed, try a
deliberately vague description and confirm the model doesn’t invent a fifth action name.
It’s tempting to let the planner call order-ops/process-return directly since it’s “just
following the model’s own reasoning.” That reasoning runs on every call, with no human in
the loop reading it first. Route anything destructive through a propose/confirm pair as
shown in Step 4, the planner should decide what to propose, a human should still decide
whether it happens.
Recap
A planner ability is an execute_callback that calls wp_ai_client_prompt() with a schema
constrained to a fixed set of known actions, then calls wp_get_ability( $name )->execute()
on whichever action the model chose. It moves planning one level down, from the outer
client’s model into your own PHP, which is useful for a bounded, well-known task, but it is
still a model making a decision, so destructive outcomes still need the confirmation
pattern from Course 3 sitting between the plan and anything irreversible.
Resources & further reading
- Abilities API reference, developer.wordpress.org
- wp_register_ability() function reference, developer.wordpress.org
- Introducing the AI Client in WordPress 7.0, make.wordpress.org