Recurring schedules & DST
A recurring schedule fires the same delivery over and over on a cron cadence. You give
SchedStack a cron expression and (optionally) a timezone; it computes each fire time
itself, DST-correct, and previews the next runs in the response.
Create a recurring schedule
Section titled “Create a recurring schedule”Pass cron instead of a one-shot timing field (delay / fire_at / local_fire_at).
Add timezone to anchor the cron to a wall clock; omit it and SchedStack uses UTC.
curl -X POST https://api.schedstack.com/v1/schedules \ -H "Authorization: Bearer sk_test_…" \ -H "Content-Type: application/json" \ -d '{ "endpoint": "https://acme.dev/cron/digest", "method": "POST", "body": "{\"job\":\"daily-digest\"}", "cron": "30 9 * * *", "timezone": "America/New_York", "metadata": {"job": "daily-digest"} }'30 9 * * * in America/New_York means 09:30 local time, every day. The response
echoes the schedule and previews the next fire instants — note next_runs is computed
against the target timezone, so the absolute UTC offset shifts across a DST boundary while
the local time stays at 09:30:
{ "id": "sch_01J9ZK3F8Q", "object": "schedule", "mode": "test", "kind": "recurring", "state": "active", "endpoint": "https://acme.dev/cron/digest", "method": "POST", "header_keys": [], "fire_at": null, "cron": "30 9 * * *", "timezone": "America/New_York", "next_fire_at": "2026-06-28T13:30:00Z", "next_runs": [ "2026-06-28T13:30:00Z", "2026-06-29T13:30:00Z", "2026-06-30T13:30:00Z", "2026-07-01T13:30:00Z", "2026-07-02T13:30:00Z" ], "ttl": null, "retry_policy": { "max_attempts": 8, "strategy": "exponential", "base": "5s", "factor": 2, "max": "1h0m0s", "jitter": true }, "metadata": {"job": "daily-digest"}, "created_at": "2026-06-27T18:00:00Z", "updated_at": "2026-06-27T18:00:00Z"}The jitter field is accepted and echoed back, but it is not currently applied to
retry timing — retries use deterministic exponential backoff.
Each fire produces its own delivery — signed and retried independently. A recurring
schedule is at-least-once like any SchedStack delivery, so your receiver still verifies the
signature and deduplicates on the Idempotency-Key. See
Verifying signatures.
The cron format
Section titled “The cron format”cron is a 5-field standard cron expression:
┌───────────── minute (0–59)│ ┌─────────── hour (0–23)│ │ ┌───────── day-of-month (1–31)│ │ │ ┌─────── month (1–12 or JAN–DEC)│ │ │ │ ┌───── day-of-week (0–6 or SUN–SAT; 0 = Sunday)│ │ │ │ │* * * * *It is minute-granular: there is no seconds field. The five fields support the usual
operators — * (every), , (lists), - (ranges), and / (steps).
| Expression | Meaning |
|---|---|
30 9 * * * |
09:30 every day |
0 * * * * |
Top of every hour |
*/15 * * * * |
Every 15 minutes |
0 0 * * 1 |
Midnight every Monday |
0 9 1 * * |
09:00 on the 1st of every month |
0 8 * * 1-5 |
08:00 on weekdays |
timezone is any IANA timezone name (e.g. America/New_York, Europe/London,
Asia/Kolkata, UTC).
DST-correct recurrence
Section titled “DST-correct recurrence”This is where SchedStack differs from naive cron. When a schedule is anchored to a timezone that observes daylight saving, two clock-shift days a year break the assumption that every local time happens exactly once. A naive scheduler either skips a fire or fires it twice. SchedStack does neither — it computes occurrences from the local wall clock and resolves the edge cases deterministically.
Spring-forward — fires once, never zero times
Section titled “Spring-forward — fires once, never zero times”On the spring-forward day the local clock jumps ahead (in US zones, 02:00 → 03:00), so a
wall time inside the gap — say 30 2 * * * (02:30) — does not exist that day.
- Naive cron: the 02:30 slot is invalid, so the job is skipped entirely for that day.
- SchedStack: the fire shifts to the gap-end — the first valid instant after the transition (here, 03:00 local) — so the job fires once, never zero times.
Fall-back — fires once, never twice
Section titled “Fall-back — fires once, never twice”On the fall-back day the local clock repeats an hour (in US zones, 02:00 → 01:00), so a
wall time inside the repeated hour — say 30 1 * * * (01:30) — happens twice.
- Naive cron: the 01:30 slot matches both occurrences, so the job fires twice.
- SchedStack: the fire lands on the first (pre-transition) occurrence only, so the job fires once, never twice.
Times outside the gap or fold are unaffected: a 09:30 daily job fires at 09:30 local every day, and the engine carries the corresponding UTC offset for you.
Pause, resume, or cancel
Section titled “Pause, resume, or cancel”A recurring schedule keeps firing until you stop it. Use the lifecycle controls — they don’t lose in-flight deliveries:
-
Pause to stop future fires without deleting the schedule:
Terminal window curl -X POST https://api.schedstack.com/v1/schedules/sch_01J9ZK3F8Q/pause \-H "Authorization: Bearer sk_test_…" -
Resume re-activates the schedule and any paused deliveries:
Terminal window curl -X POST https://api.schedstack.com/v1/schedules/sch_01J9ZK3F8Q/resume \-H "Authorization: Bearer sk_test_…" -
Cancel to stop the schedule permanently:
Terminal window curl -X POST https://api.schedstack.com/v1/schedules/sch_01J9ZK3F8Q/cancel \-H "Authorization: Bearer sk_test_…"
To change the cron or timezone of an existing schedule, use the reschedule operation
rather than editing in place. The full state machine — active → paused → canceled,
and what happens to a delivery already in flight when you pause or cancel — is covered in
Schedule lifecycle.