Runners
What executes the actions, and what the maintenance tick does.
The plugin runs nothing by itself. Each due action is a Payload job (scheduler:run, input { actionId }) on the action's queue, so whatever runs Payload jobs runs actions:
| Setup | How |
|---|---|
| Payload Clock — recommended | A free hosted clock calling GET /api/payload-jobs/run?allQueues=true with your cron secret, with retries, run history and alerts when your queue stops answering. Nothing to deploy, and the same on every host. |
| Long-lived server | jobs.autoRun: [{ cron: '* * * * *', queue: 'default' }] |
| Vercel / serverless | A cron calling the same endpoint with your cron secret. Note that Vercel Hobby runs a cron once a day. |
| Separate worker | payload jobs:run --all-queues --handle-schedules on a schedule |
| Admin | Run queue → POST /api/scheduled-actions/scheduler/run-queue |
One call does both jobs: it queues anything whose schedule has come round (the maintenance tick) and then runs the due jobs. Once a minute is the cadence the plugin is designed around — at one call a day, an action scheduled for 09:00 runs at the next call instead.
The tick
scheduler:tick is a task with Payload's own schedule (every minute by default). Each tick records a heartbeat (runner health in the admin), promotes actions that came within the dispatch horizon (15 minutes), recovers attempts whose runner died (lost, then retried), re-dispatches orphans, deletes transport jobs stuck in processing on Payload 3.x, and purges finished rows past retention once an hour.
Because the tick uses schedule, Payload turns on job stats, which adds one small global and a meta column on payload-jobs. Set tick: false to avoid that: every action is then dispatched at schedule time, health is unknown, and sweeping runs inside runQueue().
Exactly once
Claims and outcomes are compare-and-set statements: raw conditional UPDATE … RETURNING on Postgres and SQLite, findOneAndUpdate on MongoDB. Two runners picking up the same job invoke the handler once. Another adapter can provide payload.db.casUpdate(args); without it the plugin falls back to read-then-write and warns at start-up to run a single runner.