Every lesson so far in this course has assumed an AI call happens somewhere inside a
request, an ability’s execute_callback, a save_post handler, a page render, and
the user waits for it. That’s fine for a call that returns in under a second. It’s a
real problem for anything slower: generating a long-form draft, processing a batch of
products, running a multi-step analysis. Keeping a request open while a model provider
takes ten or twenty seconds to respond ties up a PHP worker, risks a web server timeout,
and gives the visitor nothing to look at but a spinner. Action Scheduler, the real,
independently maintained library behind WooCommerce’s background processing, moves
that work out of the request entirely: you enqueue a job, the request returns
immediately, and the AI call runs afterward, on its own schedule, with retry and
concurrency handling you didn’t have to write.
General familiarity with WordPress hooks (add_action()), and the caching and
invalidation patterns from Lessons 1 and 2, since the worked example in this lesson
writes its result into the same cache this course already built.
Step 1: Action Scheduler is not just WP-Cron with a nicer name
Raw WP-Cron schedules a callback to run on a future page load, with no persistence, no retry, and no record of whether it actually ran or failed. Action Scheduler, the library WooCommerce and many other plugins rely on for background processing, solves that by storing every scheduled job in the database as a real, queryable record: what it is, when it’s due, whether it ran, and whether it succeeded. That persistence is exactly what a slow AI call needs, if a job fails partway through, you have a durable record of it to inspect and act on, rather than a callback that silently never ran.
Step 2: Two real entry points, run-now versus run-later
Both calls schedule a WordPress action hook, exactly like do_action() would, just
deferred. You still need your own add_action() for that hook, Action Scheduler
handles the timing and persistence, not the logic:
// File: my-plugin/class-async-summarizer.php
add_action( 'my_plugin_generate_post_summary_async', 'my_plugin_run_post_summary_job', 10, 1 );
function my_plugin_run_post_summary_job( int $post_id ): void {
$post = get_post( $post_id );
if ( ! $post ) {
return;
}
$prompt = 'Summarize in two sentences: ' . $post->post_title . "\n\n" . $post->post_content;
$system = 'You are a concise editorial summarizer.';
$result = wp_ai_client_prompt( $prompt )
->using_system_instruction( $system )
->generate_text();
if ( is_wp_error( $result ) ) {
error_log( 'AI summary job failed for post ' . $post_id . ': ' . $result->get_error_message() );
return;
}
update_post_meta( $post_id, '_ai_summary', $result );
}
Step 3: Queue the job instead of generating inline on save_post
This is the actual production shift: instead of calling generate_text() directly
inside a save_post handler and making the editor wait on the model provider before
their save even finishes, queue the job and let the request return immediately:
// File: my-plugin.php
add_action( 'save_post', function ( int $post_id, WP_Post $post ) {
if ( wp_is_post_autosave( $post_id ) || wp_is_post_revision( $post_id ) ) {
return;
}
if ( 'publish' !== $post->post_status ) {
return;
}
as_enqueue_async_action(
'my_plugin_generate_post_summary_async',
array( $post_id ),
'my-plugin-ai'
);
}, 10, 2 );
The editor’s save completes the moment save_post finishes, with no wait on the model
provider at all. The summary appears in post meta shortly after, generated by the
queued job, not the request that triggered it.
If the job should run at a specific later time rather than as soon as possible,
digesting a day’s comments once at midnight, for instance, use
as_schedule_single_action( strtotime( 'tomorrow midnight' ), 'my_plugin_daily_digest', array(), 'my-plugin-ai' )
instead. The $group argument ('my-plugin-ai' in both examples) keeps your plugin’s
jobs identifiable and separable from every other plugin’s scheduled actions on a busy
site.
Step 4: Where to actually see this running
Action Scheduler ships with its own admin list of scheduled actions, showing pending, in-progress, complete, and failed jobs, reachable under WooCommerce’s Status screen on sites running WooCommerce, or via Action Scheduler’s own admin screen when it’s loaded standalone by another plugin. That list is the real place to confirm a queued AI job actually ran, rather than adding your own separate tracking UI just to answer that question.
Action Scheduler processes its queue on WordPress’s normal request lifecycle (similar to how WP-Cron itself depends on site traffic to fire, unless you’ve configured a real system cron trigger). On a very low-traffic site, queued actions can sit longer than expected before they’re picked up. For anything time-sensitive, make sure the site has a reliable way for scheduled hooks to actually fire, the same operational concern that applies to WP-Cron generally.
Test it
Save a published post and confirm the summary appears in post meta shortly after, without the save request itself waiting on it:
wp-env run cli wp eval '
$post_id = wp_insert_post( array(
"post_title" => "Async Summary Test",
"post_content" => "This is a test post used to confirm background AI summarization.",
"post_status" => "publish",
) );
echo "Post saved instantly, ID: " . $post_id . "\n";
'
Then, after giving the queue a moment to process, check that same post ID (substitute the ID printed above):
wp-env run cli wp eval 'echo get_post_meta( 123, "_ai_summary", true );'
You should see the generated summary, produced by the queued job rather than the
wp_insert_post() call itself.
Recap
Action Scheduler turns “schedule this for later” into a persisted, retryable,
queryable job rather than a hopeful WP-Cron callback. as_enqueue_async_action() moves
a slow AI call out of the request that triggered it, running it as soon as the queue
picks it up; as_schedule_single_action() handles work that genuinely needs to wait
until a specific future time. Either way, you still register a normal add_action()
for the hook Action Scheduler fires, the library handles timing and persistence, your
code handles the actual AI call.
Resources & further reading
- Action Scheduler API documentation, GitHub
- Action Scheduler repository, GitHub
- PHP AI Client SDK repository, GitHub