Planning
Planning gives an agent route a visible todo list that can be seeded from a file, updated by the model, stored in route state, and streamed to clients.
Use planning when the route regularly does multi-step work and you want the plan to be inspectable instead of buried in the model's hidden reasoning.
Quick start
Add plan.md next to an agent() route:
src/app/support/[tenant]/
index.ts
plan.mdSeed it with markdown checklist items:
- [ ] Understand the customer request
- [ ] Check account context
- [ ] Decide whether to answer or escalate
- [ ] Write the final responseWhen the route runs, Dawn adds four things:
- a
writeTodostool - a
todosstate channel - a planning prompt fragment
- a
plan_updatestream event afterwriteTodosruns
The model can then keep the plan current during the run.
The plan.md file
The file is route-local. Only the route with that plan.md gets planning.
The parser reads markdown checklist lines:
- [ ] pending item
- [x] completed item
- [X] also completedOther markdown is ignored. Empty checklist items are ignored. Seed items can start as pending or completed; runtime updates can also use in_progress.
If plan.md exists but is empty, the route still opts into planning and starts with an empty todo list.
Dawn skips seed loading for files larger than 64 KiB. The capability still exists, but the initial todo list is empty.
The writeTodos tool
Planning adds this capability tool:
writeTodos({
todos: [
{ content: "Understand the customer request", status: "completed" },
{ content: "Check account context", status: "in_progress" },
{ content: "Write the final response", status: "pending" },
],
})Each todo has:
content: non-empty stringstatus:"pending","in_progress", or"completed"
writeTodos is full-replace. The agent must pass the entire list every time, not just the changed item.
The tool updates runtime state only. It does not write changes back to plan.md; that file is the seed, not a persistence target.
The tool returns the new todos and updates the route state:
{
result: { todos },
state: { todos },
}The LangChain bridge turns that into a state update so later model turns see the new list.
State and prompts
Planning contributes a todos state field with a replace reducer.
Do not also declare a todos field in state.ts. Dawn treats that as a capability conflict and fails route preparation with a message telling you to remove either the state field or the planning marker file.
Do not create a route-local tool named writeTodos. Dawn treats user tools and capability tools with the same name as a conflict.
The planning prompt fragment is re-rendered with the current state. If there are todos, the model sees:
# Planning
For tasks with multiple steps, maintain a plan using `writeTodos({ todos: [...] })`.
...
Current plan:
- [completed] Understand the customer request
- [in_progress] Check account context
- [pending] Write the final responseThat prompt is what keeps the plan visible between tool calls.
Streaming
When runs/stream sees a writeTodos tool result, Dawn emits:
{
"type": "plan_update",
"data": {
"todos": [
{ "content": "Check account context", "status": "in_progress" }
]
}
}If a subagent emits a planning event, the parent stream forwards it with a subagent. prefix, such as subagent.plan_update.
Use /threads/:id/runs/wait when you only need the final output. Use /threads/:id/runs/stream when a UI should show progress.
Generated types
dawn typegen includes writeTodos in the generated route tool types when the route directory contains plan.md.
That makes the tool visible to route-aware TypeScript surfaces, but planning is still only applied automatically to agent() routes at runtime. The contributed todos state field is runtime state; generated route state types still come from state.ts.
Under the hood
Planning is implemented by createPlanningMarker().
During agent route preparation, Dawn detects plan.md, reads seed todos with the checklist parser, contributes the writeTodos tool, contributes the todos state field, and contributes a stream transformer that watches tool results.
The stream transformer accepts both direct { todos } outputs and LangGraph Command-shaped outputs where the update is stored at update.todos. That keeps planning events stable across the current LangChain bridge path.