As part of our ongoing work improving the WooCommerce MCP experience, WooCommerce 11.3 adds an experimental API that lets extensions add their own data to WooCommerce resource abilities, such as products and orders. AI agents and other clients then read and change that data with the abilities they already use. An extension no longer needs its own copy of a product ability only to expose a few extra fields.
The feature is off by default. To try it, go to WooCommerce > Settings > Advanced > Features and turn on Extensible abilities in the experimental section. With the feature off, none of the hooks below fire, and the abilities work as in 11.2.
What extensions can do in 11.3
- Add fields to reads. A field shows in the ability output under
extensions.<namespace>.<field>, and in the output schema. Fields work on theproduct,orderandorder_itemresources. - Add fields to writes.
woocommerce/product-createandwoocommerce/product-updateaccept the sameextensionsobject as input. WooCommerce saves its own changes and the extension fields in one save. - Refuse a change. A validator sees the changed product before the save. If it returns a
WP_Error, nothing is saved, and the client gets the error. - Add query filters.
woocommerce/products-queryandwoocommerce/orders-queryaccept extension filters underfilters.extensions.<namespace>.<filter>. - Build their own extensible abilities. An extension can extend
Automattic\WooCommerce\Abilities\ActionableAbility. Its ability then loads the object, changes it, writes the extension fields, runs the validators and saves one time, the same way Core’s abilities do.
Extending abilities example
A gift wrap extension stores a gift wrap price on each product. It registers a field, a list filter and a validator inside the woocommerce_extensible_abilities_init action:
<?php
use Automattic\WooCommerce\Abilities\ExtensibleAbilities;
add_action(
'woocommerce_extensible_abilities_init',
function () {
ExtensibleAbilities::register_field(
array(
'resource' => 'product',
'namespace' => 'giftwrap',
'field' => 'price',
'schema' => array(
'type' => array( 'number', 'null' ),
'minimum' => 0,
'title' => 'Gift wrap price',
'description' => 'Price of gift wrapping. Null when the product cannot be gift wrapped.',
),
'get_callback' => function ( WC_Product $product ) {
$price = $product->get_meta( '_giftwrap_price' );
return '' === $price ? null : (float) $price;
},
'update_callback' => function ( WC_Product $product, $value ) {
if ( null === $value ) {
$product->delete_meta_data( '_giftwrap_price' );
return;
}
$product->update_meta_data( '_giftwrap_price', $value );
},
)
);
ExtensibleAbilities::register_query_filter(
array(
'resource' => 'product',
'namespace' => 'giftwrap',
'filter' => 'available',
'schema' => array(
'type' => 'boolean',
'description' => 'True lists products that can be gift wrapped. False lists the others.',
),
'callback' => function ( bool $available ) {
return array(
'meta_query' => array(
array(
'key' => '_giftwrap_price',
'compare' => $available ? 'EXISTS' : 'NOT EXISTS',
),
),
);
},
)
);
ExtensibleAbilities::register_validator(
array(
'resource' => 'product',
'namespace' => 'giftwrap',
'callback' => function ( WC_Product $product ) {
if ( $product->is_virtual() && '' !== $product->get_meta( '_giftwrap_price' ) ) {
return new WP_Error( 'giftwrap_virtual_product', 'A virtual product cannot be gift wrapped. Remove the gift wrap price first.' );
}
return null;
},
)
);
}
);An agent then reads the field from woocommerce/products-query:
{
"products": [
{
"id": 42,
"name": "Coffee Box",
"extensions": {
"giftwrap": { "price": 4.5 }
}
}
]
}It changes the name and the gift wrap price in one call to woocommerce/product-update, and WooCommerce saves the product one time:
{
"id": 42,
"name": "Coffee Club",
"extensions": {
"giftwrap": { "price": 5 }
}
}And it lists the products that can be gift-wrapped with { "filters": { "extensions": { "giftwrap": { "available": true } } } }.
Use the API in your own abilities
An extension can make its own abilities extensible too. Other extensions can then add fields, filters and validators to them, the same way they do for products and orders.
For example, a support ticket extension stores tickets in My_Ticket, a class that extends WC_Data. An SLA extension wants to add a due_at field to tickets with register_field() and 'resource' => 'ticket'. The ticket extension needs three new calls for that. The rest of each ability is registered as usual.
1. Register the resource. Other extensions use the resource name to add fields to tickets.
<?php
add_action( 'woocommerce_extensible_abilities_resources_init', function () {
ExtensibleAbilities::register_resource( 'ticket' );
} );2. Add the fields to the read ability. add_fields_schema() adds the fields to the output schema, and add_fields_to_object() adds their values to the output.
<?php
wp_register_ability( 'my-tickets/ticket-get', array(
// label, description, category, input_schema and permission_callback as usual.
'output_schema' => ExtensibleAbilities::add_fields_schema( $ticket_schema, 'ticket' ),
'execute_callback' => function ( array $input ) {
$ticket = My_Ticket::find( $input['id'] );
return ExtensibleAbilities::add_fields_to_object(
array( 'id' => $ticket->get_id(), 'subject' => $ticket->get_subject() ),
'ticket',
$ticket
);
},
) );my-tickets/ticket-get now returns the SLA field:
{
"id": 7,
"subject": "Refund request",
"extensions": {
"sla": { "due_at": "2026-10-22T12:00:00" }
}
}3. Build the write ability on ActionableAbility. The ability gives four small methods, and ActionableAbility runs the rest: it writes the extension fields, runs the validators and saves one time. It also adds the extensions input to the schema, so the write ability needs no execute_callback.
<?php
class My_Ticket_Update extends ActionableAbility {
public function get_resource(): string {
return 'ticket';
}
public function load( array $input ) {
return My_Ticket::find( $input['id'] )
?? new WP_Error( 'my_tickets_not_found', 'Ticket not found.', array( 'status' => 404 ) );
}
public function change( $ticket, array $input ) {
if ( isset( $input['subject'] ) ) {
$ticket->set_subject( $input['subject'] );
}
return null;
}
public function prepare_response( $ticket ) {
return my_tickets_format( $ticket );
}
}
wp_register_ability( 'my-tickets/ticket-update', array(
// label, description, category, schemas and permission_callback as usual.
'ability_class' => My_Ticket_Update::class,
) );my-tickets/ticket-update accepts the field too, and it saves the subject and the due date in one save:
{
"id": 7,
"subject": "Refund request: order 1234",
"extensions": {
"sla": { "due_at": "2026-10-23T12:00:00" }
}
}For every argument and callback rule, see the docblocks in ExtensibleAbilities and ActionableAbility.
Known gaps
- Order writes. Extensions can read and filter order fields, but Core’s order abilities do not write them yet.
woocommerce/order-update-statuskeeps its current write. - Custom product types. The product abilities support only a fixed list of product types. Product types that extensions register, such as
subscriptionin Subscriptions, are not on that list. - Variable products and multi-step saves. A variable product needs several saves: one for the parent and one for each variation. Currently, product create/update only performs one save.
Try it and tell us what you think
We’re still changing the design and the API, so please don’t use it in production yet. We welcome testing and feedback from extension developers. Tell us which fields you would add and where the API gets in your way in the GitHub discussion
Leave a Reply