Background jobs: Action Scheduler
Long work — sending an order to an accounting system, updating stock, mailing emails — must not run while a customer waits for a page. It is put into a queue. WooCommerce ships with the Action Scheduler library: a job is written to the database and run later by a separate request.
How it works
A job is a hook name plus arguments. You put it into the queue, and Action Scheduler calls do_action() with that name in the background. The job code is an ordinary function attached to the hook.
Where to write the code
Create the includes/jobs.php file and include it in acme-shop.php.
An as-soon-as-possible job
// wp-content/plugins/acme-shop/includes/jobs.php
add_action('woocommerce_order_status_processing', function (int $orderId) {
as_enqueue_async_action('acme_export_order', ['order_id' => $orderId], 'acme');
});
add_action('acme_export_order', function (int $orderId) {
$order = wc_get_order($orderId);
if (!$order || $order->get_meta('_acme_exported') === 'yes') {
return;
}
$response = wp_remote_post('https://erp.example.com/orders', [
'headers' => ['Content-Type' => 'application/json'],
'body' => wp_json_encode(['id' => $orderId, 'total' => $order->get_total()]),
]);
if (is_wp_error($response)) {
throw new Exception($response->get_error_message());
}
$order->update_meta_data('_acme_exported', 'yes');
$order->save();
});The first function fires when an order is paid and only enqueues the job — that is instant. The second runs in the background and sends the order. The arguments from the array are passed to the function in order. The third argument, acme, is the group that makes the plugin's jobs easy to find.
Functions
| Function | What it does |
|---|---|
as_enqueue_async_action($hook, $args, $group) | Run as soon as possible |
as_schedule_single_action($time, $hook, $args, $group) | Run once at the given time |
as_schedule_recurring_action($time, $interval, $hook, $args, $group) | Repeat with an interval in seconds |
as_has_scheduled_action($hook, $args, $group) | Whether such a job is in the queue |
as_unschedule_all_actions($hook, $args, $group) | Cancel jobs |
A delayed job
// wp-content/plugins/acme-shop/includes/jobs.php
add_action('woocommerce_order_status_completed', function (int $orderId) {
as_schedule_single_action(time() + 7 * DAY_IN_SECONDS, 'acme_ask_review', ['order_id' => $orderId], 'acme');
});
add_action('acme_ask_review', function (int $orderId) {
$order = wc_get_order($orderId);
if ($order) {
wp_mail($order->get_billing_email(), 'How was your purchase?', 'Please leave a review of the product on the site.');
}
});A week after the order is completed the customer receives an email asking for a review.
A recurring job
// wp-content/plugins/acme-shop/includes/jobs.php
add_action('init', function () {
if (!as_has_scheduled_action('acme_sync_stock', [], 'acme')) {
as_schedule_recurring_action(time(), HOUR_IN_SECONDS, 'acme_sync_stock', [], 'acme');
}
});
add_action('acme_sync_stock', function () {
// request stock from the external system and update products
});The as_has_scheduled_action() check prevents creating a new job on every page load. The sync runs once an hour.
The queue in the admin panel
Open WooCommerce → Status → Scheduled Actions. The list shows jobs with the Pending, Complete and Failed statuses, and you can type acme_export_order into the search. A pending job is run by hand with the Run link under its name. If the function threw an exception, the job lands in Failed and the error text is visible in the log column.
Running from the console
By default the queue is processed while someone visits the site. On a production server it is run by the system scheduler:
wp action-scheduler run --group=acmeThe command runs all due jobs of the group and prints their number.
Common mistake
A job is not designed to run twice. The network dropped after sending, the job was run by hand once more — and the order went to the accounting system twice. Before acting, check whether it has already been done: in the example the _acme_exported mark serves that purpose.