WP Webhooks / Blog / WordPress internals
Article · WordPress internals

The Action Scheduler Database Schema: All Four Tables

wp_actionscheduler_actions database schema — every column in the actions, claims, groups and logs tables, their indexes, and how the queue runner uses them.

8 min 2026-07-23
#Action Scheduler#Database

TL;DR: Action Scheduler stores every background job across four MySQL tables, all prefixed wp_actionscheduler_.

  • wp_actionscheduler_actions — one row per scheduled job (hook, args, status, scheduled date).
  • wp_actionscheduler_claims — one row per batch a runner locks, handing out a claim_id.
  • wp_actionscheduler_groups — a slug lookup so actions can be grouped and filtered.
  • wp_actionscheduler_logs — one row per lifecycle event and execution attempt.

/ Overview

What tables does Action Scheduler create?

Action Scheduler — the background-job library maintained by WooCommerce and bundled inside WooCommerce, Jetpack, and hundreds of other plugins — creates four custom tables the first time it loads. On a standard single install they are wp_actionscheduler_actions, wp_actionscheduler_claims, wp_actionscheduler_groups, and wp_actionscheduler_logs (your prefix replaces wp_). Together they replace the single serialised cron option that WP-Cron uses, giving each job its own indexed row, a lock, a group, and an audit trail.

The default data store is the custom-table store shipped with the library. Older or constrained installs can fall back to a post-type store that keeps actions in wp_posts, but the custom tables are the standard and the ones you will inspect in production. The Scheduled Actions admin screen reads directly from them.

The four Action Scheduler database tables and the claim cycleAction Scheduler stores every job as one row in wp_actionscheduler_actions, tagged by a slug in wp_actionscheduler_groups. To run work, the queue runner inserts a row in wp_actionscheduler_claims to get a claim_id, stamps that claim_id onto a batch of due pending actions so no other runner can take them, executes each hook, and writes a per-attempt message to wp_actionscheduler_logs before releasing the claim.

yes

threw

wp_actionscheduler_groups
group slug lookup

wp_actionscheduler_actions
hook, args, status, scheduled_date

queue runner: find due pending actions
scheduled_date <= now

wp_actionscheduler_claims
new claim_id

stamp claim_id onto the batch
status = in-progress

do_action( hook, ...args )

wp_actionscheduler_logs
one row per attempt

ran cleanly?

status = complete
release claim

status = failed
retry, release claim

FIG 01 — How the four Action Scheduler tables work together

/ actions

What columns are in wp_actionscheduler_actions?

This is the core table — one row is one scheduled job. Every function in the Action Scheduler PHP API ultimately writes or reads a row here. The columns that matter most when you are debugging a stuck queue are status, scheduled_date_gmt, and claim_id.

ColumnTypeWhat it holds
action_idbigintPrimary key. The integer returned by the scheduling functions.
hookvarcharThe action hook fired via do_action() when the job runs.
statusvarcharpending, in-progress, complete, failed, or canceled.
scheduled_date_gmtdatetimeEarliest time the action may run. The runner claims rows whose value is in the past.
argsvarcharJSON-encoded arguments spread into the callback. A hash is also stored for dedup matching.
group_idbigintForeign key into wp_actionscheduler_groups.
claim_idbigint0 when unclaimed; a claim row id while a runner holds the action.
attemptsintHow many times the runner has tried to execute the action.
last_attempt_gmtdatetimeTimestamp of the most recent execution attempt.

The args column is indexed via a stored hash rather than the raw JSON, which is how the $unique flag can match "the same hook, args and group" quickly. Keep argument payloads small — large serialised arrays bloat both this column and every dedup lookup.

/ claims

What is the wp_actionscheduler_claims table for?

The claims table is how Action Scheduler stops two queue runners from executing the same job. It is deliberately tiny — essentially just an id and a timestamp. When a runner wants work, it inserts one claim row to mint a fresh claim_id, then runs an UPDATE that stamps that id onto a batch of due, unclaimed actions.

SQL — how a runner claims a batch (simplified)

-- 1. mint a claim id
INSERT INTO wp_actionscheduler_claims (date_created_gmt) VALUES (UTC_TIMESTAMP());
-- claim_id = LAST_INSERT_ID()

-- 2. lock a batch of due, unclaimed actions
UPDATE wp_actionscheduler_actions
   SET claim_id = :claim_id, last_attempt_gmt = UTC_TIMESTAMP()
 WHERE claim_id = 0
   AND status = 'pending'
   AND scheduled_date_gmt <= UTC_TIMESTAMP()
 ORDER BY scheduled_date_gmt
 LIMIT 25;

