One cron file.
One box or a fleet.

A scheduled job usually breaks the moment you run a second replica: either every instance fires it, or you bolt on a lock and hope. Here coordination is part of the primitive — the same file runs once per tick whether you deploy one container or twenty, and nothing in it changes when you scale.

nightly.cron.tsx
TypeScript
// billing/nightly.cron.tsx — one file, discovered automatically
export default defineSchedule({
  name: 'invoices.nightly',
  cron: '0 3 * * *',
  handler: (ctx) => ctx.store.query(...),
})
// Twenty replicas fire this ONCE. No lock, no designated cron box.
// $ voltro schedule run invoices.nightly     # fire it on purpose

What the primitive handles.

A file is a schedule

A cron definition declares its expression and its handler, and the framework discovers it. No registry to append to, nothing to remember at boot, and no separate worker entrypoint to keep in step with the app it belongs to.

N replicas, one firing

Coordination is built in: the fleet agrees on who runs a given occurrence, so scaling out does not multiply your emails, invoices or exports. You do not write a lock, and you do not run a designated "cron box" that becomes a single point of failure.

Missed ticks are caught up

A deploy, a crash or a scale-to-zero window means occurrences pass while nothing is running. On boot the scheduler walks forward from the last recorded run rather than pretending the gap did not happen — bounded, so a long outage cannot stampede.

Fire one on purpose

`voltro schedule run NAME` fires a registered schedule on demand — against a running process or standalone, marked as a manual trigger so the record says how it happened. Testing a job stops meaning "wait for the hour" or "temporarily change the expression".

Backfill an explicit range

Fill a window that boot catch-up cannot reach: every occurrence between two timestamps, run sequentially, each recorded against its own cron-derived time. It refuses above a threshold without confirmation and refuses outright above a hard cap — never a silent prefix of what you asked for.

Or hand the trigger to something else

Prefer Kubernetes CronJobs, a cloud scheduler or an external orchestrator? The same definition can be triggered externally instead of ticking internally, so the choice of scheduler is a deployment decision rather than a rewrite.

The bug this design removes.

The subject a job runs as is not a request's subject

A scheduled job has no caller, so whatever identity it runs as is invented by the code that starts it — and when two boot paths each invent one, they can disagree while both look correct. That happened here: development supplied a tenant-flavoured fallback and production supplied a tenant-less one, so the same cron read a single tenant in dev and every tenant in production, silently. The engine hands the subject to the job now instead of each path constructing one. A callback that RECEIVES a value cannot drift; one that invents it can, and did.

A nightly job, on more than one replica.

A cron container, hoping it is the only one
Assembled
# A cron container beside the app.
0 7 * * *  node dist/jobs/digest.js

# Scale the app to three replicas and this is three digests.
# The usual fixes:
#   - a lock in redis that nobody owns when the holder dies
#   - a designated leader pod, which is a single point of failure
#   - one special replica, which is the same thing with fewer words
A declared schedule, coordinated
Built in
// schedules/digest.schedule.ts
export default defineSchedule({
  name: 'digest.daily',
  cron: '0 7 * * *',

  // Coordinated through the database you already run: one run is
  // ONE execution, however many replicas are up. A process that
  // dies mid-run releases its claim rather than blocking the next.
  run: (ctx) => sendDigest(ctx),
})

Three replicas and a naive cron is three invoices, three emails, three charges. The usual fixes are a lock nobody owns or a designated leader pod that becomes a single point of failure. A declared schedule coordinates through the database you already have — one run means one execution, however many processes are up.

Scheduled jobs, in depth.

Why do scheduled jobs break when you scale out?

Because a cron library ticks in every process. Run two containers and the nightly invoice job runs twice; run twenty and you have sent twenty emails to the same customer. The usual answers are a distributed lock somebody maintains, or a designated cron box that becomes a single point of failure.

Here coordination is part of the primitive. The fleet agrees on who runs a given occurrence, so the same file fires once per tick whether you deploy one container or twenty — and nothing in the job changes when you scale.

