# Schedule Agent follow-ups

> Schedule future Agent invocations, repeat checks, and inspect or cancel schedules owned by a Session.

`ctx.schedule()` records future Agent invocations owned by the current Session. The creating Run can finish; the schedule remains until it fires, is replaced, or is cancelled. Each occurrence delivers ordinary JSON input to a new Run through the existing Agent admission path.

## Before you begin {#before-you-begin}

Use `@constal/sdk` 3.9.0 or newer and deploy the Agent on a platform with schedule support. Self-schedules use the current Agent and Session. To call another Agent, bind that Agent as a Resource and choose its destination Session explicitly.

Use `ctx.await()` with a timeout when the current Run should sleep and resume at the same point. Use a schedule when a future invocation should outlive the current Run. A busy destination Session queues the invocation; a scheduled input does not answer an open conversational wait.

## Steps {#steps}

### Schedule one check {#one-check}

```ts
await ctx.schedule("check-pr-42", {
  after: "10m",
  input: { kind: "check-pr", repository: "acme/api", pullRequest: 42 },
});
```

The call returns the durably recorded `Schedule`, including its name, generation, destination, input, `registeredAt`, and `nextRunAt`. It does not wait for the check to execute. The input arrives through the Agent's normal `onMessage(input, ctx)` entry point, or normal initialization in durable mode.

For an absolute instant, replace `after` with `at: "2026-10-01T12:00:00Z"`. The timestamp must include a UTC offset. An instant in the past is immediately eligible.

## Let each check choose its next time {#adaptive-checks}

The Agent can schedule another check after inspecting current state:

```ts
const pr = await ctx.invoke<{ merged: boolean }>(
  ctx.resources.github!, "pull_request.get",
  { repository: input.repository, number: input.pullRequest },
);

if (!pr.merged) {
  await ctx.schedule(`check-pr-${input.pullRequest}`, {
    after: "10m",
    input,
  });
}
```

Use the operation declared by your GitHub binding. Once the PR has merged, the Agent finishes without scheduling another check. Each check can choose a different delay. `after` is measured from registration, so scheduling after the check expresses a delay after that work.

## Maintain a fixed cadence {#recurring}

```ts
await ctx.schedule("repository-watch", {
  every: "15m",
  input: { kind: "check-repository", repository: "acme/api" },
});
```

The first occurrence is one interval after registration. Subsequent times stay anchored to that cadence regardless of execution duration. Fixed durations accept `ms`, `s`, `m`, `h`, `d`, and `w`; they must resolve to a positive whole number of milliseconds.

For a daily wall-clock time:

```ts
await ctx.schedule("morning-review", {
  every: "day",
  at: "09:00",
  timezone: "America/Los_Angeles",
  input: { kind: "review-repository", repository: "acme/api" },
});
```

Daily schedules require a timezone. On a daylight-saving gap, a missing clock time moves to the first valid instant that local date. On a repeated clock time, the first occurrence is used, once per local date. A local date skipped entirely by a timezone change has no occurrence.

### Missed occurrences {#missed}

The default `missed: "catch-up"` preserves every due occurrence. Recovery processes the backlog through ordinary admission and backpressure.

For monitoring where one current check covers missed checks, choose `missed: "coalesce"` explicitly:

```ts
await ctx.schedule("repository-watch", {
  every: "15m",
  missed: "coalesce",
  input: { kind: "check-repository", repository: "acme/api" },
});
```

Coalescing combines pending, undelivered occurrences. Accepted invocations retain their own identity. The receiving Run exposes the covered range through `ctx.run.schedule`: `scheduledAt`, `through`, and `count`, together with the schedule identity, generation, and creating Run. The saved input remains unchanged.

## Choose another destination {#destination}

```ts
await ctx.schedule("research-follow-up", {
  agent: ctx.resources.researcher!,
  session: "repository-research",
  after: "1h",
  input: { kind: "check-repository", repository: "acme/api" },
});
```

Omit both `agent` and `session` to use the current Agent and Session. Specify only `session` for another Session of the current Agent. Another Agent must be a bound Resource and requires an explicit Session, following one-way `ctx.send()` conventions. The creating Session continues to own, list, and cancel the schedule. Tenant and customer identity come from accepted execution authority.

