Skip to main content

Command Palette

Search for a command to run...

BDK

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

ModeWhat fires
Cursor-managed hostingCron cadence, from the schedules in the deployed release. See Schedules on Cursor hosting
Production bdk serveCron cadence. --no-schedules disables them. Run them in exactly one process per project
serve --devNothing 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.

FormWhat it doesAfter a restart
PromptSends prompt into the session. until is the standing cancellation condition for the modelStays armed
Named handlerRuns a discovered handler with serializable args; it starts a model turn only if the handler calls followupStays armed
Anonymous runArchitecture v1 only. Host handler returns stop, skip, or delivered per tick; it starts a model turn only if it calls followupMust 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 one

handle.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

ScheduleReminder
Definedat deploy time, bot/schedules/*at runtime, createReminder / host.reminders
Scopeglobal to the agentone channel continuation (one conversation)
Sessionstarts a new task session (or hands off through receive)wakes an existing conversation
Cadencecron (UTC)every, cron, delay, or at
Dev modemanual dispatch onlymanual dispatch only (timers off)