Scheduled Dispatch
One-liner:
@deepseek-ai/dsh-schedulegives live agents 3 in-session tools (schedule_create/schedule_list/schedule_delete) to manage persistent reminders: the session event log owns reminder state, and the timer and model follow-up are only discardable projections of the log.
The ability to make an agent do something on a schedule. Core insight: reminder state lives not in "an in-memory timer" but in the session event log; so fold/recover reads only the log, and a crashed timer doesn't matter.
1. Three tools
| Tool | Input | Behavior |
|---|---|---|
schedule_create | after_seconds (positive integer) / at (absolute time) / every_seconds (≥5 minutes) | create a reminder |
schedule_list | — | list active reminders (incl. scheduled / overdue state) |
schedule_delete | id | delete a reminder |
schedule_dispatchis not a tool: dispatch is an internal operation in theschedule/changeevent. On-time delivery is done by the runtime (see below).
2. State model (a key design)
session event log (single source of truth)
├── schedule/change events (create/delete/dispatch union, version 1)
├── timer (discardable projection)
└── model follow-up (discardable projection)
Important properties:
- session fold = the full log; a fork folds only past
seedLength: a child session does not inherit the parent session's reminders - before every read, first
ctx.sessions.flush(session); persistence unavailable →persistence_uncertain, and never treat un-confirmed state as a result - replay rejects: unknown versions, duplicate ids, shape-mismatched dispatches, and transitions on inactive records
- after each successful preflight, and after a real barrier succeeds, callback
onDurableChangedrives the owner to recompute
3. Time semantics (strict)
at supports two forms:
| Form | Example |
|---|---|
| RFC 3339 string | 2026-08-12T10:00:00Z or with an offset |
| local form | { date, time, time_zone }: must be explicit UTC or an IANA time zone |
Rejections: missing time zone, offset-less strings, times inside a DST gap (overlap takes the earlier one), and targets that aren't in the future. After creation only the normalized UTC is kept: a Schedule never reads the browser/session/model-context time zone.
Minimum interval: every_seconds can't be smaller than MIN_EVERY_INTERVAL_SECONDS = 300 (5 minutes).
4. Runtime delivery: how a reminder becomes a real turn
An on-time reminder isn't "just a state change" — it's delivered to the agent as one real user message:
one ScheduleRuntime per root agent maintains the loop
→ a timer uses whenIdle() + MAX_TIMER_DELAY_MS(2147483647) to wait for an idle phase
→ on time, uses agent.runMaintenance(...) to claim an idle phase
→ builds a user message; agent.followup(message) injects one "follow-up turn"
→ this is precisely the next-turn input the model actually receives and processes
Tool calls also carry a cancellable transaction (runCancellableScheduleTransaction / exec.signal) to prevent duplicate delivery.
5. Composition requirements
- load order: after
ctx.sessions,ctx.agents,ctx.tools,ctx.sessionPersistence+ the flush-persistence listener; a failed static injection = the composition fails outright - only listens to future
agent/created: agents and subagents that already exist at load time will not get a Schedule schedule/change's durably-commit delivery barrier and theonDurableChangeobserver drive recomputation after create/delete
6. Verification
# find schedule/change events in the session log (default zstd, two-level directories)
zstdcat ~/.dsh/sessions/*/*/session.jsonl.zstd | jq -r 'select(.type == "schedule/change")'
# inspect what turn it gets delivered into
zstdcat ~/.dsh/sessions/*/*/session.jsonl.zstd | grep -E "user/message|schedule/" | tail
Next steps
- Agent Main Loop: how a follow-up gets driven
- Event System: which plane
schedule/changelives on