Skip to content

Core concepts

Schedules

A schedule creates a job on a recurring timetable: a cron expression, a time zone and the job to create. Use it for reports, cleanups, syncs and anything else that repeats.

On this page

Creating a schedule

Each time a schedule fires, Halyard creates an ordinary job with the queue, task and payload you set, so the run gets the same retries, logs and alerts as any other job. The job's payload also includes scheduled_for, the exact time it was due.

Declare schedules in halyard.yaml so they live in version control and deploy with your code, or manage them through the API.

# halyard.yamlschedules:  nightly-report:    cron: "30 2 * * *"    timezone: Europe/Amsterdam    queue: reports    task: nightly-report    payload: { format: "pdf" }    overlap: skip
Schedule object
{  "id": "sch_5Tq2Bw9Nf4",  "object": "schedule",  "name": "nightly-report",  "cron": "30 2 * * *",  "timezone": "Europe/Amsterdam",  "queue": "reports",  "task": "nightly-report",  "overlap": "skip",  "state": "active",  "last_run": { "job_id": "job_8Vn4Hc2Lp6", "status": "succeeded", "at": "2026-10-17T00:30:00Z" },  "next_run_at": "2026-10-18T00:30:00Z"}

Cron syntax

Schedules use standard five-field cron expressions. Fields are separated by spaces and evaluated in the schedule's time zone. The finest resolution is one minute.

30minuteat :30
2hour02:xx
*day of monthevery
*monthevery
*day of weekevery
30 2 * * * runs every day at 02:30 in the schedule's time zone.
FieldAllowed valuesSpecial characters
Minute0–59* , - /
Hour0–23* , - /
Day of month1–31* , - / L
Month1–12 or JAN–DEC* , - /
Day of week0–6 or SUN–SAT* , - / L #
  • * any value, , a list (1,15), - a range (9-17), / a step (*/10).
  • L means the last day of the month, or with a weekday the last one of that kind (5L is the last Friday).
  • # picks the nth weekday of the month: MON#1 is the first Monday.
  • Shortcuts @hourly, @daily, @weekly and @monthly are accepted.

Examples

ExpressionRuns
*/15 * * * *Every 15 minutes
0 * * * *At the start of every hour
30 2 * * *Every day at 02:30
0 9 * * MON-FRIWeekdays at 09:00
0 0 1 * *Midnight on the first of every month
0 18 L * *18:00 on the last day of every month
0 10 * * MON#110:00 on the first Monday of the month

Check an expression before you deploy it. The CLI prints the next runs in local time and UTC:

Preview
halyard schedules preview "30 2 * * *" --tz Europe/Amsterdam --count 3   Sun 2026-10-18 02:30 CEST   (00:30 UTC)  Mon 2026-10-19 02:30 CEST   (00:30 UTC)  Tue 2026-10-20 02:30 CEST   (00:30 UTC)

Time zones

Set timezone to an IANA name such as America/New_York or Asia/Tokyo. The default is UTC. A schedule for 09:00 in Europe/Amsterdam runs at 09:00 local time all year, which is 07:00 UTC in summer and 08:00 UTC in winter. Timestamps in the API are always UTC.

Daylight saving time

Twice a year, local clocks skip or repeat an hour. Halyard handles both cases predictably:

TransitionExample (Europe/Amsterdam)What happens
Clocks go forward02:00–02:59 does not exist on 29 March 2026A run due in the skipped hour starts at 03:00, once.
Clocks go back02:00–02:59 happens twice on 25 October 2026A run due in the repeated hour starts on the first pass only.
Interval schedules*/15 * * * *Keep a steady interval in UTC, so they never double up or skip.

The nightly-report above at 02:30 is exactly in the danger window. If that matters, schedule it at 30 3 * * * or use UTC.

Overlapping runs

A run can still be going when the next one is due, for example a report that usually takes 10 minutes on a 15-minute schedule after a slow day. overlap decides what happens:

PolicyBehavior
skipDefaultIf the previous run is still queued or running, the new run is skipped and a schedule.missed event is published.
queueThe new run is queued behind the previous one. At most one run waits; extra runs are skipped.
allowRuns start on time regardless of earlier runs. Use only for idempotent, independent work.
replaceThe running job is cancelled and the new run starts. Useful for snapshot-style syncs.

Pausing and resuming

Pause a schedule to stop it creating jobs without losing its configuration, for example during a migration. Jobs it already created are not affected. When you resume, missed times are skipped unless you pass catch_up, which creates a single job for the whole gap, never one per missed run.

curl -X POST https://api.halyard.example/v3/schedules/sch_5Tq2Bw9Nf4/pause \  -H "Authorization: Bearer hy_test_51Hq8Zx2rTmV9cKpL3d" curl -X POST https://api.halyard.example/v3/schedules/sch_5Tq2Bw9Nf4/resume \  -H "Authorization: Bearer hy_test_51Hq8Zx2rTmV9cKpL3d" \  -d catch_up=false

Delayed jobs or schedules?

Both run work in the future. Pick by whether the work repeats:

Delayed jobSchedule
RunsOnce, at run_at or after delayRepeatedly, on a cron expression
Created byYour code, per eventConfiguration, once
Typical useReminder 3 days after sign-upNightly report, hourly sync
Furthest ahead365 daysNo limit
CancelCancel the jobPause or delete the schedule

See Delayed jobs for delay and run_at.

Suggested