Skip to main content
Pylon runs background work with the same functions you already write — no separate worker process, queue service, or cron daemon. There are two shapes: recurring (cron) and one-shot (deferred).

Recurring: cron(...)

Declare a cron in your manifest. It fires a function every time the schedule matches (the scheduler checks once a minute).
app.ts
The second argument is the name of a function in functions/ — a normal query / mutation / action:
functions/hourlyRollup.ts
Make cron functions internal: true. A cron function is also a regular function, so without internal: true it’s reachable at /api/fn/<name>. Marking it internal means only the scheduler (and ctx.runMutation) can invoke it.
Auth. A cron has no caller, so it runs with anonymous auth — the same as Pylon’s own built-in maintenance jobs. You usually don’t need to do anything about that: a function’s own ctx.db.* calls run server-side and aren’t subject to entity policies (those gate client sync, not trusted server code), so a maintenance cron reads and writes its entities directly. Elevate only in two cases, and always with an audit reason:
reason is mandatory — every elevation is logged so an operator can audit who elevated and why. Pylon never grants admin to a cron implicitly. Schedule format. Standard 5-field cron: minute hour day-of-month month day-of-week. A schedule that doesn’t parse, or a function name that isn’t registered, is logged loudly at boot and skipped — so a typo fails visibly in pylon dev instead of silently never running. Durability. Cron jobs are backed by a persistent job store, so a fire that’s in flight survives a restart, and the schedule resumes after a redeploy.
Multiple replicas. Each running Pylon process schedules crons independently. On a shared datastore (Postgres — the Pylon Cloud default), the runtime takes a per-minute lease so each cron fires exactly once per tick across all replicas — no extra work on your part. On per-replica SQLite there’s no shared lease, so each replica fires every tick; keep those handlers idempotent (guard on a row you write — most maintenance is naturally idempotent) or run the scheduled work on a single machine. A single-machine app always fires exactly once.

One-shot: ctx.scheduler

To run something later (not on a repeating schedule), schedule it from inside a mutation or action:
The scheduled function runs with the auth identity of the caller that scheduled it (the chain-of-custody is enforced at schedule time), and the args you pass are delivered verbatim. Like crons, these are durable across restarts.
Before cron(...) existed, the idiom for recurring work was a self-perpetuating function — one that re-armed itself with ctx.scheduler.runAfter(...) at the end. That still works, but cron(...) is clearer and can’t silently stop if a re-arm is missed.