Schedules
A schedule runs one of your functions on a cron timer. It is declared in project.toml next to the function it calls, so the timing, the time zone, and the payload are versioned with the code they trigger — and snoozestack dev fires them locally, which means a recurring job is something you can watch work before it is ever published.
Some work has no request to hang off: a nightly digest, a weekly roll-up, an hourly sync with someone else's API, a cleanup that reclaims what expired. Without a scheduler that work ends up in a cron entry on a machine somebody has to remember, or in a task queue no one reads — either way it is outside the project, and it drifts.
Declaring it beside the function keeps the whole job in one reviewable place: a pull request that changes when the digest runs looks exactly like a pull request that changes what it says. And because the timer targets a function rather than a shell command, everything the rest of the platform gives that function — declared capabilities, the same local runtime, its own signals — applies unchanged.
Declaring one#
[[schedules]]name = "nightly-report"function = "report" # a declared [[functions]] entrycron = "0 3 * * *" # minute hour day month weekdaytimezone = "America/Phoenix" # optional; UTC when omittedpayload = '{"period": "daily"}'enabled = true| Field | Meaning |
|---|---|
function | The declared function to invoke — validation fails if no such function exists |
cron | Five fields: minute hour day month weekday |
timezone | An IANA zone, so a 9am job stays 9am across daylight saving |
payload | JSON delivered as the request body, for a function that serves several schedules |
enabled | Set false to keep the declaration but stop it firing |
snoozestack check validates the expression and the target before anything runs, so a malformed cron field or a typo'd function name is a build error rather than a job that silently never fires.
| Surface | Availability |
|---|---|
| Portal UI | Read-only — the console lists a project's schedules; the declaration owns them |
| CLI | snoozestack check validates, snoozestack publish ships; functions schedules … covers one-off changes |
| SDK / HTTP | None — a schedule is infrastructure, not something app code turns on at runtime |
Running it locally#
snoozestack dev honours declared schedules, so the recurring path is exercised on your machine the same way the request path is. For a job that only runs at 3am, don't wait for it — invoke the function directly, since a scheduled run is an ordinary call with the payload as its body:
snoozestack devcurl -X POST localhost:8787/report -d '{"period":"daily"}' # the same thing the timer doessnoozestack signals tail --type log # watch what it didWriting the function#
export default async function handler(req: Request, capabilities) { const { period } = await req.json().catch(() => ({})); // the declared payload const { rows } = capabilities.db.query( "select count(*) as rounds from rounds where played_at >= date('now', '-1 day')", ); capabilities.signals.emit("event", "digest_sent", { period, rounds: rows[0][0] }); return new Response("ok");}Two habits worth keeping. Make the handler idempotent — a retry, or an operator running it by hand, shouldn't double-send. And have it say what it did through Signals: nobody is watching at 3am, so the emitted event is the only evidence the run happened and what it decided.
- Digests and summaries
- The weekly round-up email, the Monday report. One timer, one function, and the copy it sends lives in the same commit as the schedule.
- Syncing an external system
- Pull yesterday's records from someone else's API on an hourly timer. Pair it with a queue when the work per run is large — the schedule enqueues, the consumer drains.
- Housekeeping
- Expiring invitations, pruning stale drafts, recomputing a leaderboard. Cheap work that has to happen on a rhythm rather than on a request.
Hosted behaviour#
snoozestack publish creates the declared schedules on the managed clock. Each namespace keeps its own — a preview's timers fire against that preview's data and never against live. A run is an ordinary function invocation, so its output lands in logs alongside everything else the function does, and a failure is visible there rather than being swallowed by the timer.