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 from api/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 in api/cron.js until 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.

Jobs run against your production bindings. Use manage_cron (action: run) to dry-run a job on demand before relying on the schedule.