Course 3 showed a model chaining tool calls on its own, deciding the sequence fresh, every time, based on what’s in front of it. Lesson 2 showed a planner ability that picks one action from a fixed menu. This lesson is the third pattern: a coordinator ability that always runs the same fixed sequence of other abilities, in the same order, passing each one’s output forward as the next one’s input. No model decides the order here at all, the order is your own PHP.
The order-ops abilities from Lessons 1, 3, and 4, especially process-return. This lesson
introduces two new abilities, validate-return-eligibility and restock-returned-item, to
build a realistic four-step chain.
Step 1: Coordinator versus planner, a real distinction
A planner ability (Lesson 2) calls a model to decide which action to take from several options. A coordinator ability, this lesson’s pattern, always runs the same fixed sequence, no model involved in deciding order at all. Use a coordinator when a process is genuinely linear and repeatable: processing a return always means validating eligibility, then processing the refund, then restocking the item, then notifying the customer, in that order, every time. If that sequence sometimes needs to branch based on judgment (skip restocking for a damaged item, escalate instead of refunding for a high-value order), you’re describing a planner, not a coordinator, don’t force branching logic into a fixed chain.
Step 2: The four abilities the chain will run
// File: order-ops/class-return-chain-steps.php
wp_register_ability( 'order-ops/validate-return-eligibility', array(
'label' => 'Validate a return is within policy',
'description' => 'Checks an order is eligible for return (within the return window, not '
. 'already refunded). Returns eligibility and the refund amount if eligible.',
'input_schema' => array(
'type' => 'object', 'properties' => array( 'order_id' => array( 'type' => 'integer' ) ),
'required' => array( 'order_id' ),
),
'execute_callback' => 'order_ops_validate_return_eligibility',
'permission_callback' => 'order_ops_can_resolve',
'meta' => array( 'mcp' => array( 'public' => true ), 'annotations' => array( 'readonly' => true ) ),
) );
wp_register_ability( 'order-ops/refund-order', array(
'label' => 'Refund an order',
'description' => 'Issues a refund for the given amount against an order already validated '
. 'as eligible. Requires order_id and amount from a prior eligibility check.',
'input_schema' => array(
'type' => 'object',
'properties' => array(
'order_id' => array( 'type' => 'integer' ),
'amount' => array( 'type' => 'number' ),
),
'required' => array( 'order_id', 'amount' ),
),
'execute_callback' => 'order_ops_refund_order',
'permission_callback' => 'order_ops_can_resolve',
'meta' => array( 'mcp' => array( 'public' => true ), 'annotations' => array( 'destructive' => true, 'idempotent' => false ) ),
) );
wp_register_ability( 'order-ops/restock-returned-item', array(
'label' => 'Restock a returned item',
'description' => 'Adds the item from a refunded order back into available inventory.',
'input_schema' => array(
'type' => 'object', 'properties' => array( 'order_id' => array( 'type' => 'integer' ) ),
'required' => array( 'order_id' ),
),
'execute_callback' => 'order_ops_restock_returned_item',
'permission_callback' => 'order_ops_can_resolve',
'meta' => array( 'mcp' => array( 'public' => true ), 'annotations' => array( 'idempotent' => false ) ),
) );
wp_register_ability( 'order-ops/notify-customer-refunded', array(
'label' => 'Notify the customer their refund was processed',
'description' => 'Sends the customer a notification that their return was refunded.',
'input_schema' => array(
'type' => 'object',
'properties' => array( 'order_id' => array( 'type' => 'integer' ), 'amount' => array( 'type' => 'number' ) ),
'required' => array( 'order_id', 'amount' ),
),
'execute_callback' => 'order_ops_notify_customer_refunded',
'permission_callback' => 'order_ops_can_resolve',
'meta' => array( 'mcp' => array( 'public' => true ) ),
) );
Each of these is also independently callable, exactly as Lesson 3 of Course 3 recommended:
restock-returned-item makes sense on its own if a warehouse tool needs to call it directly,
it isn’t only meaningful as one link in this specific chain.
Step 3: The coordinator, passing output forward as input
// File: order-ops/class-coordinate-return.php
wp_register_ability( 'order-ops/coordinate-return', array(
'label' => 'Run the full return process for an order',
'description' => 'Validates eligibility, refunds, restocks, and notifies the customer, '
. 'in that fixed order, for an order already claimed via order-ops/claim-escalation.',
'input_schema' => array(
'type' => 'object', 'properties' => array( 'order_id' => array( 'type' => 'integer' ) ),
'required' => array( 'order_id' ),
),
'execute_callback' => 'order_ops_coordinate_return',
'permission_callback' => 'order_ops_can_resolve',
'meta' => array( 'mcp' => array( 'public' => true ) ),
) );
function order_ops_coordinate_return( $input ) {
$order_id = $input['order_id'];
// Step 1: validate.
$eligibility = wp_get_ability( 'order-ops/validate-return-eligibility' )->execute( array( 'order_id' => $order_id ) );
if ( is_wp_error( $eligibility ) || empty( $eligibility['eligible'] ) ) {
return new WP_Error( 'not_eligible', 'Order is not eligible for return.' );
}
// Step 2: refund, using the amount step 1 determined, not a client-supplied one.
$refund = wp_get_ability( 'order-ops/refund-order' )->execute( array(
'order_id' => $order_id,
'amount' => $eligibility['refund_amount'],
) );
if ( is_wp_error( $refund ) ) {
return $refund;
}
// Step 3: restock, only reachable once the refund above actually succeeded.
$restock = wp_get_ability( 'order-ops/restock-returned-item' )->execute( array( 'order_id' => $order_id ) );
if ( is_wp_error( $restock ) ) {
return $restock;
}
// Step 4: notify, using the same confirmed amount throughout.
$notify = wp_get_ability( 'order-ops/notify-customer-refunded' )->execute( array(
'order_id' => $order_id,
'amount' => $eligibility['refund_amount'],
) );
return array(
'order_id' => $order_id,
'refunded' => $eligibility['refund_amount'],
'restocked' => ! is_wp_error( $restock ),
'notified' => ! is_wp_error( $notify ),
);
}
Notice refund_amount comes from step 1’s return value, not from the original $input. That
matters: a client could pass any order_id it wants, but it can never pass its own refund
amount, the chain itself decides that, from a step whose execute_callback you control and
trust.
Test it
# File: terminal
wp-env run cli wp eval '
$result = wp_get_ability( "order-ops/coordinate-return" )->execute( array( "order_id" => 4821 ) );
var_dump( $result );
'
Confirm all four steps actually ran, an order that was refunded (refunded matches the real
refund amount, checkable against your payment gateway or ledger), restocked (inventory count
increased), and notified (an email or notification actually sent), not just a plausible
return value with no real side effects behind it.
It’s tempting to add an if inside step 2 for “unless the order is high-value, then escalate
instead.” The moment a chain needs a real decision, not just sequential steps, you’ve quietly
turned it into a planner wearing a coordinator’s clothes, and it should probably call
wp_ai_client_prompt() explicitly (Lesson 2’s pattern) rather than accumulating untested
conditional branches inside what’s supposed to be a simple, fixed sequence.
Recap
A coordinator ability runs a fixed, known sequence of other abilities via
wp_get_ability( $name )->execute(), passing each step’s trusted output into the next step’s
input, entirely inside your own PHP. It’s the right tool for a genuinely linear, repeatable
process, and it removes an entire class of sequencing and tampering risk that exists when a
client’s model has to get a multi-step order right on its own every time. The next lesson
covers what this same chain needs when one of its four steps fails partway through.
Resources & further reading
- Abilities API reference, developer.wordpress.org
- wp_register_ability() function reference, developer.wordpress.org
- MCP Adapter repository, GitHub