Hand-drawn diagram: The async, human-gated content pipeline. Left to right: Draft (REST route enqueues, 202) to WP-Cron worker (claims the row atomically) to Model call (off-thread, 60-90 s) to ai_review (wp_kses sanitized) to human_review (a person owns publish) to Publish (approved, audited); async: survives the ~30 s request wall.

How to Build an AI Content Workflow in WordPress Without Extra Plugins

One model call outlives a PHP request, and raw output cannot go straight to publish. Here is the asynchronous, human-gated editorial state machine that solves both, built only from WordPress core: a REST route, WP-Cron, custom post statuses, a queue table, and transients.

Why does "click generate, paste text" break in production?#

Most tutorials wire an AI content workflow in WordPress as one synchronous step. Click a button, wait, get text. However, that flow ignores the wall every real deployment hits. A page save and a REST request both run under a hard time budget. Meanwhile one model completion routinely runs past it. So the request dies before the text arrives, or it blocks a worker process the whole time.

Because this is the failure every competing guide skips, it is worth feeling the numbers before we design around them. The gap is not small. In practice it is the difference between a request that answers and one that times out.

The request-lifetime wall: PHP execution ceiling vs real model latency#

PHP caps how long a request may run. Typical hosts set max_execution_time near 30 seconds, and the REST handler answers inside that same budget. Meanwhile a real completion for a full article often takes 60 to 90 seconds or more. Therefore a synchronous call cannot physically finish in the request that started it.

Request budget vs real model latencyoften 2x the budget or more

~30 s

PHP execution and REST request budget

Over the wall

60-90 s+

One real model completion

These are illustrative, order-of-magnitude typical values, not a published benchmark and not Atyantik client data. The point is the ratio: a model call for a full draft routinely outlives the request budget, so it cannot run in the request that triggered it. The work has to move off-thread.

Request budget vs real model latency (wall-clock seconds (typical, illustrative))
Optionwall-clock seconds (typical, illustrative)
PHP execution and REST request budget~30 s
One real model completion60-90 s+

Source: WordPress REST API Handbook (request model; latency figures illustrative)

The two blockers nobody names: timeout AND governance#

The timeout wall is only the first blocker. Because a queue solves latency but not trust, the second blocker sits one layer up. Raw model output is unreviewed text. Therefore shipping it straight to publish is a governance failure, not a feature.

The AI content workflow in WordPress: an asynchronous, human-gated state machine#

Here is the load-bearing idea. An AI content workflow in WordPress is a state machine, and every state is a value WordPress core already stores. Because the post status is the state, and a queue row carries the retry bookkeeping, no plugin is needed to hold the pipeline together. The diagram below traces the whole lifecycle, including the retry loop and the human gate.

The asynchronous, human-gated editorial state machine for an AI content workflow in WordPressA post starts as a draft, gets enqueued by the REST route, and is claimed by a WP-Cron worker. On success the model output is sanitized and moved to ai_review, then a human opens it for human_review and either rejects it back to the queue or approves publish. On failure the job waits a geometric backoff and retries until attempts are exhausted.

Custom post statuses as the machine's state#

The state store is already in core. Because register_post_status lets you add your own statuses, ai_review and human_review become real, queryable states a post can occupy. Therefore the pipeline never needs a separate status column or a workflow plugin. Register the two statuses once, and the post itself records where it sits in the flow.

inc/ai-statuses.php · php
// functions.php, or a small must-use plugin. Two custom statuses model the two
// gates the pipeline walks a post through. No plugin, just core.
add_action('init', function () {
    register_post_status('ai_review', [
        'label'                     => 'AI review',
        'public'                    => false,
        'internal'                  => true,
        'show_in_admin_status_list' => true,
    ]);

    register_post_status('human_review', [
        'label'                     => 'Human review',
        'public'                    => false,
        'internal'                  => true,
        'show_in_admin_status_list' => true,
    ]);
});

Now the admin post list shows each draft's stage, and a status query returns everything waiting on a human. See the register_post_status reference for the full options list.

Why post meta alone isn't enough: the queue table#

Post status answers where a draft sits. However, it cannot safely answer how many times a job has run, or whether two cron ticks grabbed the same work. Because retries and idempotency need atomic bookkeeping, a small queue table earns its place. It carries attempts, a locked_until claim window, and a unique idempotency_key.

