# Schedules

> Cron-triggered runs of a declared function — versioned with the code, testable locally.

[HTML version](https://snoozestack.com/docs/schedules/) · [Docs index](https://snoozestack.com/llms.txt)

A schedule runs one of your [functions](https://snoozestack.com/docs/functions.md) 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](https://snoozestack.com/docs/permissions.md), the same local runtime, its own signals — applies unchanged.

### Declaring one

```bash title="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
```

| 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:

```bash title="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

```ts title="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](https://snoozestack.com/docs/signals.md): 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](https://snoozestack.com/docs/queues.md) 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](https://snoozestack.com/docs/logs.md) alongside everything else the function does, and a failure is visible there rather than being swallowed by the timer.
