WooCommerce Action Scheduler jobs
Action Scheduler is bundled with WooCommerce and is the right tool for background work: order sync, product imports, webhooks, retries, batch recalculation, export jobs, and recurring maintenance.
Use it instead of doing slow work during checkout, order status hooks, admin saves, or frontend requests.
Misconception this skill corrects
"I will pass
$unique = trueso there is only one job per order ID."
In WooCommerce 10.8's DB store, unique inserts guard by pending/running hook + group. They do not include args in the SQL uniqueness check. If you schedule myplugin_sync_order with group myplugin and $unique = true, a pending job for order 10 can block scheduling order 11.
For per-order uniqueness, check as_has_scheduled_action( $hook, $args, $group ) with the exact args, then schedule normally.
When to use this skill
Trigger when ANY of the following is true:
- Moving slow work out of checkout, webhooks, admin saves, or order status hooks.
- Scheduling order/customer/product sync jobs.
- Running imports, exports, cleanup, reindexing, or recurring maintenance.
- Avoiding duplicate background jobs.
- The diff contains
as_enqueue_async_action,as_schedule_single_action,as_schedule_recurring_action,as_schedule_cron_action,as_has_scheduled_action,as_next_scheduled_action,as_unschedule_action, orActionScheduler.
API map
| Need | Function |
|---|---|
| Run as soon as possible | as_enqueue_async_action( $hook, $args, $group, $unique, $priority ) |
| Run once at a timestamp | as_schedule_single_action( $timestamp, $hook, $args, $group, $unique, $priority ) |
| Run repeatedly by interval | as_schedule_recurring_action( $timestamp, $interval, $hook, $args, $group, $unique, $priority ) |
| Run repeatedly by cron expression | as_schedule_cron_action( $timestamp, $schedule, $hook, $args, $group, $unique, $priority ) |
| Check pending/running action efficiently | as_has_scheduled_action( $hook, $args, $group ) |
Get next timestamp or running/async true |
as_next_scheduled_action( $hook, $args, $group ) |
| Cancel next matching pending action | as_unschedule_action( $hook, $args, $group ) |
| Cancel all matching actions | as_unschedule_all_actions( $hook, $args, $group ) |
Always set a plugin-specific group, for example myplugin. It makes admin filtering, CLI runs, and cleanup safer.
Queue from an order hook
add_action(
'woocommerce_order_status_processing',
static function ( int $order_id ): void {
$hook = 'myplugin_sync_order';
$args = array( 'order_id' => $order_id );
$group = 'myplugin';
if ( as_has_scheduled_action( $hook, $args, $group ) ) {
return;
}
as_enqueue_async_action( $hook, $args, $group );
}
);
add_action(
'myplugin_sync_order',
static function ( int $order_id ): void {
$order = wc_get_order( $order_id );
if ( ! $order instanceof WC_Order ) {
return;
}
// Make this idempotent: if the remote sync already happened, exit.
if ( $order->get_meta( '_myplugin_synced_at' ) ) {
return;
}
myplugin_sync_order_to_remote_system( $order );
$order->update_meta_data( '_myplugin_synced_at', current_time( 'mysql', true ) );
$order->save();
},
10,
1
);
Action args are serialized. Pass scalar IDs and small arrays, not WC_Order, WC_Product, closures, HTTP clients, or service objects. Load fresh objects inside the callback.
Single delayed job
$hook = 'myplugin_follow_up_order';
$args = array( 'order_id' => $order_id );
$group = 'myplugin';
if ( ! as_has_scheduled_action( $hook, $args, $group ) ) {
as_schedule_single_action( time() + HOUR_IN_SECONDS, $hook, $args, $group );
}
as_next_scheduled_action() returns a timestamp for a pending scheduled action, true for running/async, and false for no match. Use as_has_scheduled_action() when you only need a boolean.
Recurring job on activation/deactivation
Guard for sites where WooCommerce or Action Scheduler is unavailable.
register_activation_hook(
MYPLUGIN_FILE,
static function (): void {
if ( ! function_exists( 'as_has_scheduled_action' ) ) {
return;
}
if ( ! as_has_scheduled_action( 'myplugin_hourly_maintenance', array(), 'myplugin' ) ) {
as_schedule_recurring_action(
time() + 5 * MINUTE_IN_SECONDS,
HOUR_IN_SECONDS,
'myplugin_hourly_maintenance',
array(),
'myplugin'
);
}
}
);
register_deactivation_hook(
MYPLUGIN_FILE,
static function (): void {
if ( function_exists( 'as_unschedule_all_actions' ) ) {
as_unschedule_all_actions( 'myplugin_hourly_maintenance', array(), 'myplugin' );
}
}
);
Keep recurring callbacks small. If one run may need to process thousands of rows, split it into batches.
Batch pattern
add_action(
'myplugin_rebuild_product_cache',
static function ( int $offset = 0 ): void {
$product_ids = myplugin_get_product_ids_for_rebuild( $offset, 100 );
foreach ( $product_ids as $product_id ) {
myplugin_rebuild_one_product_cache( (int) $product_id );
}
if ( count( $product_ids ) === 100 ) {
as_schedule_single_action(
time() + 30,
'myplugin_rebuild_product_cache',
array( 'offset' => $offset + 100 ),
'myplugin'
);
}
},
10,
1
);
For imports that can change while the batch runs, prefer a cursor based on IDs or timestamps instead of an offset.
CLI operations
WooCommerce's bundled Action Scheduler registers WP-CLI commands. Useful smoke/debug commands:
wp action-scheduler run --group=myplugin --batch-size=25 --path=/path/to/site
wp action-scheduler action list --group=myplugin --status=pending --path=/path/to/site
wp action-scheduler action get 123 --path=/path/to/site
Use the CLI runner for deterministic local tests and production maintenance windows. It accepts --hooks, --group, --exclude-groups, --batch-size, --batches, and --force.
Common mistakes
- Using
$unique = truefor per-order/per-product jobs. In WC 10.8 DB store, that is hook+group uniqueness, not args-level uniqueness. - Omitting the group and later being unable to isolate your jobs.
- Passing objects or closures as args.
- Doing slow external API calls directly in WooCommerce hooks instead of queueing.
- Assuming a job can run only once. Crashes, timeouts, manual CLI runs, or duplicate scheduling can happen; callbacks must be idempotent.
- Scheduling a recurring action on every page load without checking for an existing pending/running one.
- Forgetting to unschedule recurring plugin jobs on deactivation.
Cross-skill routing
- Order lifecycle hooks that enqueue jobs:
wc-order-lifecycle-and-items - Subscription renewal scheduler:
wcs-renewal-scheduler - HPOS-safe order reads/writes inside jobs:
wc-hpos-compatibility - Store API/block cart updates that need async follow-up:
wc-store-api