Schedules
Run a job on a cron expression in a time zone, pause and resume schedules, and understand what happens when a schedule overlaps or is missed.
A schedule runs a job on a timetable: every night at two, every fifteen minutes, the first Monday of the month. Runnel keeps the timetable, so you no longer need a cron server, and every scheduled run is an ordinary run with retries, logs and a status.
Defining a schedule
Schedules are declared in code next to the job they run. runnel deploy registers them, and they start firing in that environment straight away.
import { runnel } from '@runnel/sdk';
import { usageReport } from './jobs/usage-report';
import { syncSupplier } from './jobs/sync-supplier';
runnel.schedule({
name: 'nightly-usage',
cron: '0 2 * * *',
timezone: 'Europe/London',
job: usageReport,
payload: { period: 'day' },
});
runnel.schedule({
name: 'supplier-sync',
cron: '*/15 * * * *',
job: syncSupplier,
payload: { supplierId: 'sup_atlas' },
});Import schedules.ts from your worker file so the definitions are picked up. One job can have several schedules with different payloads.
Cron expressions
Runnel uses the standard five-field syntax: minute, hour, day of month, month, day of week. A few that come up often:
| Expression | Runs |
|---|---|
*/15 * * * * | every 15 minutes |
0 2 * * * | daily at 02:00 |
30 8 * * 1-5 | weekdays at 08:30 |
0 9 1 * * | the first of every month at 09:00 |
0 0 * * 0 | Sundays at midnight |
The shorthands @hourly, @daily and @weekly work too. Seconds are not supported; the shortest interval is one minute.
Time zones
Without timezone, a schedule runs in UTC. With it, the expression is read in that zone, using IANA names such as Europe/London or America/Chicago. Daylight saving is handled the way people expect:
- When clocks go forward and a time does not exist (02:30 on the last Sunday of March in London), the run fires at the next valid minute.
- When clocks go back and a time happens twice, the run fires once, on the first occurrence.
Tip Reports for people should use their time zone. Housekeeping jobs (cleaning up expired sessions, rotating files) are simpler in UTC, where every day has 24 hours.
Overlaps and missed runs
If a run is still going when the next one is due, Runnel starts the next one anyway. For jobs that must not overlap, set overlap: 'skip' and the new run is skipped and logged instead:
runnel.schedule({ name: 'supplier-sync', cron: '*/15 * * * *', job: syncSupplier, overlap: 'skip', payload: { supplierId: 'sup_atlas' } });Schedules fire from Runnel’s side, so a worker that is down does not lose them: the runs wait in the queue until a worker picks them up. If you would rather drop runs that waited too long, add expireAfter: '10m'; queued scheduled runs older than that are cancelled.
Pausing and resuming
You can pause a schedule from the dashboard or the CLI without a deploy. A paused schedule fires nothing, and on resume it does not catch up on the runs it skipped.
runnel schedules list --env production
runnel schedules pause nightly-usage --env production
runnel schedules resume nightly-usage --env productionA paused schedule stays paused across deploys until you resume it. Removing a schedule from your code and deploying deletes it.
Python and Go
runnel.schedule(name="nightly-usage", cron="0 2 * * *", timezone="Europe/London",
job=usage_report, payload={"period": "day"})runnel.NewSchedule("nightly-usage", "0 2 * * *", UsageReport,
runnel.InTimezone("Europe/London"), runnel.WithPayload(Period{Kind: "day"}))To see when a schedule will next fire, run runnel schedules list; it prints the next three times in the schedule’s own zone. The Jobs API returns the same under each job’s schedules.