Scheduled jobs
Run background work on a schedule — digests, cleanups, syncs. You write named job handlers in api/cron.js, and schedule them with the cron MCP tools. Each job runs with the same env bindings as your routes.
Define handlers in api/cron.js
api/cron.js is a special file (not an HTTP route). Default-export a map of job name → handler. Handlers receive (env, ctx):
// api/cron.js
export default {
async daily_digest(env, ctx) {
const { results } = await env.DB
.prepare("SELECT email FROM subscribers WHERE digest = 1")
.all();
for (const row of results) {
await env.EMAIL.send({
to: row.email,
subject: "Your daily digest",
html: "<p>Here's what's new…</p>",
});
}
},
async cleanup_temp(env, ctx) {
// …
},
};An array of { name, handler } objects works too — use whichever reads better.
Schedule jobs with the MCP tools
manage_cron(action: set) — create or update a schedule, pointing at a job name fromapi/cron.js.manage_cron(action: list) — see an app's scheduled jobs.manage_cron(action: run) — trigger a job immediately (handy for testing).manage_cron(action: delete) — stop a schedule. (The handler stays inapi/cron.jsuntil you remove it.)
In practice you just ask your assistant: “run daily_digest every morning at 7am” — it writes the handler and calls manage_cron (action: set).
Limits
The number of jobs per app, and how often they can run, depend on your plan. Free apps run once a day at most; paid plans can run as often as every 5 minutes — see Limits & quotas.
Each run has a 5-minute ceiling — if a handler runs longer it's cut off and recorded as a timeout (you'll get a heads-up notification at ~4 minutes). For longer work, return quickly and finish in ctx.waitUntil(...), or split it across runs.
manage_cron (action: run) to dry-run a job on demand before relying on the schedule.