Lesson 1 covered the seven official WooCommerce abilities, and none of them reach down
to the variation level or handle bulk stock scanning. woocommerce/product-update can
change a simple product’s stock, but a store selling shirts in five sizes and four
colors needs something that reasons about individual variations, and a low-stock sweep
across the whole catalog. That’s a real gap, and it’s exactly the kind of thing this
course’s custom abilities are for.
A WooCommerce install with at least one variable product (varying by size, color, or
similar) and its variations carrying individual stock quantities. WordPress 6.9+ for
the Abilities API. WooCommerce version doesn’t need to be 10.9+ for this lesson, the
functions used here (wc_get_product(), set_stock_quantity())
have been stable in WooCommerce for years.
Step 1: Registering a category for your custom abilities
Keep custom abilities visually and organizationally distinct from WooCommerce’s own, so anyone (or any agent) browsing the list can tell them apart:
// File: my-plugin.php
add_action( 'wp_abilities_api_categories_init', function () {
wp_register_ability_category(
'store-inventory',
array( 'label' => __( 'Store inventory (custom)', 'my-plugin' ) )
);
} );
Step 2: A read-only ability, finding low-stock variations
This ability loops every variable product, checks each variation’s stock quantity
against a threshold, and returns a flat list, exactly the kind of catalog-wide sweep
woocommerce/products-query isn’t built for.
// File: my-plugin.php
add_action( 'wp_abilities_api_init', function () {
wp_register_ability(
'my-plugin/find-low-stock-variations',
array(
'label' => __( 'Find low-stock product variations', 'my-plugin' ),
'description' => __( 'Scans all variable products and returns variations at or below a given stock threshold.', 'my-plugin' ),
'category' => 'store-inventory',
'input_schema' => array(
'type' => 'object',
'properties' => array(
'threshold' => array( 'type' => 'integer', 'default' => 5 ),
),
),
'output_schema' => array(
'type' => 'object',
'properties' => array(
'variations' => array( 'type' => 'array' ),
),
),
'permission_callback' => function () {
return current_user_can( 'manage_woocommerce' );
},
'execute_callback' => function ( $input ) {
$threshold = $input['threshold'] ?? 5;
$low_stock = array();
$variable_products = wc_get_products( array(
'type' => 'variable',
'limit' => -1,
'status' => 'publish',
) );
foreach ( $variable_products as $parent ) {
foreach ( $parent->get_children() as $variation_id ) {
$variation = wc_get_product( $variation_id );
if ( ! $variation || ! $variation->managing_stock() ) {
continue;
}
$qty = $variation->get_stock_quantity();
if ( null !== $qty && $qty <= $threshold ) {
$low_stock[] = array(
'variation_id' => $variation_id,
'parent_name' => $parent->get_name(),
'attributes' => $variation->get_attributes(),
'stock' => $qty,
);
}
}
}
return array( 'variations' => $low_stock );
},
'meta' => array(
'annotations' => array( 'readonly' => true ),
'mcp' => array( 'public' => true ),
),
)
);
} );
Step 3: A write ability, adjusting one variation’s stock
This is the write half: given a variation ID and a new quantity, it updates stock and keeps stock status consistent.
// File: my-plugin.php
add_action( 'wp_abilities_api_init', function () {
wp_register_ability(
'my-plugin/adjust-variant-stock',
array(
'label' => __( 'Adjust product variation stock', 'my-plugin' ),
'description' => __( 'Sets the stock quantity for a specific product variation and updates its stock status accordingly.', 'my-plugin' ),
'category' => 'store-inventory',
'input_schema' => array(
'type' => 'object',
'properties' => array(
'variation_id' => array( 'type' => 'integer' ),
'quantity' => array( 'type' => 'integer', 'minimum' => 0 ),
),
'required' => array( 'variation_id', 'quantity' ),
),
'output_schema' => array(
'type' => 'object',
'properties' => array(
'success' => array( 'type' => 'boolean' ),
'new_status' => array( 'type' => 'string' ),
),
),
'permission_callback' => function () {
return current_user_can( 'manage_woocommerce' );
},
'execute_callback' => function ( $input ) {
$variation = wc_get_product( $input['variation_id'] );
if ( ! $variation ) {
return array( 'success' => false, 'new_status' => '' );
}
$variation->set_stock_quantity( $input['quantity'] );
$variation->set_stock_status( $input['quantity'] > 0 ? 'instock' : 'outofstock' );
$variation->save();
return array(
'success' => true,
'new_status' => $variation->get_stock_status(),
);
},
'meta' => array(
'annotations' => array( 'readonly' => false, 'destructive' => false, 'idempotent' => true ),
'mcp' => array( 'public' => false ),
),
)
);
} );
Notice the write ability keeps meta.mcp.public false, unlike the read-only sweep. An
agent finding low-stock variations is safe to expose broadly, an agent adjusting real
stock numbers is not, until you’ve decided who’s allowed to trigger it and reviewed
that decision.
Step 4: Why manage_woocommerce, not edit_posts
Test it
wp-env run cli wp eval '
$ability = wp_get_ability( "my-plugin/find-low-stock-variations" );
var_dump( $ability->execute( array( "threshold" => 10 ) ) );
'
Then test the write path against a known variation ID from that output:
wp-env run cli wp eval '
$ability = wp_get_ability( "my-plugin/adjust-variant-stock" );
var_dump( $ability->execute( array( "variation_id" => 123, "quantity" => 20 ) ) );
'
WC_Product objects (variations included) are change-tracked, calling
set_stock_quantity() alone does nothing to the database until save() runs. Just as
common: checking get_stock_quantity() before confirming managing_stock() is even
true for that variation, a variation not managing its own stock returns null for
quantity, which your low-stock comparison needs to explicitly skip, not silently treat
as zero.
Recap
The official abilities cover simple-product stock changes but not variation-level
sweeps or targeted adjustments, so a custom ability pair fills that gap: a read-only
scan built on wc_get_products() and get_stock_quantity(), and a write ability built
on set_stock_quantity(), set_stock_status(), and save(). Scoping the write
ability’s permission check to manage_woocommerce and keeping it off meta.mcp.public
until reviewed follows the same default-safe pattern from Course 1.
Resources & further reading
- WC_Product class reference, woocommerce.github.io
- wc_get_products() function reference, woocommerce.github.io
- Managing Stock in WooCommerce, woocommerce.com
- Abilities API reference, developer.wordpress.org