2> Fetch the complete documentation index at: https://code.claude.com/docs/llms.txt2> Fetch the complete documentation index at: https://code.claude.com/docs/llms.txt
3> Use this file to discover all available pages before exploring further.3> Use this file to discover all available pages before exploring further.
4 4
5# Todo Lists5# Track todos
6 6
7> Track and display todos using the Claude Agent SDK for organized task management7> Track todos in Agent SDK sessions and render Claude's progress in your application from structured tool calls
8 8
9The Claude Agent SDK includes built-in todo functionality that helps organize complex workflows and keep users informed about task progression.9On the models listed under [Model availability](#model-availability), Claude tracks multi-step work without a written todo list, and Claude Code leaves the [task-tracking tools](/docs/en/tools-reference#task-tool-availability) out of sessions by default. You don't need anything on this page for Claude to work through multi-step tasks on those models.
10
11In a session that has the task-tracking tools, Claude keeps a written todo list, updating each item's status as it works. You see each change in the message stream as a structured tool call. Opt a session in only when your application reads those tool calls, whether to log task activity or to render its own progress display.
12
13## Model availability
10 14
11<Note>15<Note>
12 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:16 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:
20 On other models, Claude Code provides the Task tools by default and `TodoWrite` only when you set `CLAUDE_CODE_ENABLE_TASKS=0`.24 On other models, Claude Code provides the Task tools by default and `TodoWrite` only when you set `CLAUDE_CODE_ENABLE_TASKS=0`.
21</Note>25</Note>
22 26
23### Model availability27On the listed models, unless you opt a session in, you see no `tool_use` blocks for the tools in the message stream. The Agent SDK applies these defaults through the Claude Code binary that it bundles. If you point `pathToClaudeCodeExecutable` (TypeScript) or `cli_path` (Python) at your own Claude Code install, you get whichever tools that install provides, under its own defaults. To see the exact set in a running session, [check which tools are available](/docs/en/tools-reference#check-which-tools-are-available). To opt a session in, do one of the following:
24
25On 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:
26 28
27* Name one of the tools in the [`allowedTools`](/docs/en/agent-sdk/permissions#allow-and-deny-rules) option, `allowed_tools` in Python29* Name one of the tools in the [`allowedTools`](/docs/en/agent-sdk/permissions#allow-and-deny-rules) (TypeScript) or `allowed_tools` (Python) option
28* 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 use30* 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
29* 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 environment31* 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
30 32
31### Todo Lifecycle33## Todo lifecycle
32 34
33Claude moves each todo through a predictable lifecycle:35Claude moves each todo through a predictable lifecycle:
34 36
373. **Completed**: Claude marks it completed when the task finishes successfully393. **Completed**: Claude marks it completed when the task finishes successfully
384. **Removed**: Claude deletes a todo it no longer needs by setting `status: "deleted"` in a `TaskUpdate` call404. **Removed**: Claude deletes a todo it no longer needs by setting `status: "deleted"` in a `TaskUpdate` call
39 41
40### When Todos Are Used42## When Claude creates todos
41 43
42In a [session that has the task-tracking tools](#model-availability), Claude creates todos for most multi-step work, such as:44In a [session that has the task-tracking tools](#model-availability), Claude creates todos for most multi-step work, such as:
43 45
44* **Complex multi-step tasks** requiring 3 or more distinct actions46* **Complex multi-step tasks** requiring three or more distinct actions
45* **User-provided task lists** when multiple items are mentioned47* **User-provided task lists** when multiple items are mentioned
46* **Non-trivial operations** that benefit from progress tracking48* **Longer operations** that benefit from progress tracking
47* **Explicit requests** when users ask for todo organization49* **Explicit requests** when users ask for todo organization
48 50
49It may skip todos for very short or single-step requests.51Claude may skip todos for very short or single-step requests.
50 52
51## Examples53## Examples
52 54
53Before running these examples, install the Claude Agent SDK by following the [quickstart](/docs/en/agent-sdk/quickstart).55Before running these examples, install the Claude Agent SDK by following the [quickstart](/docs/en/agent-sdk/quickstart). Every example on this page shares the same permission setup and exit behavior:
54 56
55Each example runs until the agent finishes and yields its final result message. If a session reaches its turn limit first, that result message has the `error_max_turns` subtype. Check `subtype` to detect that ending.57* **Permission mode**: the example prompts ask Claude to do real work on a project, so each example sets `permissionMode: "acceptEdits"` (TypeScript) or `permission_mode="acceptEdits"` (Python) to auto-approve the file edits that work produces. See [Permission modes](/docs/en/agent-sdk/permissions#permission-modes) for the alternatives.
58* **Turn limit**: each example runs until the agent finishes and yields its final result message. If a session reaches its turn limit first, that result message has the `error_max_turns` subtype. Check `subtype` to detect that ending.
59* **Error handling**: these examples use single-shot `query()` calls. After yielding an `error_max_turns` result, `query()` raises an error that includes `Reached maximum number of turns`. Each example wraps its loop in a try block to exit cleanly when that happens. See [Handle the result](/docs/en/agent-sdk/agent-loop#handle-the-result) for the result subtypes.
60
61<Note>
62 The task system messages, [`SDKTaskNotificationMessage`](/docs/en/agent-sdk/typescript#sdktasknotificationmessage) (TypeScript) or [`TaskNotificationMessage`](/docs/en/agent-sdk/python#tasknotificationmessage) (Python) among them, report background tasks such as backgrounded commands and subagents. In the message stream, you see todo activity as `tool_use` blocks in the assistant messages.
63</Note>
56 64
57These examples use single-shot `query()` calls. After yielding an `error_max_turns` result, `query()` raises an error that includes `Reached maximum number of turns`. Each example wraps its loop in a try block to exit cleanly when that happens.65### Monitor todo changes
58 66
59See [Handle the result](/docs/en/agent-sdk/agent-loop#handle-the-result) for the result subtypes.67The following example watches the assistant stream for `TaskCreate` and `TaskUpdate` `tool_use` blocks and prints a `+` line with each new task's subject and an update line with each status change's task ID and new status. Use this shape when you want a log of task activity rather than a rendered display. The `+` lines don't include the assigned IDs, so this log can't match updates back to their creates. To keep that correlation, capture the IDs as [Display progress in real time](#display-progress-in-real-time) does.
60 68
61### Monitoring Todo Changes69The 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 both examples on this page do, rather than assuming the canonical name is always present.
62 70
63<CodeGroup>71<CodeGroup>
64 ```typescript TypeScript theme={null}72 ```typescript TypeScript theme={null}
67 try {75 try {
68 for await (const message of query({76 for await (const message of query({
69 prompt: "Optimize my React app performance and track progress with todos",77 prompt: "Optimize my React app performance and track progress with todos",
70 // Re-enable TodoWrite, which this example monitors. Without it, the SDK uses78 // Keeps the Task tools on models where Claude Code otherwise doesn't provide them.
71 // Task tools instead and these tool_use blocks never appear. ENABLE_TODO_TOOLS79 options: { maxTurns: 15, permissionMode: "acceptEdits", env: { ...process.env, CLAUDE_CODE_ENABLE_TODO_TOOLS: "1" } },
72 // keeps the tools on models where Claude Code otherwise doesn't provide them.
73 options: { maxTurns: 15, env: { ...process.env, CLAUDE_CODE_ENABLE_TASKS: "0", CLAUDE_CODE_ENABLE_TODO_TOOLS: "1" } }
74 })) {80 })) {
75 // Todo updates are reflected in the message stream81 if (message.type !== "assistant") continue;
76 if (message.type === "assistant") {
77 for (const block of message.message.content) {82 for (const block of message.message.content) {
78 if (block.type === "tool_use" && block.name === "TodoWrite") {83 if (block.type !== "tool_use") continue;
79 const todos = block.input.todos;84 if (block.name === "TaskCreate") {
80 85 const input = block.input as { subject: string };
81 console.log("Todo Status Update:");86 console.log(`+ ${input.subject}`);
82 todos.forEach((todo, index) => {87 } else if (block.name === "TaskUpdate") {
83 const status =88 const input = block.input as {
84 todo.status === "completed" ? "✅" : todo.status === "in_progress" ? "🔧" : "❌";89 taskId?: string;
85 console.log(`${index + 1}. ${status} ${todo.content}`);90 id?: string;
86 });91 task_id?: string;
87 }92 status?: string;
93 };
94 const taskId = input.taskId ?? input.id ?? input.task_id;
95 if (taskId && input.status) console.log(` ${taskId} -> ${input.status}`);
88 }96 }
89 }97 }
90 }98 }
91 } catch (error) {99 } catch (error) {
92 // A single-shot query() throws after yielding an error result,100 // A single-shot query() throws after yielding an error result.
93 // such as when the maxTurns limit is hit.
94 console.log(`Session ended with an error: ${error}`);101 console.log(`Session ended with an error: ${error}`);
95 }102 }
96 ```103 ```
105 try:111 try:
106 async for message in query(112 async for message in query(
107 prompt="Optimize my React app performance and track progress with todos",113 prompt="Optimize my React app performance and track progress with todos",
108 # Re-enable TodoWrite, which this example monitors. Without it, the SDK uses114 # Keeps the Task tools on models where Claude Code otherwise doesn't provide them.
109 # Task tools instead and these tool_use blocks never appear. ENABLE_TODO_TOOLS115 options=ClaudeAgentOptions(max_turns=15, permission_mode="acceptEdits", env={"CLAUDE_CODE_ENABLE_TODO_TOOLS": "1"}),
110 # keeps the tools on models where Claude Code otherwise doesn't provide them.
111 options=ClaudeAgentOptions(max_turns=15, env={"CLAUDE_CODE_ENABLE_TASKS": "0", "CLAUDE_CODE_ENABLE_TODO_TOOLS": "1"}),
112 ):116 ):
113 # Todo updates are reflected in the message stream117 if not isinstance(message, AssistantMessage):
114 if isinstance(message, AssistantMessage):118 continue
115 for block in message.content:119 for block in message.content:
116 if isinstance(block, ToolUseBlock) and block.name == "TodoWrite":120 if not isinstance(block, ToolUseBlock):
117 todos = block.input["todos"]121 continue
118 122 if block.name == "TaskCreate":
119 print("Todo Status Update:")123 print(f"+ {block.input.get('subject', '')}")
120 for i, todo in enumerate(todos):124 elif block.name == "TaskUpdate" and block.input.get("status"):
121 status = (125 task_id = (
122 "✅"126 block.input.get("taskId")
123 if todo["status"] == "completed"127 or block.input.get("id")
124 else "🔧"128 or block.input.get("task_id")
125 if todo["status"] == "in_progress"
126 else "❌"
127 )129 )
128 print(f"{i + 1}. {status} {todo['content']}")130 if task_id:
131 print(f" {task_id} -> {block.input['status']}")
129 except Exception as error:132 except Exception as error:
130 # A single-shot query() raises after yielding an error result,133 # A single-shot query() raises after yielding an error result.
131 # such as when the max_turns limit is hit.
132 print(f"Session ended with an error: {error}")134 print(f"Session ended with an error: {error}")
133 135
134 136
136 ```138 ```
137</CodeGroup>139</CodeGroup>
138 140
139### Real-time Progress Display141### Display progress in real time
142
143The following example watches the assistant stream for `TaskCreate` and `TaskUpdate` `tool_use` blocks and keeps a map of tasks keyed by task ID in a `TaskTracker` class, rerendering a progress summary on every change. The summary counts completed and in-progress tasks and shows each active item's `activeForm` label in place of its `subject`. Use this shape when your application maintains a progress display instead of logging each event.
144
145The assigned task ID isn't in the `TaskCreate` input. Claude Code delivers each tool's structured output on the user message that carries its `tool_result` block, in the `tool_use_result` field. For `TaskCreate`, that object is documented for TypeScript as `TaskCreateOutput` under [Tool Output Types](/docs/en/agent-sdk/typescript#tool-output-types), and in Python the field is a plain dict of the same shape. The tracker pairs each `tool_result` block with its `tool_use` call by `tool_use_id` and reads `task.id` from the paired message's `tool_use_result`. Claude can read the list back with `TaskList` and one task's full details with `TaskGet`.
140 146
141<CodeGroup>147<CodeGroup>
142 ```typescript TypeScript theme={null}148 ```typescript TypeScript theme={null}
143 import { query } from "@anthropic-ai/claude-agent-sdk";149 import { query } from "@anthropic-ai/claude-agent-sdk";
144 150
145 class TodoTracker {151 type Task = { subject: string; activeForm?: string; status: string };
146 private todos: any[] = [];152
153 class TaskTracker {
154 private tasks = new Map<string, Task>();
155 private pendingCreates = new Map<string, { subject: string; activeForm?: string }>();
147 156
148 displayProgress() {157 displayProgress() {
149 if (this.todos.length === 0) return;158 if (this.tasks.size === 0) {
159 console.log("\nProgress: no open tasks\n");
160 return;
161 }
150 162
151 const completed = this.todos.filter((t) => t.status === "completed").length;163 const items = [...this.tasks.values()];
152 const inProgress = this.todos.filter((t) => t.status === "in_progress").length;164 const completed = items.filter((t) => t.status === "completed").length;
153 const total = this.todos.length;165 const inProgress = items.filter((t) => t.status === "in_progress").length;
154 166
155 console.log(`\nProgress: ${completed}/${total} completed`);167 console.log(`\nProgress: ${completed}/${this.tasks.size} completed`);
156 console.log(`Currently working on: ${inProgress} task(s)\n`);168 console.log(`Currently working on: ${inProgress} task(s)\n`);
157 169
158 this.todos.forEach((todo, index) => {170 for (const [id, task] of this.tasks) {
159 const icon =171 const icon =
160 todo.status === "completed" ? "✅" : todo.status === "in_progress" ? "🔧" : "❌";172 task.status === "completed" ? "✅" : task.status === "in_progress" ? "🔧" : "❌";
161 const text = todo.status === "in_progress" ? todo.activeForm : todo.content;173 const text = task.status === "in_progress" && task.activeForm ? task.activeForm : task.subject;
162 console.log(`${index + 1}. ${icon} ${text}`);174 console.log(`${id}. ${icon} ${text}`);
175 }
176 }
177
178 handleToolUse(block: { id: string; name: string; input: unknown }) {
179 if (block.name === "TaskCreate") {
180 const input = block.input as { subject: string; activeForm?: string; active_form?: string };
181 this.pendingCreates.set(block.id, {
182 subject: input.subject,
183 activeForm: input.activeForm ?? input.active_form,
163 });184 });
185 } else if (block.name === "TaskUpdate") {
186 const input = block.input as {
187 taskId?: string;
188 id?: string;
189 task_id?: string;
190 status?: string;
191 activeForm?: string;
192 active_form?: string;
193 };
194 const taskId = input.taskId ?? input.id ?? input.task_id;
195 if (!taskId) return;
196 if (input.status === "deleted") {
197 this.tasks.delete(taskId);
198 this.displayProgress();
199 return;
200 }
201 const task = this.tasks.get(taskId);
202 if (!task) return;
203 if (input.status) task.status = input.status;
204 const active = input.activeForm ?? input.active_form;
205 if (active) task.activeForm = active;
206 this.displayProgress();
207 }
208 }
209
210 handleToolResult(block: { tool_use_id: string; is_error?: boolean }, result: unknown) {
211 const create = this.pendingCreates.get(block.tool_use_id);
212 if (!create) return;
213 this.pendingCreates.delete(block.tool_use_id);
214 if (block.is_error) return;
215 // The result's user message carries the tool's structured output as
216 // tool_use_result; for TaskCreate that's TaskCreateOutput,
217 // { task: { id, subject } }.
218 const out = result as { task?: { id: string } };
219 if (!out?.task?.id) return;
220 this.tasks.set(out.task.id, { ...create, status: "pending" });
221 this.displayProgress();
164 }222 }
165 223
166 async trackQuery(prompt: string) {224 async trackQuery(prompt: string) {
167 try {225 try {
168 for await (const message of query({226 for await (const message of query({
169 prompt,227 prompt,
170 // On every model, re-enable TodoWrite, which this tracker watches for.228 options: { maxTurns: 20, permissionMode: "acceptEdits", env: { ...process.env, CLAUDE_CODE_ENABLE_TODO_TOOLS: "1" } },
171 options: { maxTurns: 20, env: { ...process.env, CLAUDE_CODE_ENABLE_TASKS: "0", CLAUDE_CODE_ENABLE_TODO_TOOLS: "1" } }
172 })) {229 })) {
173 if (message.type === "assistant") {230 if (message.type === "assistant") {
174 for (const block of message.message.content) {231 for (const block of message.message.content) {
175 if (block.type === "tool_use" && block.name === "TodoWrite") {232 if (block.type === "tool_use") this.handleToolUse(block);
176 this.todos = block.input.todos;
177 this.displayProgress();
178 }233 }
179 }234 }
235 if (message.type === "user" && Array.isArray(message.message.content)) {
236 for (const block of message.message.content) {
237 if (block.type === "tool_result") this.handleToolResult(block, message.tool_use_result);
238 }
180 }239 }
181 }240 }
182 } catch (error) {241 } catch (error) {
188 }247 }
189 248
190 // Usage249 // Usage
191 const tracker = new TodoTracker();250 const tracker = new TaskTracker();
192 await tracker.trackQuery("Build a complete authentication system with todos");251 await tracker.trackQuery("Build a complete authentication system with todos");
193 ```252 ```
194 253
195 ```python Python theme={null}254 ```python Python theme={null}
196 import asyncio255 import asyncio
197 256
198 from claude_agent_sdk import query, ClaudeAgentOptions, AssistantMessage, ToolUseBlock257 from claude_agent_sdk import (
199 from typing import List, Dict258 query,
259 ClaudeAgentOptions,
260 AssistantMessage,
261 UserMessage,
262 ToolUseBlock,
263 ToolResultBlock,
264 )
200 265
201 266
202 class TodoTracker:267 class TaskTracker:
203 def __init__(self):268 def __init__(self):
204 self.todos: List[Dict] = []269 self.tasks: dict[str, dict] = {}
270 self.pending_creates: dict[str, dict] = {}
205 271
206 def display_progress(self):272 def display_progress(self):
207 if not self.todos:273 if not self.tasks:
274 print("\nProgress: no open tasks\n")
208 return275 return
209 276
210 completed = len([t for t in self.todos if t["status"] == "completed"])277 completed = len([t for t in self.tasks.values() if t["status"] == "completed"])
211 in_progress = len([t for t in self.todos if t["status"] == "in_progress"])278 in_progress = len([t for t in self.tasks.values() if t["status"] == "in_progress"])
212 total = len(self.todos)
213 279
214 print(f"\nProgress: {completed}/{total} completed")280 print(f"\nProgress: {completed}/{len(self.tasks)} completed")
215 print(f"Currently working on: {in_progress} task(s)\n")281 print(f"Currently working on: {in_progress} task(s)\n")
216 282
217 for i, todo in enumerate(self.todos):283 for task_id, task in self.tasks.items():
218 icon = (284 icon = (
219 "✅"285 "✅"
220 if todo["status"] == "completed"286 if task["status"] == "completed"
221 else "🔧"287 else "🔧"
222 if todo["status"] == "in_progress"288 if task["status"] == "in_progress"
223 else "❌"289 else "❌"
224 )290 )
225 text = (291 text = (
226 todo["activeForm"]292 task["activeForm"]
227 if todo["status"] == "in_progress"293 if task["status"] == "in_progress" and task.get("activeForm")
228 else todo["content"]294 else task["subject"]
229 )295 )
230 print(f"{i + 1}. {icon} {text}")296 print(f"{task_id}. {icon} {text}")
297
298 def handle_tool_use(self, block: ToolUseBlock):
299 if block.name == "TaskCreate":
300 self.pending_creates[block.id] = {
301 "subject": block.input.get("subject", ""),
302 "activeForm": block.input.get("activeForm") or block.input.get("active_form"),
303 }
304 elif block.name == "TaskUpdate":
305 task_id = (
306 block.input.get("taskId")
307 or block.input.get("id")
308 or block.input.get("task_id")
309 )
310 if not task_id:
311 return
312 if block.input.get("status") == "deleted":
313 self.tasks.pop(task_id, None)
314 self.display_progress()
315 return
316 task = self.tasks.get(task_id)
317 if not task:
318 return
319 if block.input.get("status"):
320 task["status"] = block.input["status"]
321 active = block.input.get("activeForm") or block.input.get("active_form")
322 if active:
323 task["activeForm"] = active
324 self.display_progress()
325
326 def handle_tool_result(self, block: ToolResultBlock, tool_use_result):
327 create = self.pending_creates.pop(block.tool_use_id, None)
328 if create is None or block.is_error:
329 return
330 # The result's user message carries the tool's structured output as
331 # tool_use_result; for TaskCreate that's {"task": {"id": ..., "subject": ...}}.
332 task = (tool_use_result or {}).get("task") or {}
333 if not task.get("id"):
334 return
335 self.tasks[task["id"]] = {**create, "status": "pending"}
336 self.display_progress()
231 337
232 async def track_query(self, prompt: str):338 async def track_query(self, prompt: str):
233 try:339 try:
234 async for message in query(340 async for message in query(
235 prompt=prompt,341 prompt=prompt,
236 # On every model, re-enable TodoWrite, which this tracker watches for.342 options=ClaudeAgentOptions(
237 options=ClaudeAgentOptions(max_turns=20, env={"CLAUDE_CODE_ENABLE_TASKS": "0", "CLAUDE_CODE_ENABLE_TODO_TOOLS": "1"}),343 max_turns=20,
344 permission_mode="acceptEdits",
345 env={"CLAUDE_CODE_ENABLE_TODO_TOOLS": "1"},
346 ),
238 ):347 ):
239 if isinstance(message, AssistantMessage):348 if isinstance(message, AssistantMessage):
240 for block in message.content:349 for block in message.content:
241 if isinstance(block, ToolUseBlock) and block.name == "TodoWrite":350 if isinstance(block, ToolUseBlock):
242 self.todos = block.input["todos"]351 self.handle_tool_use(block)
243 self.display_progress()352 if isinstance(message, UserMessage) and isinstance(message.content, list):
353 for block in message.content:
354 if isinstance(block, ToolResultBlock):
355 self.handle_tool_result(block, message.tool_use_result)
244 except Exception as error:356 except Exception as error:
245 # A single-shot query() raises after yielding an error result,357 # A single-shot query() raises after yielding an error result,
246 # such as when the max_turns limit is hit.358 # such as when the max_turns limit is hit.
249 361
250 # Usage362 # Usage
251 async def main():363 async def main():
252 tracker = TodoTracker()364 tracker = TaskTracker()
253 await tracker.track_query("Build a complete authentication system with todos")365 await tracker.track_query("Build a complete authentication system with todos")
254 366
255 367
257 ```369 ```
258</CodeGroup>370</CodeGroup>
259 371
260## Migrate to Task tools372## Related documentation
261
262The 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.
263
264| With `TodoWrite` | With Task tools |
265| --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
266| One tool call rewrites the full `todos` array | `TaskCreate` adds one item, `TaskUpdate` patches one item by `taskId` |
267| Match `block.name === "TodoWrite"` | Match `block.name === "TaskCreate"` or `"TaskUpdate"` |
268| 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 |
269| Render `block.input.todos` directly | Accumulate items across calls, or read a snapshot from a `TaskList` tool result |
270
271The 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.
272
273The 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.
274
275The 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.
276
277<CodeGroup>
278 ```typescript TypeScript theme={null}
279 import { query } from "@anthropic-ai/claude-agent-sdk";
280
281 try {
282 for await (const message of query({
283 prompt: "Optimize my React app performance and track progress with todos",
284 // Keeps the Task tools on models where Claude Code otherwise doesn't provide them.
285 options: { maxTurns: 15, env: { ...process.env, CLAUDE_CODE_ENABLE_TODO_TOOLS: "1" } },
286 })) {
287 if (message.type !== "assistant") continue;
288 for (const block of message.message.content) {
289 if (block.type !== "tool_use") continue;
290 if (block.name === "TaskCreate") {
291 const input = block.input as { subject: string };
292 console.log(`+ ${input.subject}`);
293 } else if (block.name === "TaskUpdate") {
294 const input = block.input as {
295 taskId?: string;
296 id?: string;
297 task_id?: string;
298 status?: string;
299 };
300 const taskId = input.taskId ?? input.id ?? input.task_id;
301 if (taskId && input.status) console.log(` ${taskId} -> ${input.status}`);
302 }
303 }
304 }
305 } catch (error) {
306 // A single-shot query() throws after yielding an error result.
307 console.log(`Session ended with an error: ${error}`);
308 }
309 ```
310
311 ```python Python theme={null}
312 import asyncio
313
314 from claude_agent_sdk import query, ClaudeAgentOptions, AssistantMessage, ToolUseBlock
315
316 async def main():
317 try:
318 async for message in query(
319 prompt="Optimize my React app performance and track progress with todos",
320 # Keeps the Task tools on models where Claude Code otherwise doesn't provide them.
321 options=ClaudeAgentOptions(max_turns=15, env={"CLAUDE_CODE_ENABLE_TODO_TOOLS": "1"}),
322 ):
323 if not isinstance(message, AssistantMessage):
324 continue
325 for block in message.content:
326 if not isinstance(block, ToolUseBlock):
327 continue
328 if block.name == "TaskCreate":
329 print(f"+ {block.input['subject']}")
330 elif block.name == "TaskUpdate" and block.input.get("status"):
331 task_id = (
332 block.input.get("taskId")
333 or block.input.get("id")
334 or block.input.get("task_id")
335 )
336 if task_id:
337 print(f" {task_id} -> {block.input['status']}")
338 except Exception as error:
339 # A single-shot query() raises after yielding an error result.
340 print(f"Session ended with an error: {error}")
341
342
343 asyncio.run(main())
344 ```
345</CodeGroup>
346
347## Related Documentation
348 373
349* [TypeScript SDK Reference](/docs/en/agent-sdk/typescript)374* [Agent SDK reference - TypeScript](/docs/en/agent-sdk/typescript): the options, types, and tool schemas for the TypeScript SDK, including the Task tool input and output types
350* [Python SDK Reference](/docs/en/agent-sdk/python)375* [Agent SDK reference - Python](/docs/en/agent-sdk/python): the options, types, and tool documentation for the Python SDK
351* [Streaming vs Single Mode](/docs/en/agent-sdk/streaming-vs-single-mode)376* [Streaming Input](/docs/en/agent-sdk/streaming-vs-single-mode): the two input modes, and when to use streaming input instead of the single-shot calls these examples use
352* [Custom Tools](/docs/en/agent-sdk/custom-tools)377* [Give Claude custom tools](/docs/en/agent-sdk/custom-tools): define your own tools with the SDK's in-process MCP server