Because that UPDATE filters on claim_id = 0, only one runner can win each row. When the batch finishes, the claim row is deleted and the actions' claim_id resets. This is also the exact statement that can deadlock when several runners scan overlapping rows at once — the subject of Action Scheduler MySQL deadlocks under concurrent workers.

The claims table is a lock table. A non-zero claim_id on an action means "a runner owns this row" — that single column is what makes concurrent processing safe. — Action Scheduler data store

/ groups + logs

What do the groups and logs tables store?

wp_actionscheduler_groups is a simple lookup: an group_id and a slug. The free-text group you pass to a scheduling function (for example 'order-webhooks') is normalised into one row here, and actions reference it by group_id. That is what powers the group filter in the admin UI and bulk operations like as_unschedule_all_actions().

wp_actionscheduler_logs records the lifecycle. Each row ties an action_id to a message and a timestamp — "action created", "action started via WP Cron", "action complete", or the exception message when a run throws. This is the history that raw WP-Cron events can never give you, and it is what the admin screen renders when you expand an action's log.

TableKey columnsRole
wp_actionscheduler_groupsgroup_id, slugNormalises group labels; actions reference it by id.
wp_actionscheduler_logslog_id, action_id, message, log_date_gmtPer-event and per-attempt audit trail shown in wp-admin.

/ Inspecting

How do you inspect the tables safely with SQL?

Reading is safe and often the fastest way to diagnose a backlog. A count of actions grouped by status tells you at a glance whether the queue is draining or stuck. Use the $wpdb object rather than a hard-coded prefix so the query works on any install.

PHP — how healthy is the queue right now?

global $wpdb;
$rows = $wpdb->get_results(
    "SELECT status, COUNT(*) AS n
     FROM {$wpdb->prefix}actionscheduler_actions
     GROUP BY status"
);
// pending piling up while in-progress stays high = runner is stalled

Do not delete rows by hand as a cleanup strategy. Actions are linked to log rows, and an in-progress action still referenced by a live claim will confuse the runner. Cancel through as_unschedule_action() / as_unschedule_all_actions() or the admin screen, and let Action Scheduler's own retention task trim completed and failed rows.

/ Maintenance

Why do these tables grow, and how is that managed?

Completed and failed actions are retained so you can audit them — by default for 30 days — together with every log row. On a busy WooCommerce store that is easily millions of rows. Action Scheduler ships a built-in cleanup action that deletes old, finished actions past the retention window; the performance guide documents tuning batch sizes and the retention period for high-throughput sites. If your actions and logs tables are enormous, the fix is tuning retention and indexes — not truncating the tables.

For the functions that create and query all of these rows — scheduling, cancelling, and checking whether an action exists — see the Action Scheduler PHP API reference.

FAQ

Common questions always ask.

Don't see yours? Open an issue on GitHub or check the full reference in the API docs.

How many database tables does Action Scheduler create? +
Four, all prefixed wp_actionscheduler_: actions (one row per scheduled job), claims (one row per batch a runner locks), groups (a lookup of group slugs), and logs (one row per lifecycle event or attempt).
What is the wp_actionscheduler_claims table for? +
It hands out claim IDs. A queue runner inserts one row to get a unique claim_id, then stamps that id onto a batch of due pending actions so no other runner can process the same rows. When the batch finishes the claim row is deleted.
What does the claim_id column in wp_actionscheduler_actions mean? +
A non-zero claim_id means a runner has locked that action for execution. Zero means the action is unclaimed and available. The runner sets it in an UPDATE that also filters on claim_id = 0, which is where concurrent runners can deadlock under load.
Can I query or delete Action Scheduler rows directly in MySQL? +
You can read them freely for debugging. Deleting rows by hand is risky: prefer the as_unschedule_* functions or the admin Scheduled Actions screen, and only bulk-purge completed/failed rows once you understand the foreign relationships to the logs table.
Why do the Action Scheduler tables grow so large? +
Completed and failed actions are retained (default 30 days) along with their log rows for auditing. High-volume sites accumulate millions of rows; the built-in cleanup task trims old actions, and actionscheduler.org documents tuning the retention window.
Ready

Your next automation is
one sentence away.

$ wp plugin install flowsystems-webhook-actions --activate