inc/ai-queue-table.php · php
// Run once on activation. dbDelta creates the queue table that makes retries and
// idempotency safe, which post meta alone cannot.
function acme_ai_create_queue_table(): void {
    global $wpdb;
    $table   = $wpdb->prefix . 'ai_jobs';
    $charset = $wpdb->get_charset_collate();

    $sql = "CREATE TABLE {$table} (
        id              BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
        post_id         BIGINT UNSIGNED NOT NULL,
        status          VARCHAR(20) NOT NULL DEFAULT 'queued',
        attempts        SMALLINT UNSIGNED NOT NULL DEFAULT 0,
        locked_until    DATETIME NULL,
        idempotency_key CHAR(36) NOT NULL,
        created_at      DATETIME NOT NULL,
        PRIMARY KEY (id),
        UNIQUE KEY idempotency_key (idempotency_key),
        KEY status_locked (status, locked_until)
    ) {$charset};";

    require_once ABSPATH . 'wp-admin/includes/upgrade.php';
    dbDelta($sql);
}

The unique idempotency_key is the safety rail. Since a re-fired cron event carries the same key, the worker recognises a duplicate and refuses to generate twice. Build the table with dbDelta, which is the core helper for schema changes.

Step 1: enqueue the job from a REST route (capability and nonce)#

The first build step is the front door. Because the REST route must answer fast, it does exactly two things and returns. First, it inserts a queue row and flips the post to ai_queued. Second, it schedules a single cron event. It never calls the model, so it always responds inside the REST budget with a 202 Accepted.

inc/ai-rest-route.php · php
// The route ENQUEUES and returns. It never calls the model, so it always answers
// well inside the REST timeout.
add_action('rest_api_init', function () {
    register_rest_route('acme-ai/v1', '/generate', [
        'methods'             => 'POST',
        'permission_callback' => function (WP_REST_Request $req) {
            // Capability AND nonce. Skip either and any logged-in visitor can
            // spend your whole token budget.
            return current_user_can('edit_posts')
                && wp_verify_nonce($req->get_header('X-WP-Nonce'), 'wp_rest');
        },
        'callback' => function (WP_REST_Request $req) {
            $post_id = (int) $req->get_param('post_id');
            $key     = wp_generate_uuid4();

            global $wpdb;
            $wpdb->insert($wpdb->prefix . 'ai_jobs', [
                'post_id'         => $post_id,
                'status'          => 'queued',
                'idempotency_key' => $key,
                'created_at'      => current_time('mysql'),
            ]);

            wp_update_post(['ID' => $post_id, 'post_status' => 'ai_queued']);
            wp_schedule_single_event(time(), 'acme_ai_run_job', [$key]);

            return new WP_REST_Response(['queued' => true, 'job' => $key], 202);
        },
    ]);
});

Step 2: run the model call off-thread in a WP-Cron worker#

This is the heart of the async design, and the reason an AI content workflow in WordPress survives a slow model. Because the REST route only enqueued, the actual generation happens later, on a cron tick, in a separate request. The three snippets below are one pipeline. First the worker hook claims the row. Then the model call runs with a bounded timeout. Finally a wrapper handles retries with exponential backoff.

php
// The scheduled worker runs OUTSIDE the request that enqueued it. This hook fires
// on a WP-Cron tick, claims the row, and does the slow work off-thread.
add_action('acme_ai_run_job', function (string $key): void {
    global $wpdb;
    $table = $wpdb->prefix . 'ai_jobs';

    // Claim the row so a second cron tick cannot pick up the same job.
    $claimed = $wpdb->query($wpdb->prepare(
        "UPDATE {$table}
            SET status = 'running', locked_until = %s, attempts = attempts + 1
          WHERE idempotency_key = %s AND status IN ('queued', 'failed')",
        gmdate('Y-m-d H:i:s', time() + 300),
        $key
    ));

    if (! $claimed) {
        return; // another worker already owns this job
    }

    acme_ai_generate($key);
});

Notice where each concern lives. The claim step stops two workers touching one job. Meanwhile the model call is the only slow part, and it sits safely in cron. See wp_schedule_single_event and wp_remote_post for the two core calls doing the heavy lifting.

Timeout, retry, and exponential backoff#

Retries are where a naive worker doubles your bill. Because a failed attempt reschedules the same job, the delay between attempts must grow, or a flapping API gets hammered. Therefore the backoff is geometric: 2 seconds, then 4, then 8, then 16. In short, each retry waits base times two to the attempt number.

Step 3: sanitize, then gate from ai_review to human_review to publish#

The pipeline closes with the governance the differentiation promises. First, wp_kses_post sanitizes the raw model output before it is written as content, stripping scripts and unsafe markup to the same allowlist the block editor trusts. Then the post advances to human_review, where an editor owns the publish decision. Because only a person can move a post from human_review to publish, unreviewed text never ships on its own.

inc/ai-review-gate.php · php
// Sanitize BEFORE model output ever becomes reviewable content, then advance the
// status so a human owns the publish decision.
function acme_ai_stage_for_review(int $post_id, string $raw): void {
    $clean = wp_kses_post($raw); // strips scripts to the block editor allowlist

    wp_update_post([
        'ID'           => $post_id,
        'post_content' => $clean,
        'post_status'  => 'human_review',
    ]);
}

