Process Background Jobs in WordPress with Action Scheduler

Action Scheduler is an open-source background job queue library developed by WooCommerce and bundled with WooCommerce core since version 3.0 — it is also available as a standalone plugin and is used by hundreds of plugins for reliable deferred and recurring task processing. Unlike WordPress’s built-in wp-cron, which depends on HTTP requests to trigger scheduled events and silently misses jobs on low-traffic sites, Action Scheduler uses a database table (wp_actionscheduler_actions) as a persistent queue, supports concurrent claim locking to prevent duplicate execution, provides a full admin UI at Tools → Scheduled Actions, and retries failed actions with configurable retry logic. Scheduling a one-time background job is done with as_schedule_single_action( time() + 300, 'my_hook', [ $arg1, $arg2 ] ) — the hook fires 5 minutes from now with the provided arguments in a separate request, off the user’s current request. Recurring actions use as_schedule_recurring_action( time(), 3600, 'my_hourly_hook' ) — the interval is in seconds and the library recalculates the next run time after each execution. Unique actions can be enforced by checking as_has_scheduled_action( 'my_hook', [ $arg ] ) before scheduling — preventing duplicate queue entries when the same action gets triggered multiple times during a bulk operation. The Action Scheduler queue processor runs as a WP-Cron hook but can be triggered asynchronously via a loopback HTTP request or via WP-CLI (wp action-scheduler run) for reliable execution in CI/CD pipelines and high-throughput environments. Grouping related actions with the $group parameter allows filtering the admin UI and bulk-managing all actions in a batch import or export operation. The library handles timeouts, retries (3 attempts by default with exponential backoff), and complete audit logging of past action execution status. The systemd timers post covers system-level scheduling; Action Scheduler is the application-level job queue that runs within WordPress itself.

Problem: A WooCommerce store sends a post-purchase email sequence — a welcome email immediately, a review request after 7 days, and a repurchase discount after 30 days — implemented with wp-cron. Orders placed during server downtime miss their scheduled emails, and there is no way to see which emails were sent or debug failures.

Solution: Replace the wp-cron implementation with Action Scheduler: schedule the three email actions when an order is marked complete, check for duplicate schedules before adding, and use the admin UI to monitor sent/failed actions and retry any that errored.

// ── Schedule email sequence when an order is completed ───────────────────
add_action( 'woocommerce_order_status_completed', 'myplugin_schedule_email_sequence', 10, 1 );

function myplugin_schedule_email_sequence( int $order_id ): void {
    $group = 'purchase-emails';

    // Immediately: welcome email (fire in 60 seconds to let order fully save)
    if ( ! as_has_scheduled_action( 'myplugin_welcome_email', [ $order_id ], $group ) ) {
        as_schedule_single_action(
            time() + 60,
            'myplugin_welcome_email',
            [ $order_id ],
            $group
        );
    }

    // 7 days: review request
    if ( ! as_has_scheduled_action( 'myplugin_review_request', [ $order_id ], $group ) ) {
        as_schedule_single_action(
            time() + 7 * DAY_IN_SECONDS,
            'myplugin_review_request',
            [ $order_id ],
            $group
        );
    }

    // 30 days: repurchase discount
    if ( ! as_has_scheduled_action( 'myplugin_repurchase_discount', [ $order_id ], $group ) ) {
        as_schedule_single_action(
            time() + 30 * DAY_IN_SECONDS,
            'myplugin_repurchase_discount',
            [ $order_id ],
            $group
        );
    }
}

// ── Action handlers ───────────────────────────────────────────────────────
add_action( 'myplugin_welcome_email', function( int $order_id ): void {
    $order = wc_get_order( $order_id );
    if ( ! $order ) return;
    // Send email using WooCommerce mailer...
    WC()->mailer()->emails['WC_Email_Customer_Processing_Order']
        ->trigger( $order_id, $order );
} );

add_action( 'myplugin_review_request', function( int $order_id ): void {
    $order = wc_get_order( $order_id );
    if ( ! $order || $order->get_status() === 'refunded' ) return;
    // Send review request transactional email...
} );

// ── Recurring: daily digest of new orders (for admin reporting) ───────────
add_action( 'init', function(): void {
    if ( ! as_has_scheduled_action( 'myplugin_daily_order_digest' ) ) {
        as_schedule_recurring_action(
            strtotime( 'tomorrow 08:00:00' ),
            DAY_IN_SECONDS,
            'myplugin_daily_order_digest',
            [],
            'admin-reports'
        );
    }
} );

add_action( 'myplugin_daily_order_digest', function(): void {
    $yesterday = strtotime( 'yesterday' );
    $orders    = wc_get_orders( [
        'date_created' => '>' . $yesterday,
        'status'       => 'completed',
        'limit'        => -1,
        'return'       => 'ids',
    ] );
    // Send digest email to admin...
    error_log( 'Daily digest: ' . count( $orders ) . ' orders processed.' );
} );

NOTE: Action Scheduler ships bundled with WooCommerce — if your plugin requires Action Scheduler independently of WooCommerce, include the library in your plugin using Composer (woocommerce/action-scheduler) and call ActionScheduler_Versions::register() in your plugin bootstrap. The library automatically activates the highest registered version, so multiple plugins bundling different versions coexist safely without conflicts. Do not call require on the Action Scheduler files directly — use the registration API to let the version manager select the correct copy.