That removes the decision most teams get wrong on the day they add a second replica, because at that moment the job looks fine in development and has already double-fired in production.

What happens to occurrences that were missed?

They are caught up rather than pretended away. A deploy, a crash or a scale-to-zero window means ticks pass with nothing running; on boot the scheduler walks forward from the last recorded run and fires what was missed — bounded, so a long outage cannot stampede.

The bound matters as much as the catch-up. Firing four hundred missed occurrences at once turns an outage into a second outage, so the framework caps the work rather than treating "eventually consistent" as permission to flood.

For a window catch-up cannot reach, `voltro schedule backfill` fires every occurrence between two timestamps, sequentially, each recorded against its own cron-derived time. It refuses above a threshold without confirmation and refuses outright above a hard cap — never a silent prefix of what you asked for.

How do I test or trigger a job on demand?

`voltro schedule run` fires a registered schedule immediately, against a running process or standalone, marked as a manual trigger so the record says how it happened. Testing a job stops meaning "wait until three in the morning" or "temporarily change the expression and hope you remember to change it back".

Runs are recorded like any other work: timing, outcome and failures are visible through the same inspect surface as traces and workflows, so "did last night's job run?" is a query rather than a log search.

If the work must survive a crash halfway rather than merely start on time, the job starts a durable workflow. Scheduling answers when; durability answers what happens when the process dies at step three.

What identity does a job run as?

One the engine hands it, not one the calling code invents. That sounds pedantic until you see the failure: a scheduled job has no caller, so whatever subject it runs as is constructed by whichever path started it — and when two boot paths each construct one, they can disagree while both look correct.

This framework shipped exactly that. Development supplied a tenant-flavoured fallback and production supplied a tenant-less subject, and because a tenant-less subject reads as "system, unscoped", the same cron read one tenant locally and every tenant in production — silently, with nothing at either call site able to notice.

The fix generalises: when both paths need a value that is neither request-derived nor configured, the engine passes it in. A callback that receives a value cannot drift; one that invents it can, and did.

What the primitive coordinates.

What the scheduling primitive handles
ConcernHow it is handled
DefinitionA file with an expression and a handler, discovered by convention — no registry to append to.
CoordinationN replicas fire an occurrence once, without a hand-written lock or a designated cron host.
Catch-upMissed ticks are fired on boot from the last recorded run — bounded so an outage cannot stampede.
Manual runsFire on demand, recorded as a manual trigger rather than disguised as a scheduled one.
BackfillAn explicit range, sequential, each against its own cron time; refuses above a cap rather than truncating.
External triggersThe same definition can be driven by Kubernetes CronJobs or a cloud scheduler instead of ticking internally.

Frequently asked questions

Will my job run twice if I run two containers?

No. Coordination is part of the primitive: the fleet agrees on who runs each occurrence, so scaling out does not multiply your emails, invoices or exports. You do not write a lock and you do not need a designated cron host.

What happens to jobs missed during a deploy?

They are caught up on boot, walking forward from the last recorded run, and the catch-up is bounded so a long outage cannot fire hundreds of occurrences at once. For a window that boot catch-up cannot reach, backfill an explicit range.

Can I trigger a schedule manually?

Yes, with `voltro schedule run`, against a running process or standalone. It is recorded as a manual trigger, so the run history distinguishes it from a scheduled firing rather than blurring the two.

Can I use Kubernetes CronJobs instead?

Yes. The same definition can be triggered externally rather than ticking internally, so which scheduler you use is a deployment decision instead of a rewrite of the job.

What tenant does a scheduled job see?

The one the engine hands it, explicitly. That is deliberate: when two boot paths each invented a subject, the same cron read a single tenant in development and every tenant in production, silently — so the value is passed in rather than constructed per path.

What scheduling leans on.

Open the framework. See it for yourself.

Every primitive on this page is in the framework today. Clone the starter, run `voltro dev`, and have it on screen in two minutes.