# 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:

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

### Schedule handlers

Use `defineSchedule` with a `run` handler to call tools or hand work into
a channel so its delivery events fire:

```ts
import { defineSchedule } from "@grok/agent-sdk/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](#schedules-on-cursor-hosting) |
| Production `agent-sdk 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 |

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

### 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 `@grok/agent-sdk` 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.

```ts
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`:

```ts
// bot/reminders/ci_recheck.ts
import { defineReminder } from "@grok/agent-sdk/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" };
  },
});
```

```ts
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](/docs/reference/agent-config.md#built-in-tools).

`--dev` does not auto-fire reminders. Dispatch one 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.

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](/docs/reference/channels.md): `receive` and the delivery events
- [GitHub guide](/docs/guides/github.md): reminders in a real webhook loop
- [HTTP API](/docs/reference/http-api.md#dev-mode-routes): the dev
  dispatch routes
