Schedules (Cron)
Declare cron jobs with defineSchedule — the compiler emits the Worker scheduled() handler and wrangler.toml triggers.
defineSchedule is the trigger-side mirror of defineQueue. Drop a file in services/schedules/ and the compiler emits a Worker scheduled() (cron) handler plus the matching [triggers] crons in wrangler.toml — no separate cron Worker, no hand-patching the generated entry.
Define a schedule
// services/schedules/sweepDigests.ts
import { defineSchedule } from "quickback";
export default defineSchedule({
name: "sweepDigests", // identifier + audit actor (system:cron-<name>)
cron: "* * * * *", // Cloudflare cron syntax
description: "Send pending unread-email digests",
execute: async ({ db, env, services }) => {
await runDigestSweep(db, env, services);
},
});One file per schedule under services/schedules/*.ts. Files prefixed with _ are ignored. name must be a valid camelCase identifier (it's used as a generated symbol and the audit actor).
What the compiler emits
scheduled(event, env, ctx)in the Worker's default export, dispatching to every schedule whose cron matchesevent.cron. Multiple schedules can share a cron; a failure in one is isolated from the others.[triggers] crons = [...]inwrangler.toml— one deduped, sorted list across all schedules.src/lib/schedules.ts— the generated runner. Regenerated on everyquickback compile, so there's nothing to hand-maintain.
Execute context
| Field | Type | Notes |
|---|---|---|
db | Drizzle instance | The same DB the queue handlers get |
env | CloudflareBindings | All worker bindings |
services | Services | The services layer (createServices(env)) |
cron | string | The cron pattern that fired |
scheduledTime | number | event.scheduledTime (ms epoch) |
withInternalContext | helper | Opt into the INTERNAL trust zone for roles: ['INTERNAL']-gated rows |
Audit & trust
Cron runs are server-internal: there's no authenticated user, no request, no org scope. A fired tick gets a plain db (unscoped Drizzle) plus a withInternalContext helper:
execute: async ({ db, withInternalContext }) => {
// `db` here is unscoped — no firewall WHERE, no audit actor.
// Opt into the INTERNAL trust zone for cross-tenant work:
await withInternalContext(async ({ ctx, db, services }) => {
// ctx.internal = true, ctx.userId = "system:cron-<name>"
// audited writes are attributable to e.g. system:cron-sweepDigests
});
},withInternalContext builds a context with internal: true and userId: "system:cron-<name>", so audited writes made through it are attributable to the schedule (e.g. system:cron-sweepDigests) and can reach roles: ['INTERNAL']-gated rows. Reach for it whenever the schedule legitimately operates across tenants.
Failure isolation
The generated scheduled() dispatcher runs every schedule whose cron matches event.cron concurrently (Promise.all), and each handler's errors are caught individually — one failing schedule logs [Cron] Schedule error and the others still run to completion. A cron with no registered handler logs [Cron] No schedule registered and returns.
Cron syntax
Standard Cloudflare cron (minute hour day-of-month month day-of-week):
| Pattern | Runs |
|---|---|
* * * * * | every minute |
0 * * * * | top of every hour |
0 0 * * * | daily at 00:00 UTC |
*/5 * * * * | every 5 minutes |
See Cloudflare's Cron Triggers docs for the full grammar and limits.