workflow / schedule / plan / todo: Orchestration Primitives
What each of the four orchestration primitives owns, and why they were never merged into one monolith. Core docs: four files including docs/subsystems/workflow.zh.md.
Four real scenarios; pick one primitive per scene. Wrong answers are fine—the verdict explains why. Hit “Play” for the auto walkthrough, or tap a card anytime to answer yourself.
docs/subsystems/workflow.zh.md, schedule.zh.md, plan.zh.md, and packages/todo/tool-todo/README.zh.md, verified on 2026-08-13.Bottom line first: these four primitives do not share a task engine—they don’t even share a persistence shape. workflow is one-shot: the model writes a JS script; the engine runs it in a node:worker_threads vm; agent() in the script calls back to the host to spawn sub-Agents; when it finishes, only results and display records remain—the logic itself never becomes a durable state machine. schedule is durable: create, dispatch, and delete are all schedule/change session events; replaying the log rebuilds every reminder. plan is lighter still—just a fold of plan/mode boolean log events. todo is a snapshot: each todo_write replaces the whole table; the UI projects the latest copy.
The outline question—“should multi-step orchestration be model-written scripts or a framework state machine?”—DSH’s answer is both, with a clear split. Execution orchestration goes to model-written scripts, because orchestration logic varies endlessly and a framework can’t predefine it all; time, stance, and display go to framework state machines, because those three need cross-turn—and even cross-restart—determinism that a model script can’t provide. The workflow doc itself says its meta-field vocabulary aligns with Claude Code’s dynamic workflows (workflow.zh.md lines 41, 49): same lineage, different landing.
One discipline that is easy to miss. Mis-spell an agent() option inside a workflow script and you get a WorkflowError with fatal: true; the parallel() combinator rethrows it and aborts the whole script. Only real sub-Agent runtime failures map to per-item nulls (workflow.zh.md line 116). Bad code and runtime failure are two error classes—mix them and the script becomes undebuggable.
plan is not permissionplan mode is soft guidance: when active it appends a plan:policy section to the system prompt; the tool catalog stays identical (to keep request cache stable). What actually blocks writes is the sandbox and approval—neither reads plan state, so both must be configured separately.
schedule stays in-sessionReminders return only as followup turns in the original session—no push, no external notify channel; a cold session does nothing. Delivery is at-least-once: crash after admission but before dispatch, and recovery will fire the reminder once more.
todo does not drive executiontodo_write is pure display state: full-table replace, write to the log, project to the UI. No partial update, no read-back tool, no stable ids. Treating it as a task engine is the most common misread of this primitive.
First, schedule’s fixed-rate decision—the only code block in this lesson worth reading in full: if the session was offline and missed N due times, recovery does not replay each one; a single division lands on the latest due occurrence, then advances the record into the future. No enumeration, no replay, no backlog:
const steps = Math.floor((acceptedAt - target) / interval)
const occurrence = target + steps * interval
/* v8 ignore next -- bounded operands and a quotient-derived product stay safe. */
if (!Number.isSafeInteger(occurrence) || occurrence < target || occurrence > acceptedAt) {
throw new ScheduleLogError('every occurrence arithmetic must stay within the accepted interval')
}
const occurrenceAt = new Date(occurrence).toISOString()
const next = occurrence + interval
packages/schedule/schedule/src/domain.ts, verified on 2026-08-13. Code blocks keep the original source text.The second boundary is when plan mode takes effect—explained in words. If the user toggles while the model is still streaming, the plugin does not write the log immediately; it parks the choice in process-memory pending and waits for the next in-turn pre-step boundary. Order matters: the listener first await next() asks whether the downstream step accepts; if the downstream refuses, the signal is already cancelled, or there is no pending, it passes through unchanged. Only after all three checks does it append the choice to the log. If the append fails, it logs a warn and still lets the step through—never block a whole turn because a stance switch failed once. That also answers crash semantics: pending lives only in process memory; if you crash before the switch hits the log, after restart plan mode stays as it was before the toggle.
Source: the agent/pre-step listener in packages/plan/plan-mode/src/index.ts lines 205–218, verified on 2026-08-13.
todo’s most opinionated design needs no code paste: allowParallelInProgress is required config—the schema says z.boolean().required() with no default (packages/todo/tool-todo/src/index.ts lines 41–43). Whether multiple tasks may be in progress at once depends on whether this deployment runs concurrent sub-Agents; the tool cannot observe that, so the deployer must declare it. Set to false, and marking a second task in progress yields Error: invalid todos: at most one task may be in_progress (lines 107–109).
Claude Code takes the aggregation path: seven kinds of async work (shell commands, local sub-Agents, remote Agents, Teammate, workflows, MCP monitors, memory consolidation) hang under one Task framework, sharing registerTask, updateTaskState, and kill as one lifecycle (draft study/chapters/06-task-system.md lines 27–47 cite tasks/types.ts). DSH does the opposite: the subagent docs explicitly say the continue path “does not create a Task, nor a wrapper layer that carries intermediate results,” and the four orchestration primitives each have their own persistence shape. Aggregation buys a unified progress UI and management entry; splitting lets each primitive say its semantics all the way down—schedule’s miss coalesce, plan’s pending switch—both of which would have to compromise inside a unified frame.
The split on the small todo tool shows each house’s temperament clearest. Claude Code’s TodoWrite hard-codes discipline in the prompt: “Exactly ONE task must be in_progress at any time (not less, not more)”; items also require content plus activeForm dual forms, with progressive tense shown while running (draft study/chapters/14-all-prompts.md lines 1243–1293 cite TodoWriteTool/prompt.ts verbatim). DSH turns the same discipline into required deploy config: pick true for concurrent sub-Agent setups, false for single-thread discipline; with false, code rejects rather than a prompt advising; item shape stays minimal—only content and a three-state status. One constrains the model with a prompt; the other constrains the deploy with a schema, then lets code enforce it.
Walk two boundaries
First: the model is streaming a long plan; the user clicks “enter plan mode” right then. When is that choice actually written to the log, and when does it start affecting model requests? If the process crashes before this turn ends, is plan mode on or off after restart? (Hint: pending lives only in process memory.) Second: a reminder with every_seconds: 3600; the session was offline 5 hours, then recovers—how many reminders fire at recovery, and where is the next target? Hand-push it with the steps formula from this lesson’s first source block.