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";
import { runDigestSweep } from "../../features/comms/lib/digest-sweep";
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).
Importing feature code
Schedule files live at quickback/services/schedules/<name>.ts, so imports are relative to that location. To reach a feature table, a feature lib/ helper, or db/, climb up to the quickback/ root first (../../):
// quickback/services/schedules/bookingReminders.ts
import { bookings } from "../../features/crm/bookings"; // a feature table
import { generateIcsInvite } from "../../features/crm/lib/ics-generate"; // a feature lib/ helper
import { runDigestSweep } from "../../lib/digest-sweep"; // a shared lib/ helperTwo ../ — up from services/schedules/ to quickback/, then down into features/ (or lib/, db/). The compiler relocates the handler into the generated src/lib/schedules.ts and rewrites these paths to resolve from there automatically — you always write them relative to your own source file, exactly as your editor resolves them.
A single ../features/… is the common mistake — from services/schedules/ that resolves to services/features/…, which doesn't exist (the compiler consumes services/; there is no src/services/). Use ../../features/….
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.