### Model availability
The whole hunk
from line 5, old and new numbered
/
lines
from line 5
55The Claude Agent SDK includes built-in todo functionality that helps organize complex workflows and keep users informed about task progression.
66
77<Note>
8 As of TypeScript Agent SDK 0.3.142 and Claude Code v2.1.142, sessions use the structured Task tools `TaskCreate`, `TaskUpdate`, `TaskGet`, and `TaskList` instead of `TodoWrite`. The Python SDK gets this change from the Claude Code CLI it launches, not from the Python package version: the switch applies once that CLI — the copy bundled inside the pip package, or one you point to with `cli_path` — is v2.1.142 or later. See [Migrate to Task tools](#migrate-to-task-tools) for how monitoring code changes. The examples on this page set `CLAUDE_CODE_ENABLE_TASKS=0` to keep showing `TodoWrite` for sessions that have not migrated yet.
8 On TypeScript Agent SDK 0.3.233 and later, or Python Agent SDK 0.2.139 and later, the following tools aren't available on Opus 4.8, Sonnet 5, Fable 5, Mythos 5, or later versions of those families unless you opt in:
9
10 * `TodoWrite`
11 * `TaskCreate`
12 * `TaskGet`
13 * `TaskUpdate`
14 * `TaskList`
15
16 On other models, Claude Code provides the Task tools by default and `TodoWrite` only when you set `CLAUDE_CODE_ENABLE_TASKS=0`.
917</Note>
1018
19### Model availability
20
21On the [models that don't get the task-tracking tools](/docs/en/tools-reference#task-tool-availability), you see no `tool_use` blocks for them in the message stream unless you opt in. If you point `cli_path` in Python or `pathToClaudeCodeExecutable` in TypeScript at your own Claude Code install, you get whichever tools that install provides. To get the same tools as on other models, do one of the following:
22
23* Name one of the tools in the [`allowedTools`](/docs/en/agent-sdk/permissions#allow-and-deny-rules) option, `allowed_tools` in Python
24* List the tools in the `tools` option, which restricts the session's built-in tools to the ones it names. Include the tools you want alongside the other built-in tools you use
25* Set `CLAUDE_CODE_ENABLE_TODO_TOOLS=1` in the `env` option, as the examples on this page do. In TypeScript, `env` replaces the subprocess environment, so spread `...process.env` to keep inherited variables. In Python, `env` is merged on top of the inherited environment
26
1127### Todo Lifecycle
1228
1329Claude moves each todo through a predictable lifecycle:
from line 64
4864 for await (const message of query({
4965 prompt: "Optimize my React app performance and track progress with todos",
5066 // Re-enable TodoWrite, which this example monitors. Without it, the SDK uses
51 // Task tools instead and these tool_use blocks never appear.
52 options: { maxTurns: 15, env: { ...process.env, CLAUDE_CODE_ENABLE_TASKS: "0" } }
67 // Task tools instead and these tool_use blocks never appear. ENABLE_TODO_TOOLS
68 // keeps the tools on models where Claude Code otherwise doesn't provide them.
69 options: { maxTurns: 15, env: { ...process.env, CLAUDE_CODE_ENABLE_TASKS: "0", CLAUDE_CODE_ENABLE_TODO_TOOLS: "1" } }
5370 })) {
5471 // Todo updates are reflected in the message stream
5572 if (message.type === "assistant") {
from line 102
85102 async for message in query(
86103 prompt="Optimize my React app performance and track progress with todos",
87104 # Re-enable TodoWrite, which this example monitors. Without it, the SDK uses
88 # Task tools instead and these tool_use blocks never appear.
89 options=ClaudeAgentOptions(max_turns=15, env={"CLAUDE_CODE_ENABLE_TASKS": "0"}),
105 # Task tools instead and these tool_use blocks never appear. ENABLE_TODO_TOOLS
106 # keeps the tools on models where Claude Code otherwise doesn't provide them.
107 options=ClaudeAgentOptions(max_turns=15, env={"CLAUDE_CODE_ENABLE_TASKS": "0", "CLAUDE_CODE_ENABLE_TODO_TOOLS": "1"}),
90108 ):
91109 # Todo updates are reflected in the message stream
92110 if isinstance(message, AssistantMessage):
from line 163
145163 try {
146164 for await (const message of query({
147165 prompt,
148 // Re-enable TodoWrite, which this tracker watches for.
149 options: { maxTurns: 20, env: { ...process.env, CLAUDE_CODE_ENABLE_TASKS: "0" } }
166 // On every model, re-enable TodoWrite, which this tracker watches for.
167 options: { maxTurns: 20, env: { ...process.env, CLAUDE_CODE_ENABLE_TASKS: "0", CLAUDE_CODE_ENABLE_TODO_TOOLS: "1" } }
150168 })) {
151169 if (message.type === "assistant") {
152170 for (const block of message.message.content) {
from line 229
211229 try:
212230 async for message in query(
213231 prompt=prompt,
214 # Re-enable TodoWrite, which this tracker watches for.
215 options=ClaudeAgentOptions(max_turns=20, env={"CLAUDE_CODE_ENABLE_TASKS": "0"}),
232 # On every model, re-enable TodoWrite, which this tracker watches for.
233 options=ClaudeAgentOptions(max_turns=20, env={"CLAUDE_CODE_ENABLE_TASKS": "0", "CLAUDE_CODE_ENABLE_TODO_TOOLS": "1"}),
216234 ):
217235 if isinstance(message, AssistantMessage):
218236 for block in message.content:
from line 255
237255
238256## Migrate to Task tools
239257
240The Task tools split the single `TodoWrite` call into `TaskCreate` for each new item and `TaskUpdate` for each status change, with `TaskList` and `TaskGet` available for the model to read back the current list. Your monitoring code still inspects `tool_use` blocks in the assistant stream, but maintains a map keyed by task ID instead of replacing the whole list on every call. The Task tools are the default as of TypeScript Agent SDK 0.3.142 and Claude Code v2.1.142, so no `options.env` change is needed.
258The Task tools split the single `TodoWrite` call into `TaskCreate` for each new item and `TaskUpdate` for each status change, with `TaskList` and `TaskGet` available for the model to read back the current list. Your monitoring code still inspects `tool_use` blocks in the assistant stream, but maintains a map keyed by task ID instead of replacing the whole list on every call.
241259
242260| With `TodoWrite` | With Task tools |
243261| --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
from line 264
246264| Item shape: `{ content, status, activeForm }` | `TaskCreate` input: `{ subject, description, activeForm?, metadata? }`. `TaskUpdate` input: `{ taskId, status?, subject?, description?, activeForm?, addBlocks?, addBlockedBy?, owner?, metadata? }`. `status` is `"pending"`, `"in_progress"`, or `"completed"`; set `status: "deleted"` to delete |
247265| Render `block.input.todos` directly | Accumulate items across calls, or read a snapshot from a `TaskList` tool result |
248266
249The assigned task ID is not in the `TaskCreate` input. It comes back in the matching `tool_result` as `{ task: { id, subject } }`, so capture it from the result block to key your map. The following example shows the minimal change to the [Monitoring Todo Changes](#monitoring-todo-changes) loop. It reads only `tool_use` inputs and skips capturing IDs from `tool_result` blocks. To render a complete list, watch for a `TaskList` tool result in the stream or accumulate `TaskCreate` results and `TaskUpdate` inputs into a map.
267The assigned task ID is not in the `TaskCreate` input. It comes back in the matching `tool_result` as `{ task: { id, subject } }`, so capture it from the result block to key your map.
250268
269The following example shows the minimal change to the [Monitoring Todo Changes](#monitoring-todo-changes) loop. It leaves `CLAUDE_CODE_ENABLE_TASKS` unset, because the Task tools are the default, and sets only `CLAUDE_CODE_ENABLE_TODO_TOOLS=1`, the [opt-in](#model-availability) for the models that otherwise don't get the tools. It reads only `tool_use` inputs and skips capturing IDs from `tool_result` blocks. To render a complete list, watch for a `TaskList` tool result in the stream or accumulate `TaskCreate` results and `TaskUpdate` inputs into a map.
270
251271The streamed `tool_use` input is the raw shape the model emitted. Claude Code repairs some close-but-incorrect key names before execution, mapping `id` or `task_id` to `taskId` and `active_form` to `activeForm`, but that repair is not reflected in the stream. Read `TaskUpdate` input fields defensively, as the samples below do, rather than assuming the canonical name is always present.
252272
253273<CodeGroup>
from line 277
257277 try {
258278 for await (const message of query({
259279 prompt: "Optimize my React app performance and track progress with todos",
260 options: { maxTurns: 15 },
280 // Keeps the Task tools on models where Claude Code otherwise doesn't provide them.
281 options: { maxTurns: 15, env: { ...process.env, CLAUDE_CODE_ENABLE_TODO_TOOLS: "1" } },
261282 })) {
262283 if (message.type !== "assistant") continue;
263284 for (const block of message.message.content) {
from line 313
292313 try:
293314 async for message in query(
294315 prompt="Optimize my React app performance and track progress with todos",
295 options=ClaudeAgentOptions(max_turns=15),
316 # Keeps the Task tools on models where Claude Code otherwise doesn't provide them.
317 options=ClaudeAgentOptions(max_turns=15, env={"CLAUDE_CODE_ENABLE_TODO_TOOLS": "1"}),
296318 ):
297319 if not isinstance(message, AssistantMessage):
298320 continue