Skip to content

Schedules and reminders

Two ways an agent acts without an inbound message. 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.

Schedules in Markdown

A plain markdown file with cron: frontmatter is a fire-and-forget task:

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

Schedules as handlers

defineSchedule with a run handler gives you full control, most usefully to hand the work into a channel so its delivery events fire:

ts
import { defineSchedule } from "@cursor/july/schedules";
import webhook from "../channels/webhook.js";

export default defineSchedule({
  cron: "*/30 * * * *",
  async run({ receive, waitUntil, appAuth, host }) {
    // optional: await host.mcp.callTool("units", "celsius_to_fahrenheit", { value: 0 });
    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 and dev mode

In production (agent-sdk serve), schedules fire on their cron cadence. Disable them with --no-schedules. There's no cross-host coordination, so run them in exactly one process per project.

In dev (serve --dev), schedules never fire automatically. Dispatch one by hand, exactly once, through the same path production uses:

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

Reminders

A reminder is created at runtime and bound to a channel continuation. When it fires, it wakes that conversation. Recurring reminders behave like setInterval, one-shots like setTimeout, and both are durable on disk.

ts
await handle.createReminder({
  purpose: "ci_recheck",
  channelId: "drive",
  continuationToken: "pr:owner/repo#1",
  delay: "2h", // or a cron / explicit schedule
  prompt: "Re-check CI. Only act if still failing.",
  until: "Cancel once CI is green or the PR is merged.",
});

Use run when host code should decide what happens on each tick:

ts
await handle.createReminder({
  purpose: "ci_recheck",
  channelId: "drive",
  continuationToken: "pr:owner/repo#1",
  every: "30m",
  async run({ fireCount, followup }) {
    if (fireCount >= 3) {
      return { action: "stop" };
    }

    await followup({
      message: "Re-check CI and report only if the status changed.",
    });
    return { action: "delivered" };
  },
});

This reminder wakes the conversation three times, then stops itself.

The same API is host.reminders on channel handlers, tools, and schedule runs. An agent can even be given a tool that creates its own reminders.

For example, create agent/tools/remind_me.ts:

ts
import { defineTool } from "@cursor/july/tools";
import { z } from "zod";

export default defineTool({
  description: "Schedule a one-time reminder in this conversation.",
  inputSchema: z.object({
    delay: z
      .string()
      .describe("When to wake the conversation, such as 20m or 2h"),
    prompt: z
      .string()
      .min(1)
      .describe("What the agent should do when it wakes"),
  }),
  async execute({ delay, prompt }, ctx) {
    const reminders = ctx.host.reminders;
    if (reminders === undefined) {
      throw new Error("Reminders are disabled on this host.");
    }

    const continuationToken = ctx.session.continuationKey;
    if (continuationToken == null) {
      throw new Error("This session cannot receive reminder follow-ups.");
    }

    const reminder = await reminders.create({
      purpose: "user_follow_up",
      channelId: ctx.session.channelId,
      continuationToken,
      delay,
      prompt,
    });

    return {
      reminderId: reminder.id,
      nextFireAt: reminder.nextFireAt,
    };
  },
});

The tool binds the reminder to the current channel conversation. When the delay expires, the prompt returns to the same session as a follow-up.

Reminders fire in one of two styles. The prompt form (above) sends prompt into the session, with until stating the standing cancellation condition for the model to honor. The run form passes a run handler instead: it returns stop, skip, or delivered per tick. That's silent host-side policy with no model turn. Run handlers are in-memory, so after a restart those reminders are disarmed (handler_lost_on_restart); re-arm them from the code path that created them, or prefer the prompt form.

Dev mode matches schedules. Auto-timers follow ServeOptions.reminders (default !dev), so in --dev fire by hand:

bash
curl http://127.0.0.1:3000/<slug>/v1/dev/reminders             # list
curl -X POST http://127.0.0.1:3000/<slug>/v1/dev/reminders/<id>  # fire one

handle.dispatchReminder(id) is the programmatic equivalent.

Two habits worth copying: cancel reminders when their subject dies (say, cancel PR-scoped reminders on pull_request.closed), and keep wake prompts generic. A plain "re-check the PR" works better than replaying stale payload details, because the agent re-reads the live state when it wakes.

Schedule or reminder?

The split comes down to scope and timing.

ScheduleReminder
Definedat deploy time, agent/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)delay, cron, or explicit schedule
Dev modemanual dispatch onlymanual dispatch only (timers off)

What's next

Continue with these pages: