wp-action-scheduler — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited wp-action-scheduler (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Action Scheduler is a WordPress-native job queue used by WooCommerce and many high-volume plugins. It is not just a nicer wp_schedule_event() wrapper: it stores actions in queue tables, tracks status, claims batches, logs attempts, supports groups, and can run through WP-Cron, async loopback requests, admin tools, and WP-CLI.
Use wp-plugin-cron first when the main question is "native WP-Cron or Action Scheduler?". Use this skill once the answer is Action Scheduler or the plugin already depends on it.
cron-expression, unique, chunked, and activation/deactivation flows: read references/api-patterns.md.
or WP-CLI/admin operations: read references/operational-debugging.md.
"Action Scheduler means the callback will run once, soon, and in order."
Wrong mental model. Action Scheduler is an at-least-once background queue. Jobs can run late, fail, be retried manually, be re-created by recurring schedules, or be triggered by WP-CLI/admin tools. Callbacks must be idempotent and should advance durable state, not rely on "this hook only fires once".
Trigger when ANY of the following is true:
as_enqueue_async_action(), as_schedule_single_action(),as_schedule_recurring_action(), or as_schedule_cron_action().
pending actions, failed actions, oroversized queue tables.
$unique, as_has_scheduled_action(), oras_next_scheduled_action() guards.
Action Scheduler can be present as:
Do not assume your plugin owns the loaded version. Action Scheduler registers available versions and initializes the latest registered version. In local 3.9.3, registration happens on plugins_loaded priority 0, initialization loads the procedural API, and action_scheduler_init fires when the store, logger, runner, admin view, and recurring scheduler are ready.
Rules:
add_action( 'myplugin/process_order', ... ) should not be hidden behind an admin-only screen load.
action_scheduler_init when scheduling atruntime.
function_exists( 'as_schedule_single_action' )before calling the API. If Action Scheduler is an optional dependency, fall back to WP-Cron or show an admin notice.
load.
add_action( 'action_scheduler_init', static function (): void {
if ( ! as_has_scheduled_action( 'myplugin/hourly_sync', array(), 'myplugin' ) ) {
as_schedule_recurring_action(
time() + HOUR_IN_SECONDS,
HOUR_IN_SECONDS,
'myplugin/hourly_sync',
array(),
'myplugin',
true
);
}
} );Scheduling functions return an action ID as int; 0 means scheduling failed.
| Function | Use |
|---|---|
as_enqueue_async_action( $hook, $args = array(), $group = '', $unique = false, $priority = 10 ) | Run once as soon as possible. |
as_schedule_single_action( $timestamp, $hook, $args = array(), $group = '', $unique = false, $priority = 10 ) | Run once at/after a Unix timestamp. |
as_schedule_recurring_action( $timestamp, $interval_in_seconds, $hook, $args = array(), $group = '', $unique = false, $priority = 10 ) | Fixed interval recurrence in seconds. |
as_schedule_cron_action( $timestamp, $schedule, $hook, $args = array(), $group = '', $unique = false, $priority = 10 ) | Cron-expression recurrence. |
as_unschedule_action( $hook, $args = array(), $group = '' ) | Cancel the next pending matching action. |
as_unschedule_all_actions( $hook, $args = array(), $group = '' ) | Cancel all pending matching actions. |
as_next_scheduled_action( $hook, $args = null, $group = '' ) | Return next timestamp, true for async/running, or false. |
as_has_scheduled_action( $hook, $args = null, $group = '' ) | Efficient boolean check for pending/running actions. |
as_get_scheduled_actions( $args = array(), $return_format = OBJECT ) | Query actions by hook, group, status, date, etc. |
as_get_datetime_object( $date_string = null, $timezone = 'UTC' ) | Build AS DateTime object for queries. |
as_supports( $feature ) | Feature detection. In 3.9.3 it supports ensure_recurring_actions_hook. |
The $priority parameter is queue priority, not callback priority. Lower numbers run first; 3.9.3 expects 0-255 and defaults to 10.
myplugin/process_order, not process_order.myplugin, myplugin-import,myplugin-webhooks.
larger encoded args in extended_args, but validates against an 8000-character encoded limit and still hashes/indexes args for lookup.
WC_Order objects, full API payloads, orsecrets. Reload current state inside the callback.
array_values( $args ) )`. Associative keys are for storage/query readability, not named parameter delivery.
jobs, pass the exact args. For deactivation, prefer canceling a plugin-owned group with an empty hook if the group is exclusive to your plugin.
as_enqueue_async_action(
'myplugin/process_order',
array( 'order_id' => 123 ),
'myplugin'
);
add_action(
'myplugin/process_order',
static function ( int $order_id ): void {
myplugin_process_order( $order_id );
},
10,
1
);In 3.9.3, the scheduling functions support $unique. The public docs in the local source say a unique action is not scheduled when another pending or running action has the same hook and group parameters. The 3.9.3 DB store matches pending/running uniqueness by hook and group, not by args.
Use $unique = true only for "only one pending/running copy of this hook+group should exist", such as a global reindex, import coordinator, or recurring sync. Do not use it for per-order or per-user jobs under one hook/group unless only one outstanding job for the whole group is intended.
For per-entity duplicate suppression, check the exact args with as_has_scheduled_action() and still make the callback idempotent. That guard is not a substitute for durable state because it is not a hard business lock.
if ( function_exists( 'as_enqueue_async_action' ) ) {
$ref = new ReflectionFunction( 'as_enqueue_async_action' );
if ( $ref->getNumberOfParameters() >= 4 ) {
as_enqueue_async_action( 'myplugin/reindex', array(), 'myplugin', true );
} elseif ( ! as_has_scheduled_action( 'myplugin/reindex', array(), 'myplugin' ) ) {
as_enqueue_async_action( 'myplugin/reindex', array(), 'myplugin' );
}
}Do not use uniqueness as the only safety mechanism for payment capture, inventory mutation, email send, or external API side effects. The callback must still check durable state.
$args = array( 'order_id' => $order_id );
if ( ! as_has_scheduled_action( 'myplugin/process_order', $args, 'myplugin' ) ) {
as_enqueue_async_action( 'myplugin/process_order', $args, 'myplugin' );
}add_action(
'myplugin/capture_payment',
static function ( int $order_id ): void {
$order = wc_get_order( $order_id );
if ( ! $order ) {
return;
}
if ( 'yes' === $order->get_meta( '_myplugin_payment_captured', true ) ) {
return;
}
myplugin_capture_payment_for_order( $order );
$order->update_meta_data( '_myplugin_payment_captured', 'yes' );
$order->save();
},
10,
1
);For failures that should be retried or surfaced, throw an exception. For permanent no-op cases, return cleanly. Swallowing all exceptions makes failures look complete and hides broken jobs from the admin UI and logs.
For plugin-owned recurring actions:
action_scheduler_init.action_scheduler_ensure_recurring_actions in AS 3.9.3+ when you need arepair hook for recurring actions that may have been deleted manually.
add_action( 'action_scheduler_ensure_recurring_actions', static function (): void {
if ( ! function_exists( 'as_supports' ) || ! as_supports( 'ensure_recurring_actions_hook' ) ) {
return;
}
if ( ! as_has_scheduled_action( 'myplugin/hourly_sync', array(), 'myplugin' ) ) {
as_schedule_recurring_action(
time() + HOUR_IN_SECONDS,
HOUR_IN_SECONDS,
'myplugin/hourly_sync',
array(),
'myplugin',
true
);
}
} );Core statuses in 3.9.3:
pendingin-progresscompletefailedcanceledThe DB store uses prefixed tables based on these base names:
actionscheduler_actionsactionscheduler_claimsactionscheduler_groupsactionscheduler_logsDo not write direct SQL for normal plugin behavior. Use the public API, admin UI, or WP-CLI. Direct SQL is acceptable only for emergency diagnostics with a backup and site-specific approval.
The default queue runner schedules WP-Cron hook action_scheduler_run_queue every minute and can dispatch async admin-context requests on shutdown. Default web runner batch size is filterable through action_scheduler_queue_runner_batch_size and defaults to 25.
Admin UI: Tools -> Scheduled Actions.
Common WP-CLI commands in 3.9.3:
wp action-scheduler action list --group=myplugin --status=pending
wp action-scheduler action next myplugin/process_order --group=myplugin
wp action-scheduler action get 123 --format=json
wp action-scheduler action logs 123
wp action-scheduler action run 123
wp action-scheduler run --group=myplugin --batch-size=25 --batches=1
wp action-scheduler clean --status=complete,canceled --before='31 days ago'
wp action-scheduler fix-schemaUse WP-CLI for deterministic local/dev runs and for production debugging when the web runner is too slow or loopback requests are blocked.
action_scheduler_init as the safe runtime scheduling point.handlers.
$unique = true only for hook+group singleton suppression; still makecallbacks idempotent.
as_has_scheduled_action() for a boolean guard; useas_next_scheduled_action() only when you need the timestamp.
// WRONG - callback hidden in an admin screen, runner cannot find it from WP-Cron.
if ( is_admin() && isset( $_GET['page'] ) && 'myplugin' === $_GET['page'] ) {
add_action( 'myplugin/process_order', 'myplugin_process_order' );
}
// WRONG - passes a large payload and secrets through queued args.
as_enqueue_async_action( 'myplugin/send_payload', $full_api_payload, 'myplugin' );
// WRONG - associative args treated as named callback parameters.
add_action( 'myplugin/process_order', static function ( array $args ): void {
myplugin_process_order( $args['order_id'] );
}, 10, 1 );
// RIGHT - pass ID, reload state, receive positional callback arg.
as_enqueue_async_action(
'myplugin/process_order',
array( 'order_id' => $order_id ),
'myplugin'
);
add_action( 'myplugin/process_order', 'myplugin_process_order', 10, 1 );
// WRONG - this only matches empty-arg actions for this hook+group. It will not
// clear per-order jobs scheduled with array( 'order_id' => $order_id ).
as_unschedule_all_actions( 'myplugin/process_order', array(), 'myplugin' );
// RIGHT - exact hook + args + group for one per-entity job.
as_unschedule_all_actions(
'myplugin/process_order',
array( 'order_id' => $order_id ),
'myplugin'
);
// RIGHT - on deactivation, clear all pending actions in an exclusive
// plugin-owned group.
as_unschedule_all_actions( '', array(), 'myplugin' );wp-plugin-cron before this when choosing between WP-Cron and ActionScheduler.
wp-plugin-lifecycle for activation/deactivation structure and multisiteactivation behavior.
wp-plugin-dto when queued args should hydrate a stable input objectinside the callback.
wp-plugin-presenter when an action produces admin/REST/email output.wp-security-audit for callbacks processing persisted IDs, external APIpayloads, or user-supplied data.
wp-plugin-cron.combine this with the relevant WooCommerce skill.
Validated against the local Action Scheduler 3.9.3 plugin installed at wp-content/plugins/action-scheduler on 2026-04-29:
functions.php public API signatures.ActionScheduler::init() and action_scheduler_init timing.ActionScheduler_Action::execute() positional arg delivery.ActionScheduler_Store statuses and DB store args length behavior.ActionScheduler_QueueRunner runner hook and batch size.classes/WP_CLI.~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.