snoozestackdocs

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.

Why use it

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#

snoozestack/project.toml
[[schedules]]
name = "nightly-report"
function = "report" # a declared [[functions]] entry
cron = "0 3 * * *" # minute hour day month weekday
timezone = "America/Phoenix" # optional; UTC when omitted
payload = '{"period": "daily"}'
enabled = true
FieldMeaning
functionThe declared function to invoke — validation fails if no such function exists
cronFive fields: minute hour day month weekday
timezoneAn IANA zone, so a 9am job stays 9am across daylight saving
payloadJSON delivered as the request body, for a function that serves several schedules
enabledSet 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.

SurfaceAvailability
Portal UIRead-only — the console lists a project's schedules; the declaration owns them
CLIsnoozestack check validates, snoozestack publish ships; functions schedules … covers one-off changes
SDK / HTTPNone — 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:

terminal
snoozestack dev
curl -X POST localhost:8787/report -d '{"period":"daily"}' # the same thing the timer does
snoozestack signals tail --type log # watch what it did

Writing the function#

snoozestack/functions/report/index.ts
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.