// Only a person moving the post to 'publish' from 'human_review' ships it. Audit
// who approved, and when.
add_action('transition_post_status', function (string $new, string $old, WP_Post $post): void {
    if ($new === 'publish' && $old === 'human_review') {
        add_post_meta($post->ID, '_ai_approved_by', get_current_user_id());
        add_post_meta($post->ID, '_ai_approved_at', current_time('mysql'));
    }
}, 10, 3);

Governing cost and rate limits in transients#

Two blockers are handled. Now comes the runaway risk the async design creates: a fast worker can generate around the clock. Because cost and request rate must stay bounded, the Transients API is the plugin-free governor. A per-hour counter in a transient caps throughput, and simple arithmetic on your own numbers bounds the spend. Drag the inputs below and watch the monthly bill, the worst-case backoff schedule, and the throttle verdict recompute.

Pipeline cost and rate governor

Set the pipeline governors

Illustrative teaching values, not a vendor quote.

backoff x
Monthly spend$0.58$0.00 per post x 40 posts/day x 30 days. Output tokens only.
Worst-case backoff schedule
  1. #1: 2s
  2. #2: 4s
  3. #3: 8s
  4. #4: 16s
Cumulative worst-case wait: 30s
OKWorst-case demand is 8.3 requests/hour, within the 20/hour transient limit. The pipeline keeps up without the rate gate deferring work.
Worked example (illustrative), the same math the sliders run
tokens_per_post800 output tokens
price_per_1M$0.60 per 1M
posts_per_day40
monthly_spend800 x 0.60 / 1M x 40 x 30 = $0.58/mo
backoff (base 2s, x2, 4 retries)2s, 4s, 8s, 16s = 30s worst case
throttle check40 x 5 / 24 = 8.3 req/hr vs 20 limit = OK

The governance rule: cost, retry backoff, and request rate are all bounded in core primitives. Token spend is arithmetic on your own numbers. Backoff grows geometrically so a failing model does not hammer the API. A per-hour transient counter caps throughput, so a burst of drafts never runs the bill away.

Set your token count, price, volume, retry policy, and per-hour transient limit. The panel derives monthly spend, the worst-case geometric backoff schedule, and a throttle verdict that flips when worst-case demand crosses the rate cap. All numbers are illustrative teaching values. With JavaScript off, the worked-example table and the governance rule still read below.

Notice how the throttle verdict flips. Because worst-case demand is posts per day times one plus the retry count, spread over the day, a modest transient limit absorbs a normal load and defers a burst. Consequently the transient counter, not a metering plugin, is what keeps a runaway loop from spending the budget in an afternoon.

The proof: which core primitive replaces which plugin#

Here is the argument in one table. Every job a tutorial hands to a plugin maps to a core primitive you already have. Because the primitives compose, an AI content workflow in WordPress needs none of the third-party dependencies the other guides list. Read the matrix as the plugin-free bill of materials.

The plugin each guide told you to install, and the WordPress core primitive that replaces it
Job in the pipelinePlugin you were told to installCore primitive that replaces it
Trigger generationAn AI writer pluginregister_rest_route that only enqueues, plus a WP-Cron worker
Hold pipeline stateA workflow or status pluginregister_post_status custom statuses (ai_review, human_review)
Queue and retry jobsA background-jobs pluginA queue table plus wp_schedule_single_event
Call the model APIAn API-connector pluginwp_remote_post with an explicit timeout
Sanitize outputAn HTML-cleanup pluginwp_kses_post to the block-editor allowlist
Cap cost and rateA usage-metering pluginTransient counters (get_transient and set_transient)
Gate publishAn editorial-workflow plugintransition_post_status plus a human_review status

Each row is one dependency you do not add, one update you do not chase, and one attack surface you do not own. In short, the plugin-free build is not a purity exercise. It is less code to maintain and a pipeline you fully control.

When not to build this yourself#

This architecture is not always the right call. Because it is real software engineering, it earns its keep only past a certain scale and control need. Understanding that line is part of building the thing honestly.

Where to go next#

The disciplines here are not unique to AI. If you run a large WordPress codebase, our guide to structuring WordPress at scale with Composer and PSR-4 applies the same governance thinking to dependencies and autoloading. Meanwhile the queue, retry, and idempotency patterns behind Step 2 come straight from background-job systems, and our deep dive on running Laravel queues at scale covers the at-least-once reliability model in full.

Because a content pipeline is one piece of a larger platform, the wider practice of shipping systems that hold up sits inside enterprise software development. If you want a second read on a WordPress build, our CMS platform engineering work and AI integration work cover it, and you can hire WordPress developers from our team. No pressure and no lock-in: everything above is standard, documented WordPress you own outright.

Building an AI content workflow in WordPress and want a second read on the async design, the human gate, or the cost governor? No pressure and no lock-in.

Talk through your WordPress AI pipeline

Keep reading