Self-schedules use normal Session routing when a new Run is admitted. Calls to another bound Agent honor its pinned revision. An admitted Run keeps its accepted revision. A registered subtask is not implicitly selected by its Session; a schedule invokes the Agent's normal entry point.

A self-schedule in the same conversation preserves the original reply route for explicit presentations, such as `ctx.commit(result, { presentation })`. The Run remains independent. Another Agent or Session receives no inherited reply route. This does not automatically post a scheduled result; the Agent still chooses what to present.

## Inspect, replace, or cancel {#manage}

```ts
const page = await ctx.listSchedules({ state: "active", limit: 50 });
const next = page.nextCursor
  ? await ctx.listSchedules({ cursor: page.nextCursor })
  : null;

await ctx.schedule("research-follow-up", {
  agent: ctx.resources.researcher!,
  session: "repository-research",
  after: "2h",
  input: { kind: "check-repository", repository: "acme/api" },
});

const cancelled = await ctx.cancelSchedule("research-follow-up");
```

Names are unique within the owning Session. A new `schedule()` operation using the same name creates a new definition generation and replaces future work from the old generation. Replay returns the original operation's receipt without replacing a newer definition or resurrecting a cancelled schedule.

Cancellation stops future generation and occurrences still owned by the scheduler. Accepted destination invocations, including queued Runs, keep their normal lifecycle. The cancellation receipt's paginated `deliveries` identifies accepted and unresolved handoffs; an uncertain delivery is reconciled against the destination's acceptance record. Use ordinary Run controls to stop an already-accepted Run.

`listSchedules` accepts `name`, `state`, `limit` (1–200), and `cursor`. Large inputs can produce shorter pages using the platform's existing artifact page size; follow `nextCursor` to read the rest. Values are preserved in full. `lastOccurrence` belongs to the current definition generation; the occurrence API retains earlier generations. A schedule's `completed` state means it has no further due times; inspect the occurrence for delivery and execution status. Deleting the owning Session stops its future schedules and reconciles outstanding handoffs.

## Authority, input, and results {#execution}

Registration is authorized and journaled. Delivery rechecks current authority and applicable Policy; the creating Run's lease is unnecessary once registered. Each target Run uses its ordinary budgets, limits, and accounting. Repeated firings are independent invocations with causation to their creator; they do not accumulate a recursive call chain.

The schedule retains the full JSON input through ordinary artifact storage. Large inputs and receipts use references without truncation. Each recurrence receives the saved input and can fetch current state when it executes.

A delivery retry keeps the same occurrence identity. Permanent rejection and target Run failure remain visible on the occurrence. Recurrence continues on its declared cadence. Target results are recorded by reference; a schedule does not keep a result Handle open in the creating Run.

Tools can call these SDK methods. A Tool that registers or cancels schedules declares `maxEffect: "non-idempotent"`, as for Agent sends. A Tool scheduling another Agent must also declare that Agent binding in `needs`. Tool guards cannot mutate schedules.

## Verify {#verify}

Start a Run that registers a short one-time schedule and finishes. Read the owning Session's schedule, then its occurrences:

```http
GET /v1/namespaces/{namespace}/agents/{agent}/sessions/{session}/schedules
GET /v1/namespaces/{namespace}/agents/{agent}/sessions/{session}/schedules/{name}
GET /v1/namespaces/{namespace}/agents/{agent}/sessions/{session}/schedules/{name}/occurrences
DELETE /v1/namespaces/{namespace}/agents/{agent}/sessions/{session}/schedules/{name}
```

Reads require `session:read`; cancellation requires `session:steer`. URL-encode the name. Collection reads return `nextCursor`; pass it as `cursor` on the same collection for another page.

Confirm that one occurrence has one target Run ID and that the target receives the complete input. Its Run detail exposes `lineage.schedule`. Occurrence `startedAt` records the first runner lease when completion is reported; the actual target Run exposes its live execution state. Repeat the creating Run's event ID and confirm that the schedule is not duplicated. Test the application's chosen catch-up behavior before using a frequent recurrence.

## Next steps {#next-steps}

Use [Durable execution](/docs/sdk/durable-execution.md) for waits and subtasks, [Build Agents with the SDK](/docs/agents/sdk.md) for the Agent contract, and [Agents and Runs API](/docs/api/agents-and-runs.md) to inspect execution.
