Queues, Jobs, and Background Tasks
When to Use What
| Criteria |
Action Scheduler |
Laravel Queue + Job |
| Best for |
Simple recurring tasks, WP-native workflows |
Robust async, retry logic, heavy computation |
| Infrastructure |
None — runs on WP cron |
Needs queue driver (database or Redis) |
| Retry/backoff |
Manual |
Built-in ($tries, $backoff, exponential) |
| Monitoring |
WP Admin > Tools > Scheduled Actions |
lando acorn queue:failed, logs |
| Examples |
Daily cleanups, content sync, email digests |
Image processing, API syncs, bulk imports |
| Already available |
Bundled with WooCommerce; standalone via woocommerce/action-scheduler |
Requires Acorn queue config + worker |
Rule of thumb: Start with Action Scheduler for simple recurring WordPress tasks. Move to Laravel Queue + Job when you need retries, backoff, chaining, batching, or processing that could take more than a few seconds.
Quick Start — Laravel Queue
# 1. Create a job class
bash skills/acorn-queues/scripts/create-job.sh ProcessImage
# 2. Create queue tables (database driver)
lando acorn queue:table
lando acorn queue:failed-table
lando acorn migrate
# 3. Set driver in .env
# QUEUE_CONNECTION=database
# 4. Start a worker
bash skills/acorn-queues/scripts/run-worker.sh
bash skills/acorn-queues/scripts/run-worker.sh emails
Quick Start — Action Scheduler
# Install (if not using WooCommerce)
lando composer require woocommerce/action-scheduler
// Schedule once
as_schedule_single_action(strtotime('+10 minutes'), 'app/sync_content', ['post_id' => 42], 'content-sync');
// Schedule recurring (guard against duplicates)
if (! as_has_scheduled_action('app/cleanup_tokens', [], 'maintenance')) {
as_schedule_recurring_action(time(), HOUR_IN_SECONDS, 'app/cleanup_tokens', [], 'maintenance');
}
Register callbacks in ThemeServiceProvider::boot():
add_action('app/sync_content', function (int $postId): void {
$this->app->make(\App\Services\ContentSyncService::class)->syncPost($postId);
});
Job Class Anatomy
class ProcessImage implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 3;
public array $backoff = [10, 60, 300]; // exponential: 10s, 1min, 5min
public function __construct(protected readonly int $attachmentId) {}
public function handle(): void
{
// If job needs WP user context:
// wp_set_current_user($this->userId);
$file = get_attached_file($this->attachmentId);
if (! $file || ! file_exists($file)) {
return; // idempotent exit — attachment gone
}
// ... process
}
public function failed(?\Throwable $exception): void
{
Log::error('ProcessImage permanently failed', ['attachment_id' => $this->attachmentId]);
}
}
Dispatching Jobs
// Basic
ProcessImage::dispatch(attachmentId: $id);
// Delayed (5 minutes)
SyncContent::dispatch(sourceId: $id)->delay(now()->addMinutes(5));
// Specific queue
SendEmail::dispatch(campaignId: $id)->onQueue('emails');
// From a WordPress hook
add_action('add_attachment', function (int $id): void {
if (wp_attachment_is_image($id)) {
ProcessImage::dispatch(attachmentId: $id);
}
});
Scripts
# Create a job class (validates PascalCase, checks lando on PATH)
bash skills/acorn-queues/scripts/create-job.sh <JobName>
# Run a queue worker (default queue, 3 tries, 60s backoff)
bash skills/acorn-queues/scripts/run-worker.sh
bash skills/acorn-queues/scripts/run-worker.sh emails
Script source: scripts/create-job.sh · scripts/run-worker.sh
Assets
Boilerplate templates with {{CLASS_NAME}} placeholder. Copy and replace.
- job-simple.php.tpl — Simple job with
handle() + failed(), $tries, $backoff, wp_set_current_user stub.
- job-batched.php.tpl — Batched job with
use Batchable, $this->batch()->cancelled() check.
References
Deep content loaded on demand — zero tokens until needed.
- action-scheduler.md — Action Scheduler setup,
as_schedule_single_action, as_schedule_recurring_action, callbacks, duplicate-schedule prevention, cron-based queue trigger.
- laravel-queue.md — Full
config/queue.php, all drivers, database setup, dispatching patterns, job chaining, batching, testing with Bus::fake().
- redis-driver.md — Redis-backed queue, Lando Redis service config (
REDIS_HOST=cache), queue isolation, failover to database driver, Supervisor config.
- job-patterns.md — Idempotency patterns, chunking large datasets, retry/backoff config,
ShouldBeUnique + uniqueId(), wp_set_current_user in jobs, pass IDs not objects.
- troubleshooting.md — Failed jobs table,
queue:work debug flags, common errors (jobs stuck, Redis refused, class not found, memory exhaustion), escalation paths.
Verification
- Dispatch a test job and confirm it appears in the queue: check the
jobs table (database driver) or lando redis-cli -h cache LLEN queues:default (Redis driver).
- Run
lando acorn queue:work --once and confirm the job processes successfully with expected log output.
- After processing, verify the job is removed from the queue and does not appear in
lando acorn queue:failed.
Failure modes
Problem: Jobs dispatched but never processed
- Cause: No worker is running, or
QUEUE_CONNECTION in .env does not match a connection in config/queue.php.
- Fix: Start a worker (
bash skills/acorn-queues/scripts/run-worker.sh). Verify QUEUE_CONNECTION matches a valid connection key. For the database driver, run lando acorn queue:table && lando acorn migrate.
Problem: Jobs fail and the failed_jobs table does not exist
- Cause: The failed jobs migration was never run.
- Fix:
lando acorn queue:failed-table && lando acorn migrate. Then retry with lando acorn queue:retry all.
For all other failure modes see references/troubleshooting.md.
Critical Rules
- Action Scheduler vs Laravel Queue: Use Action Scheduler for simple recurring WP-native tasks. Use Laravel Queue + Job when you need retries, backoff, chaining, batching, or heavy computation.
wp_set_current_user in jobs: If a job calls current_user_can() or any WP function that relies on the current user, call wp_set_current_user($userId) at the top of handle(). Jobs run outside the HTTP lifecycle — WP does not set a current user automatically.
- Idempotency: Every job must be safe to run more than once. Use guard clauses or upsert patterns. See
references/job-patterns.md.
- Pass IDs, not objects: Pass
$postId instead of $post. Re-fetch in handle() to avoid serialization issues and stale data on retry.
- Set
$tries and $backoff: Never let jobs retry infinitely. Use exponential backoff arrays for external API jobs.
- Failed jobs table must exist: Run
lando acorn queue:failed-table && lando acorn migrate before running workers in production.
sync driver is for development only. It blocks the HTTP request and runs jobs inline. Set QUEUE_CONNECTION=database or redis in production.
- Redis host inside Lando is the service name (
cache), not 127.0.0.1. See references/redis-driver.md.
1---2name: superpowers-sage-acorn-queues3description: Background job processing in WordPress via Acorn: Laravel queue, dispatch, queue:work, ShouldQueue, failed jobs, Action Scheduler, as_schedule_single_action, as_schedule_recurring_action, job retries, backoff, job chaining, job batching, ShouldBeUnique, Acorn queue, Redis queue driver, database queue driver, queue worker, background tasks — using Acorn's Laravel queue stack in Sage/WordPress4---56# Queues, Jobs, and Background Tasks78## When to Use What910| Criteria | Action Scheduler | Laravel Queue + Job |11|---|---|---|12| **Best for** | Simple recurring tasks, WP-native workflows | Robust async, retry logic, heavy computation |13| **Infrastructure** | None — runs on WP cron | Needs queue driver (database or Redis) |14| **Retry/backoff** | Manual | Built-in (`$tries`, `$backoff`, exponential) |15| **Monitoring** | WP Admin > Tools > Scheduled Actions | `lando acorn queue:failed`, logs |16| **Examples** | Daily cleanups, content sync, email digests | Image processing, API syncs, bulk imports |17| **Already available** | Bundled with WooCommerce; standalone via `woocommerce/action-scheduler` | Requires Acorn queue config + worker |1819**Rule of thumb:** Start with Action Scheduler for simple recurring WordPress tasks. Move to Laravel Queue + Job when you need retries, backoff, chaining, batching, or processing that could take more than a few seconds.2021## Quick Start — Laravel Queue2223```bash24# 1. Create a job class25bash skills/acorn-queues/scripts/create-job.sh ProcessImage2627# 2. Create queue tables (database driver)28lando acorn queue:table29lando acorn queue:failed-table30lando acorn migrate3132# 3. Set driver in .env33# QUEUE_CONNECTION=database3435# 4. Start a worker36bash skills/acorn-queues/scripts/run-worker.sh37bash skills/acorn-queues/scripts/run-worker.sh emails38```3940## Quick Start — Action Scheduler4142```bash43# Install (if not using WooCommerce)44lando composer require woocommerce/action-scheduler45```4647```php48// Schedule once49as_schedule_single_action(strtotime('+10 minutes'), 'app/sync_content', ['post_id' => 42], 'content-sync');5051// Schedule recurring (guard against duplicates)52if (! as_has_scheduled_action('app/cleanup_tokens', [], 'maintenance')) {53 as_schedule_recurring_action(time(), HOUR_IN_SECONDS, 'app/cleanup_tokens', [], 'maintenance');54}55```5657Register callbacks in `ThemeServiceProvider::boot()`:5859```php60add_action('app/sync_content', function (int $postId): void {61 $this->app->make(\App\Services\ContentSyncService::class)->syncPost($postId);62});63```6465## Job Class Anatomy6667```php68class ProcessImage implements ShouldQueue69{70 use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;7172 public int $tries = 3;73 public array $backoff = [10, 60, 300]; // exponential: 10s, 1min, 5min7475 public function __construct(protected readonly int $attachmentId) {}7677 public function handle(): void78 {79 // If job needs WP user context:80 // wp_set_current_user($this->userId);8182 $file = get_attached_file($this->attachmentId);83 if (! $file || ! file_exists($file)) {84 return; // idempotent exit — attachment gone85 }86 // ... process87 }8889 public function failed(?\Throwable $exception): void90 {91 Log::error('ProcessImage permanently failed', ['attachment_id' => $this->attachmentId]);92 }93}94```9596## Dispatching Jobs9798```php99// Basic100ProcessImage::dispatch(attachmentId: $id);101102// Delayed (5 minutes)103SyncContent::dispatch(sourceId: $id)->delay(now()->addMinutes(5));104105// Specific queue106SendEmail::dispatch(campaignId: $id)->onQueue('emails');107108// From a WordPress hook109add_action('add_attachment', function (int $id): void {110 if (wp_attachment_is_image($id)) {111 ProcessImage::dispatch(attachmentId: $id);112 }113});114```115116## Scripts117118```bash119# Create a job class (validates PascalCase, checks lando on PATH)120bash skills/acorn-queues/scripts/create-job.sh <JobName>121122# Run a queue worker (default queue, 3 tries, 60s backoff)123bash skills/acorn-queues/scripts/run-worker.sh124bash skills/acorn-queues/scripts/run-worker.sh emails125```126127Script source: [`scripts/create-job.sh`](scripts/create-job.sh) · [`scripts/run-worker.sh`](scripts/run-worker.sh)128129## Assets130131Boilerplate templates with `{{CLASS_NAME}}` placeholder. Copy and replace.132133- **[job-simple.php.tpl](assets/job-simple.php.tpl)** — Simple job with `handle()` + `failed()`, `$tries`, `$backoff`, `wp_set_current_user` stub.134- **[job-batched.php.tpl](assets/job-batched.php.tpl)** — Batched job with `use Batchable`, `$this->batch()->cancelled()` check.135136## References137138Deep content loaded on demand — zero tokens until needed.139140- **[action-scheduler.md](references/action-scheduler.md)** — Action Scheduler setup, `as_schedule_single_action`, `as_schedule_recurring_action`, callbacks, duplicate-schedule prevention, cron-based queue trigger.141- **[laravel-queue.md](references/laravel-queue.md)** — Full `config/queue.php`, all drivers, database setup, dispatching patterns, job chaining, batching, testing with `Bus::fake()`.142- **[redis-driver.md](references/redis-driver.md)** — Redis-backed queue, Lando Redis service config (`REDIS_HOST=cache`), queue isolation, failover to database driver, Supervisor config.143- **[job-patterns.md](references/job-patterns.md)** — Idempotency patterns, chunking large datasets, retry/backoff config, `ShouldBeUnique` + `uniqueId()`, `wp_set_current_user` in jobs, pass IDs not objects.144- **[troubleshooting.md](references/troubleshooting.md)** — Failed jobs table, `queue:work` debug flags, common errors (jobs stuck, Redis refused, class not found, memory exhaustion), escalation paths.145146## Verification147148- Dispatch a test job and confirm it appears in the queue: check the `jobs` table (database driver) or `lando redis-cli -h cache LLEN queues:default` (Redis driver).149- Run `lando acorn queue:work --once` and confirm the job processes successfully with expected log output.150- After processing, verify the job is removed from the queue and does not appear in `lando acorn queue:failed`.151152## Failure modes153154### Problem: Jobs dispatched but never processed155156- **Cause:** No worker is running, or `QUEUE_CONNECTION` in `.env` does not match a connection in `config/queue.php`.157- **Fix:** Start a worker (`bash skills/acorn-queues/scripts/run-worker.sh`). Verify `QUEUE_CONNECTION` matches a valid connection key. For the database driver, run `lando acorn queue:table && lando acorn migrate`.158159### Problem: Jobs fail and the `failed_jobs` table does not exist160161- **Cause:** The failed jobs migration was never run.162- **Fix:** `lando acorn queue:failed-table && lando acorn migrate`. Then retry with `lando acorn queue:retry all`.163164For all other failure modes see [`references/troubleshooting.md`](references/troubleshooting.md).165166## Critical Rules1671681. **Action Scheduler vs Laravel Queue:** Use Action Scheduler for simple recurring WP-native tasks. Use Laravel Queue + Job when you need retries, backoff, chaining, batching, or heavy computation.1692. **`wp_set_current_user` in jobs:** If a job calls `current_user_can()` or any WP function that relies on the current user, call `wp_set_current_user($userId)` at the top of `handle()`. Jobs run outside the HTTP lifecycle — WP does not set a current user automatically.1703. **Idempotency:** Every job must be safe to run more than once. Use guard clauses or upsert patterns. See `references/job-patterns.md`.1714. **Pass IDs, not objects:** Pass `$postId` instead of `$post`. Re-fetch in `handle()` to avoid serialization issues and stale data on retry.1725. **Set `$tries` and `$backoff`:** Never let jobs retry infinitely. Use exponential backoff arrays for external API jobs.1736. **Failed jobs table must exist:** Run `lando acorn queue:failed-table && lando acorn migrate` before running workers in production.1747. **`sync` driver is for development only.** It blocks the HTTP request and runs jobs inline. Set `QUEUE_CONNECTION=database` or `redis` in production.1758. **Redis host inside Lando is the service name** (`cache`), not `127.0.0.1`. See `references/redis-driver.md`.