Schedule recurring runs
Publish workflow jobs on a cron schedule with an explicit time zone.
Publish a job on a schedule
Section titled “Publish a job on a schedule”Create a schedule with createCronSchedule() and give it a cron expression and time zone. runSchedules() publishes a queue job at each matching time until you abort its signal; a worker runs the job separately.
Run the script
Section titled “Run the script”At 02:00 Paris time, Monday to Friday, the scheduler publishes a job for the audit handler with a runId such as audit-2026-09-30. Ctrl+C aborts the signal and runSchedules() resolves.
API reference: CronOptions.
Run the published jobs
Section titled “Run the published jobs”A worker opens the same queue and registers audit with defineWorkflowJob(), which runs one checkpointed workflow per runId: see Job queues and workers. The Nightly maintenance recipe shows the scheduler and the worker together.
Write the cron expression
Section titled “Write the cron expression”An expression has five fields separated by spaces: minute, hour, day of month, month, day of week.
| Syntax | Example | Fires |
|---|---|---|
Value, * | 30 2 * * * | At 02:30 every day; * matches any value. |
| List | 0 9,18 * * * | At 09:00 and 18:00. |
| Range | 0 9 * * 1-5 | At 09:00, Monday to Friday. |
| Step | 0 8-18/2 * * * | At 08:00, 10:00, …, 18:00. */15 in the minute field means every 15 minutes. |
| Names | 0 9 1 JAN,JUL * | At 09:00 on 1 January and 1 July. Names JAN–DEC and SUN–SAT ignore case; 0 and 7 are Sunday. |
| Macro | @daily | At 00:00 every day. Also @hourly, @midnight, @weekly, @monthly, @yearly and @annually. |
| Both days | 0 9 1 * MON | At 09:00 on the 1st of the month and on every Monday, as in Vixie cron. |
This union applies only when neither day field starts with *; otherwise a day must match both fields. There is no seconds field.
Name each run after its local date
Section titled “Name each run after its local date”slot.toISOString().slice(0, 10) gives the UTC date. At 01:00 in Paris, that is still the previous day.
Use slot.toLocaleDateString("en-CA", { timeZone }) with the schedule’s time zone, as in the first snippet, to get the local date as YYYY-MM-DD.
Daylight saving time
Section titled “Daylight saving time”Slots are wall-clock times in the schedule’s time zone.
- Skipped time: A time that does not exist on the spring-forward day does not fire that day.
- Repeated time: A time that occurs twice on the fall-back day fires once, at its first occurrence.
Run several schedulers
Section titled “Run several schedulers”Each slot publishes the job schedule:<name>:<slot ISO time>. A restarted scheduler, or several replicas sharing one queue, publish the same job ID, and the queue keeps a single job.
runId and input must depend only on the slot. The queue rejects a second publication of the same ID with a different request.
Two schedules can return the same runId for one day: the second job then reuses the same checkpointed run if its input is the same (a different input fails with an incompatible checkpoint), like the 07:00 resume in Nightly maintenance.
Catch up after a restart
Section titled “Catch up after a restart”A slot is published only if at most maxLateMs has passed since it (60 000 ms by default). After a restart or a suspended process, only the latest missed slot within that window is published; older ones are skipped.
Raise maxLateMs to catch up a slot missed during a longer outage, for example maxLateMs: 6 * 60 * 60_000 for six hours.
Handle publication failures
Section titled “Handle publication failures”Without onError, the first failed publication stops every schedule and rejects runSchedules(). With it, you receive the error with the schedule name and slot, and scheduling continues with the next slot.
A failed slot is not published again. Errors thrown by onError are ignored.
Compute slots without publishing
Section titled “Compute slots without publishing”next(after) returns the first slot strictly after a date, previous(at) the latest slot at or before it. Neither publishes anything.
This prints 2026-03-30T00:30:00.000Z: 02:30 does not exist in Paris on 29 March 2026.
createCronSchedule() throws on an invalid field, an unknown time zone or an expression with no occurrence, such as 0 0 30 2 *.
Schedule from CI instead
Section titled “Schedule from CI instead”Without a long-running process, a scheduled CI job, such as a GitHub Actions schedule workflow, can start the workflow directly with a checkpoint. See Run in CI.
Limits
Section titled “Limits”- The finest resolution is one minute.
- After downtime, only the latest missed slot within
maxLateMsis published. - A schedule
nameis unique and uses letters, digits,.,_and-(128 characters at most). ArunIdhas at most 256 characters.
API: createCronSchedule · runSchedules · TriggerSchedule · CronSchedule · defineWorkflowJob.