Schedules and reminders
An agent can act without an inbound message in two ways. A schedule is deploy-time cron: "every weekday at 09:00, summarize open incidents." A reminder is a runtime wake bound to one conversation: "re-check this PR's CI in two hours." Schedules live in the filesystem; reminders are created by running code.
Schedules
Cron expressions are standard 5-field, evaluated in UTC with minute granularity.
Markdown schedules
A markdown file with cron: frontmatter is a fire-and-forget task:
---cron: "0 9 * * 1-5"---Pull open incidents and post a summary to the metrics endpoint.Each firing starts a task-mode session: the body is the prompt, the
session runs to session.completed or session.failed, and it isn't
followable.
Schedule handlers
Use defineSchedule with a run handler to call tools or hand work into
a channel so its delivery events fire:
import { defineSchedule } from "@cursor/bdk/schedules";import webhook from "../channels/webhook.js";export default defineSchedule({ cron: "*/30 * * * *", async run({ receive, waitUntil, appAuth, host }) { waitUntil( receive(webhook, { message: "Check for new critical alerts. Report only when there are any.", auth: appAuth, }) ); },});defineSchedule requires exactly one of markdown or run. The run
handler receives receive (hand off into a channel), callTool
(deterministic server-tool calls), waitUntil, appAuth (a
schedule-scoped principal for work the agent does on its own behalf),
and host (shared services: host.mcp, host.github, host.slack,
host.reminders).
Dispatch a schedule
| Mode | What fires |
|---|---|
| Cursor-managed hosting | Cron cadence, from the schedules in the deployed release. See Schedules on Cursor hosting |
Production bdk serve | Cron cadence. --no-schedules disables them. Run them in exactly one process per project |
serve --dev | Nothing automatic. Dispatch by hand through the same path production uses |
curl -X POST http://127.0.0.1:3000/<slug>/v1/dev/schedules/heartbeat# {"scheduleId":"heartbeat","sessionIds":["ses_…"]}The playground can dispatch schedules in dev mode too, and
handle.dispatchSchedule("heartbeat") does it programmatically.
Schedules on Cursor hosting
Each deploy registers the schedules in its release and replaces the
previous set, so a schedule you delete from bot/schedules/ stops after
the next deploy. Hosted schedules need @cursor/bdk 0.2.14 or newer in
the project. A deployment built with an earlier version has no hosted
schedules until you upgrade the package and deploy again.
Every fire starts a new runtime. A Markdown schedule starts a task
session there, and a run handler must finish in 26 minutes, including
the work it passes to receive and waitUntil. Nothing in module memory
carries over to the next fire, so keep state in ctx.host.kv or
ctx.host.files. Each scheduled time starts at most one run, and a
stopped deployment skips its fires.
A schedule's name is its path under bot/schedules/ without the file
extension. On Cursor hosting, a name has 1 to 100 letters, digits, .,
_, -, or /, and starts with a letter or digit. A release holds at
most 64 schedules, and deploy rejects a release that breaks either
rule.
Reminders
A reminder is created at runtime and bound to a channel continuation. A
prompt reminder wakes that conversation when it fires; a handler reminder
runs host code, which can call followup to wake it. Recurring reminders
behave like setInterval, and one-shots behave like setTimeout. Prompt
and named-handler reminders stay armed across restarts.
await handle.createReminder({ purpose: "ci_recheck", channelId: "drive", continuationToken: "pr:acme/checkout#42", delay: "2h", prompt: "Re-check CI. Only act if still failing.", until: "Cancel once CI is green or the PR is merged.",});When host code must decide what happens on each tick, author a named
handler and pass its default export with serializable args:
// bot/reminders/ci_recheck.tsimport { defineReminder } from "@cursor/bdk/reminders";export default defineReminder({ async run({ args, followup }) { await followup({ message: `Re-check CI for ${String(args.prUrl)}. Report only if the status changed.`, }); return { action: "delivered" }; },});import ciRecheck from "./bot/reminders/ci_recheck.js";await handle.createReminder({ purpose: "ci_recheck", channelId: "drive", continuationToken: "pr:acme/checkout#42", every: "30m", handler: ciRecheck, args: { prUrl: "https://github.com/acme/checkout/pull/42" },});Architecture v2, the default, rejects an anonymous run handler and a
function until, locally and on Cursor hosting. Use a prompt or a named
handler.
| Form | What it does | After a restart |
|---|---|---|
| Prompt | Sends prompt into the session. until is the standing cancellation condition for the model | Stays armed |
| Named handler | Runs a discovered handler with serializable args; it starts a model turn only if the handler calls followup | Stays armed |
Anonymous run | Architecture v1 only. Host handler returns stop, skip, or delivered per tick; it starts a model turn only if it calls followup | Must be re-armed |
The same API is host.reminders on channel handlers, tools, and
schedule runs. builtinTools: { reminders: true } adds
reminders_create, reminders_list, and reminders_cancel on the
current conversation; see
Agent config: built-in tools.
--dev does not auto-fire reminders. Dispatch one by hand:
curl http://127.0.0.1:3000/<slug>/v1/dev/reminders # listcurl -X POST http://127.0.0.1:3000/<slug>/v1/dev/reminders/<id> # fire onehandle.dispatchReminder(id) is the programmatic equivalent.
Cancel reminders when their subject dies, for example on
pull_request.closed. Keep wake prompts generic so the agent re-reads
live state instead of replaying a stale payload.
Schedules vs reminders
| Schedule | Reminder | |
|---|---|---|
| Defined | at deploy time, bot/schedules/* | at runtime, createReminder / host.reminders |
| Scope | global to the agent | one channel continuation (one conversation) |
| Session | starts a new task session (or hands off through receive) | wakes an existing conversation |
| Cadence | cron (UTC) | every, cron, delay, or at |
| Dev mode | manual dispatch only | manual dispatch only (timers off) |
Related
- Channels:
receiveand the delivery events - GitHub guide: reminders in a real webhook loop
- HTTP API: the dev dispatch routes