Building It

Orchestrating Chains of Abilities

⏱ 17 min

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.

What you'll learn in this lesson
When a fixed sequence beats a planned one
A known, repeatable multi-step process doesn't need a model re-deciding its order on every call.
Chaining wp_get_ability()->execute() calls
Passing one ability's return value into the next ability's input array.
A full worked chain: validate, process, restock, notify
Four abilities, one coordinator, run in order against the order-ops example.
Where this pattern stops making sense
Once branching logic creeps in, you're back to a planner, not a coordinator.
Prerequisites

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.

Why this reads better as a chain than a client-planned sequence
1
The order is guaranteed
A model deciding to call refund before validating eligibility is a real failure mode with four loose abilities; a coordinator makes that sequencing mistake structurally impossible.
2
Intermediate values never round-trip through the client
The refund amount moves from step 1 to step 2 inside one PHP function call, not back out to a model and in again, which is one less place for it to be altered or misread.
3
One ability call for the outer agent to reason about
The support or fulfillment agent's model just sees "run the return process," not four separate decisions it has to get right in sequence.

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.

A fixed chain hides branching logic if you're not careful

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

← Task Queues for Long-Running Agent Jobs Handling Partial Failures in a Multi-Step Chain →