Skip to content

Planning with todos

Give your agent a multi-step job and it won't just dive in — it writes a plan first, then ticks items off as it goes. That plan is a real, inspectable todo list, and you didn't have to build it.

Python
import asyncio

from pydantic_deep import create_deep_agent, DeepAgentDeps, StateBackend


async def main():
    agent = create_deep_agent(
        model="anthropic:claude-sonnet-4-6",
        instructions="You are a helpful coding assistant.",
    )

    deps = DeepAgentDeps(backend=StateBackend())

    result = await agent.run(
        "Build a small CLI calculator: write calc.py with add/sub/mul/div, "
        "add a tests file, run the tests, and fix anything that fails.",
        deps=deps,
    )
    print(result.output)

    # The plan the agent built and worked through:
    for todo in deps.todos:
        print(f"[{todo.status}] {todo.content}")


asyncio.run(main())

Run it

Save it to main.py and run:

Bash Session
$ python main.py

A task with four-plus distinct steps trips the agent's planning instinct. It calls write_todos to lay out the work, marks one item in_progress, does it, marks it completed, and moves to the next — so the final loop prints something like:

Text Only
[completed] Write calc.py with add/sub/mul/div
[completed] Write tests for the calculator
[completed] Run the test suite
[completed] Fix failing tests

Check it

Add a simpler prompt — "What's 2 + 2?" — and print deps.todos after. It'll be empty. The agent only reaches for a plan when the work actually has steps; trivial asks stay todo-free.

What's happening

You didn't register a todo tool — create_deep_agent() wired one in for you. Let's look at the two halves: the tools the model uses, and the list you read.

The tools the agent gets

Because include_todo=True is the default, every agent gets a small set of planning tools:

  • read_todos — view the current list with each item's ID and status.
  • write_todos — replace the whole list (how the agent lays out a fresh plan).
  • add_todo — append a single new task.
  • update_todo_status — flip one task to pending, in_progress, or completed by ID.
  • remove_todo — drop a task by ID.

The agent's instructions nudge it to use them for anything non-trivial: break the work down, keep exactly one task in_progress at a time, and mark things completed the moment they're done — not in a batch at the end.

Three states

Every todo is pending, in_progress, or completed. That's the whole state machine — enough to drive a focused agent without ceremony.

The list you read

Python
deps = DeepAgentDeps(backend=StateBackend())
# ... after the run ...
for todo in deps.todos:
    print(f"[{todo.status}] {todo.content}")

The todos don't vanish when the run ends — they live on deps.todos, a plain list of Todo objects you can read afterward. Each one has:

  • content — the task in imperative form ("Run the test suite").
  • statuspending / in_progress / completed.
  • active_form — the present-tense label ("Running the test suite"), handy for a live status line.
  • id — a short identifier the tools use to target a single task.

Because the list is right there on your deps object, the same plan the model worked through is yours to inspect, log, or render in a UI.

Watch the plan live

deps.todos updates during the run, not just after it. Read it from a background task or a streaming handler to drive a progress display — see Streaming for the event-by-event view.

Sharing todos with subagents

When your agent delegates to a subagent, the subagent gets its own, empty todo list by default. That's deliberate: a subagent planning its slice of the work shouldn't scribble over the parent's plan.

If you do want one shared plan across the parent and its subagents, flip share_todos on the deps:

Python
deps = DeepAgentDeps(
    backend=StateBackend(),
    share_todos=True,  # subagents read and write the same todo list
)

Now a subagent's write_todos and update_todo_status land on the same list the parent sees — useful when you want a single, unified view of progress across a whole delegation tree.

Shared means shared

With share_todos=True, several agents can write to the same list concurrently. It's the right call for a single coordinated plan, but if you want each subagent to own its work independently, leave it False (the default).

Recap

Your agent now plans its own work and you can see the plan:

  • create_deep_agent() includes the todo tools by default (include_todo=True) — read_todos, write_todos, add_todo, update_todo_status, remove_todo.
  • The agent breaks multi-step tasks into todos, keeps one in_progress, and marks each completed as it finishes.
  • The plan lives on deps.todos — a list of Todo objects (content, status, active_form, id) you can read during or after the run.
  • The system prompt carries the todo workflow, not the live list. Instructions open the provider's prompt-cache prefix, so re-rendering them on every status change would invalidate the whole cached prefix mid-run — the list is already in the message history, and read_todos fetches it on demand. Pass include_current_todos=True if you want it in the prompt anyway.
  • Subagents get isolated todos by default; set share_todos=True for one shared plan across the delegation tree.

Next, let's hand the agent tools of your own.