Planning
Planning gives an agent route a todo list you can see. You seed it from a file, the model updates it, B4.run keeps it in route state, and clients get it over the stream. Use planning when a route does work in several steps and you want to see the model's plan.
Quick startCopy link to section: 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, B4.run adds four things that let the model keep the plan current:
- a
writeTodostool - a
todosstate channel - a planning prompt fragment
- a
plan_updatestream event afterwriteTodosruns
The plan.md fileCopy link to section: 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 completedThe parser skips other markdown and empty checklist items. Seed items start as pending or completed, and the model can also set in_progress later.
An empty plan.md still turns on planning, with an empty todo list.
B4.run ignores the seed items in a file larger than 64 KiB. Planning still works, but the list starts empty.
The writeTodos toolCopy link to section: 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 replaces the whole list, so the agent passes every item each time.
The tool updates the run's state. plan.md is only the starting list and stays as you wrote it.
The tool returns the new todos and updates the route state:
{
result: { todos },
state: { todos },
}B4.run's LangChain adapter turns that into a state update, so the next model turn sees the new list.
State and promptsCopy link to section: State and prompts
Planning adds a todos state field with a replace reducer. Two names are taken:
- Don't declare your own
todosfield instate.ts. The route fails to load, with an error telling you to remove the field orplan.md. - Don't create a tool named
writeTodos. A tool of yours with the same name as a capability tool is a conflict.
B4.run rebuilds the planning prompt from the current state on each turn. 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 keeps the plan in front of the model between tool calls.
StreamingCopy link to section: Streaming
When runs/stream sees a writeTodos tool result, B4.run emits:
{
"type": "plan_update",
"data": {
"todos": [
{ "content": "Check account context", "status": "in_progress" }
],
"tool_call_id": "call_writeTodos_0_1"
}
}tool_call_id is the model's id for the writeTodos call behind the update (the same id that call's tool events carry). It's left out when the model didn't supply one.
When a subagent sends a planning event, the parent stream forwards it with a subagent. prefix, 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 typesCopy link to section: Generated types
When a route directory has a plan.md, b4 typegen adds writeTodos to that route's generated tool types.
B4.run only runs planning in agent() routes, though. The todos field exists at runtime only, and the generated route state types still come from state.ts.
Under the hoodCopy link to section: Under the hood
Planning is implemented by createPlanningMarker().
When B4.run prepares an agent route, it finds plan.md and reads the seed todos. Then it adds the writeTodos tool, the todos state field, and a stream transformer that watches tool results.
The transformer handles both plain { todos } outputs and LangGraph Command outputs that keep the update in update.todos.