The two sides of this change are more than 400 edits apart, too far apart to line up, so this is the differ's own diff of it and the words inside a line are not marked.
from line 1
1# Todo Lists
2
3> Track and display todos using the Claude Agent SDK for organized task management
4
5The Claude Agent SDK includes built-in todo functionality that helps organize complex workflows and keep users informed about task progression.
1# Track todos
2
3> Track todos in Agent SDK sessions and render Claude's progress in your application from structured tool calls
4
5On 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.
6
7In 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.
8
9## Model availability
610
711<Note>
812 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:
from line 20
1620 On other models, Claude Code provides the Task tools by default and `TodoWrite` only when you set `CLAUDE_CODE_ENABLE_TASKS=0`.
1721</Note>
1822
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
23On 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
25* Name one of the tools in the [`allowedTools`](/docs/en/agent-sdk/permissions#allow-and-deny-rules) (TypeScript) or `allowed_tools` (Python) option
2426* 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
2527* 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
2628
27### Todo Lifecycle
29## Todo lifecycle
2830
2931Claude moves each todo through a predictable lifecycle:
3032
from line 35
33353. **Completed**: Claude marks it completed when the task finishes successfully
34364. **Removed**: Claude deletes a todo it no longer needs by setting `status: "deleted"` in a `TaskUpdate` call
3537
36### When Todos Are Used
38## When Claude creates todos
3739
3840In a [session that has the task-tracking tools](#model-availability), Claude creates todos for most multi-step work, such as:
3941
40* **Complex multi-step tasks** requiring 3 or more distinct actions
42* **Complex multi-step tasks** requiring three or more distinct actions
4143* **User-provided task lists** when multiple items are mentioned
42* **Non-trivial operations** that benefit from progress tracking
44* **Longer operations** that benefit from progress tracking
4345* **Explicit requests** when users ask for todo organization
4446
45It may skip todos for very short or single-step requests.
47Claude may skip todos for very short or single-step requests.
4648
4749## Examples
4850
49Before running these examples, install the Claude Agent SDK by following the [quickstart](/docs/en/agent-sdk/quickstart).
50
51Each 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.
52
53These 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.
54
55See [Handle the result](/docs/en/agent-sdk/agent-loop#handle-the-result) for the result subtypes.
56
57### Monitoring Todo Changes
58
59<CodeGroup>
60 ```typescript TypeScript theme={null}
61 import { query } from "@anthropic-ai/claude-agent-sdk";
62
63 try {
64 for await (const message of query({
65 prompt: "Optimize my React app performance and track progress with todos",
66 // Re-enable TodoWrite, which this example monitors. Without it, the SDK uses
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" } }
70 })) {
71 // Todo updates are reflected in the message stream
72 if (message.type === "assistant") {
73 for (const block of message.message.content) {
74 if (block.type === "tool_use" && block.name === "TodoWrite") {
75 const todos = block.input.todos;
76
77 console.log("Todo Status Update:");
78 todos.forEach((todo, index) => {
79 const status =
80 todo.status === "completed" ? "✅" : todo.status === "in_progress" ? "🔧" : "❌";
81 console.log(`${index + 1}. ${status} ${todo.content}`);
82 });
83 }
84 }
85 }
86 }
87 } catch (error) {
88 // A single-shot query() throws after yielding an error result,
89 // such as when the maxTurns limit is hit.
90 console.log(`Session ended with an error: ${error}`);
91 }
92 ```
93
94 ```python Python theme={null}
95 import asyncio
96
97 from claude_agent_sdk import query, ClaudeAgentOptions, AssistantMessage, ToolUseBlock
98
99
100 async def main():
101 try:
102 async for message in query(
103 prompt="Optimize my React app performance and track progress with todos",
104 # Re-enable TodoWrite, which this example monitors. Without it, the SDK uses
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"}),
108 ):
109 # Todo updates are reflected in the message stream
110 if isinstance(message, AssistantMessage):
111 for block in message.content:
112 if isinstance(block, ToolUseBlock) and block.name == "TodoWrite":
113 todos = block.input["todos"]
114
115 print("Todo Status Update:")
116 for i, todo in enumerate(todos):
117 status = (
118 "✅"
119 if todo["status"] == "completed"
120 else "🔧"
121 if todo["status"] == "in_progress"
122 else "❌"
123 )
124 print(f"{i + 1}. {status} {todo['content']}")
125 except Exception as error:
126 # A single-shot query() raises after yielding an error result,
127 # such as when the max_turns limit is hit.
128 print(f"Session ended with an error: {error}")
129
130
131 asyncio.run(main())
132 ```
133</CodeGroup>
134
135### Real-time Progress Display
136
137<CodeGroup>
138 ```typescript TypeScript theme={null}
139 import { query } from "@anthropic-ai/claude-agent-sdk";
140
141 class TodoTracker {
142 private todos: any[] = [];
143
144 displayProgress() {
145 if (this.todos.length === 0) return;
146
147 const completed = this.todos.filter((t) => t.status === "completed").length;
148 const inProgress = this.todos.filter((t) => t.status === "in_progress").length;
149 const total = this.todos.length;
150
151 console.log(`\nProgress: ${completed}/${total} completed`);
152 console.log(`Currently working on: ${inProgress} task(s)\n`);
153
154 this.todos.forEach((todo, index) => {
155 const icon =
156 todo.status === "completed" ? "✅" : todo.status === "in_progress" ? "🔧" : "❌";
157 const text = todo.status === "in_progress" ? todo.activeForm : todo.content;
158 console.log(`${index + 1}. ${icon} ${text}`);
159 });
160 }
161
162 async trackQuery(prompt: string) {
163 try {
164 for await (const message of query({
165 prompt,
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" } }
168 })) {
169 if (message.type === "assistant") {
170 for (const block of message.message.content) {
171 if (block.type === "tool_use" && block.name === "TodoWrite") {
172 this.todos = block.input.todos;
173 this.displayProgress();
174 }
175 }
176 }
177 }
178 } catch (error) {
179 // A single-shot query() throws after yielding an error result,
180 // such as when the maxTurns limit is hit.
181 console.log(`Session ended with an error: ${error}`);
182 }
183 }
184 }
185
186 // Usage
187 const tracker = new TodoTracker();
188 await tracker.trackQuery("Build a complete authentication system with todos");
189 ```
190
191 ```python Python theme={null}
192 import asyncio
193
194 from claude_agent_sdk import query, ClaudeAgentOptions, AssistantMessage, ToolUseBlock
195 from typing import List, Dict
196
197
198 class TodoTracker:
199 def __init__(self):
200 self.todos: List[Dict] = []
201
202 def display_progress(self):
203 if not self.todos:
204 return
205
206 completed = len([t for t in self.todos if t["status"] == "completed"])
207 in_progress = len([t for t in self.todos if t["status"] == "in_progress"])
208 total = len(self.todos)
209
210 print(f"\nProgress: {completed}/{total} completed")
211 print(f"Currently working on: {in_progress} task(s)\n")
212
213 for i, todo in enumerate(self.todos):
214 icon = (
215 "✅"
216 if todo["status"] == "completed"
217 else "🔧"
218 if todo["status"] == "in_progress"
219 else "❌"
220 )
221 text = (
222 todo["activeForm"]
223 if todo["status"] == "in_progress"
224 else todo["content"]
225 )
226 print(f"{i + 1}. {icon} {text}")
227
228 async def track_query(self, prompt: str):
229 try:
230 async for message in query(
231 prompt=prompt,
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"}),
234 ):
235 if isinstance(message, AssistantMessage):
236 for block in message.content:
237 if isinstance(block, ToolUseBlock) and block.name == "TodoWrite":
238 self.todos = block.input["todos"]
239 self.display_progress()
240 except Exception as error:
241 # A single-shot query() raises after yielding an error result,
242 # such as when the max_turns limit is hit.
243 print(f"Session ended with an error: {error}")
244
245
246 # Usage
247 async def main():
248 tracker = TodoTracker()
249 await tracker.track_query("Build a complete authentication system with todos")
250
251
252 asyncio.run(main())
253 ```
254</CodeGroup>
255
256## Migrate to Task tools
257
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.
259
260| With `TodoWrite` | With Task tools |
261| --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
262| One tool call rewrites the full `todos` array | `TaskCreate` adds one item, `TaskUpdate` patches one item by `taskId` |
263| Match `block.name === "TodoWrite"` | Match `block.name === "TaskCreate"` or `"TaskUpdate"` |
264| 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 |
265| Render `block.input.todos` directly | Accumulate items across calls, or read a snapshot from a `TaskList` tool result |
266
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.
268
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
271The 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.
51Before 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:
52
53* **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.
54* **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.
55* **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.
56
57<Note>
58 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.
59</Note>
60
61### Monitor todo changes
62
63The 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.
64
65The 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.
27266
27367<CodeGroup>
27468 ```typescript TypeScript theme={null}
from line 72
27872 for await (const message of query({
27973 prompt: "Optimize my React app performance and track progress with todos",
28074 // 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" } },
75 options: { maxTurns: 15, permissionMode: "acceptEdits", env: { ...process.env, CLAUDE_CODE_ENABLE_TODO_TOOLS: "1" } },
28276 })) {
28377 if (message.type !== "assistant") continue;
28478 for (const block of message.message.content) {
from line 108
314108 async for message in query(
315109 prompt="Optimize my React app performance and track progress with todos",
316110 # 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"}),
111 options=ClaudeAgentOptions(max_turns=15, permission_mode="acceptEdits", env={"CLAUDE_CODE_ENABLE_TODO_TOOLS": "1"}),
318112 ):
319113 if not isinstance(message, AssistantMessage):
320114 continue
from line 116
322116 if not isinstance(block, ToolUseBlock):
323117 continue
324118 if block.name == "TaskCreate":
325 print(f"+ {block.input['subject']}")
119 print(f"+ {block.input.get('subject', '')}")
326120 elif block.name == "TaskUpdate" and block.input.get("status"):
327121 task_id = (
328122 block.input.get("taskId")
from line 134
340134 ```
341135</CodeGroup>
342136
343## Related Documentation
344
345* [TypeScript SDK Reference](/docs/en/agent-sdk/typescript)
346* [Python SDK Reference](/docs/en/agent-sdk/python)
347* [Streaming vs Single Mode](/docs/en/agent-sdk/streaming-vs-single-mode)
348* [Custom Tools](/docs/en/agent-sdk/custom-tools)
137### Display progress in real time
138
139The 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.
140
141The 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`.
142
143<CodeGroup>
144 ```typescript TypeScript theme={null}
145 import { query } from "@anthropic-ai/claude-agent-sdk";
146
147 type Task = { subject: string; activeForm?: string; status: string };
148
149 class TaskTracker {
150 private tasks = new Map<string, Task>();
151 private pendingCreates = new Map<string, { subject: string; activeForm?: string }>();
152
153 displayProgress() {
154 if (this.tasks.size === 0) {
155 console.log("\nProgress: no open tasks\n");
156 return;
157 }
158
159 const items = [...this.tasks.values()];
160 const completed = items.filter((t) => t.status === "completed").length;
161 const inProgress = items.filter((t) => t.status === "in_progress").length;
162
163 console.log(`\nProgress: ${completed}/${this.tasks.size} completed`);
164 console.log(`Currently working on: ${inProgress} task(s)\n`);
165
166 for (const [id, task] of this.tasks) {
167 const icon =
168 task.status === "completed" ? "✅" : task.status === "in_progress" ? "🔧" : "❌";
169 const text = task.status === "in_progress" && task.activeForm ? task.activeForm : task.subject;
170 console.log(`${id}. ${icon} ${text}`);
171 }
172 }
173
174 handleToolUse(block: { id: string; name: string; input: unknown }) {
175 if (block.name === "TaskCreate") {
176 const input = block.input as { subject: string; activeForm?: string; active_form?: string };
177 this.pendingCreates.set(block.id, {
178 subject: input.subject,
179 activeForm: input.activeForm ?? input.active_form,
180 });
181 } else if (block.name === "TaskUpdate") {
182 const input = block.input as {
183 taskId?: string;
184 id?: string;
185 task_id?: string;
186 status?: string;
187 activeForm?: string;
188 active_form?: string;
189 };
190 const taskId = input.taskId ?? input.id ?? input.task_id;
191 if (!taskId) return;
192 if (input.status === "deleted") {
193 this.tasks.delete(taskId);
194 this.displayProgress();
195 return;
196 }
197 const task = this.tasks.get(taskId);
198 if (!task) return;
199 if (input.status) task.status = input.status;
200 const active = input.activeForm ?? input.active_form;
201 if (active) task.activeForm = active;
202 this.displayProgress();
203 }
204 }
205
206 handleToolResult(block: { tool_use_id: string; is_error?: boolean }, result: unknown) {
207 const create = this.pendingCreates.get(block.tool_use_id);
208 if (!create) return;
209 this.pendingCreates.delete(block.tool_use_id);
210 if (block.is_error) return;
211 // The result's user message carries the tool's structured output as
212 // tool_use_result; for TaskCreate that's TaskCreateOutput,
213 // { task: { id, subject } }.
214 const out = result as { task?: { id: string } };
215 if (!out?.task?.id) return;
216 this.tasks.set(out.task.id, { ...create, status: "pending" });
217 this.displayProgress();
218 }
219
220 async trackQuery(prompt: string) {
221 try {
222 for await (const message of query({
223 prompt,
224 options: { maxTurns: 20, permissionMode: "acceptEdits", env: { ...process.env, CLAUDE_CODE_ENABLE_TODO_TOOLS: "1" } },
225 })) {
226 if (message.type === "assistant") {
227 for (const block of message.message.content) {
228 if (block.type === "tool_use") this.handleToolUse(block);
229 }
230 }
231 if (message.type === "user" && Array.isArray(message.message.content)) {
232 for (const block of message.message.content) {
233 if (block.type === "tool_result") this.handleToolResult(block, message.tool_use_result);
234 }
235 }
236 }
237 } catch (error) {
238 // A single-shot query() throws after yielding an error result,
239 // such as when the maxTurns limit is hit.
240 console.log(`Session ended with an error: ${error}`);
241 }
242 }
243 }
244
245 // Usage
246 const tracker = new TaskTracker();
247 await tracker.trackQuery("Build a complete authentication system with todos");
248 ```
249
250 ```python Python theme={null}
251 import asyncio
252
253 from claude_agent_sdk import (
254 query,
255 ClaudeAgentOptions,
256 AssistantMessage,
257 UserMessage,
258 ToolUseBlock,
259 ToolResultBlock,
260 )
261
262
263 class TaskTracker:
264 def __init__(self):
265 self.tasks: dict[str, dict] = {}
266 self.pending_creates: dict[str, dict] = {}
267
268 def display_progress(self):
269 if not self.tasks:
270 print("\nProgress: no open tasks\n")
271 return
272
273 completed = len([t for t in self.tasks.values() if t["status"] == "completed"])
274 in_progress = len([t for t in self.tasks.values() if t["status"] == "in_progress"])
275
276 print(f"\nProgress: {completed}/{len(self.tasks)} completed")
277 print(f"Currently working on: {in_progress} task(s)\n")
278
279 for task_id, task in self.tasks.items():
280 icon = (
281 "✅"
282 if task["status"] == "completed"
283 else "🔧"
284 if task["status"] == "in_progress"
285 else "❌"
286 )
287 text = (
288 task["activeForm"]
289 if task["status"] == "in_progress" and task.get("activeForm")
290 else task["subject"]
291 )
292 print(f"{task_id}. {icon} {text}")
293
294 def handle_tool_use(self, block: ToolUseBlock):
295 if block.name == "TaskCreate":
296 self.pending_creates[block.id] = {
297 "subject": block.input.get("subject", ""),
298 "activeForm": block.input.get("activeForm") or block.input.get("active_form"),
299 }
300 elif block.name == "TaskUpdate":
301 task_id = (
302 block.input.get("taskId")
303 or block.input.get("id")
304 or block.input.get("task_id")
305 )
306 if not task_id:
307 return
308 if block.input.get("status") == "deleted":
309 self.tasks.pop(task_id, None)
310 self.display_progress()
311 return
312 task = self.tasks.get(task_id)
313 if not task:
314 return
315 if block.input.get("status"):
316 task["status"] = block.input["status"]
317 active = block.input.get("activeForm") or block.input.get("active_form")
318 if active:
319 task["activeForm"] = active
320 self.display_progress()
321
322 def handle_tool_result(self, block: ToolResultBlock, tool_use_result):
323 create = self.pending_creates.pop(block.tool_use_id, None)
324 if create is None or block.is_error:
325 return
326 # The result's user message carries the tool's structured output as
327 # tool_use_result; for TaskCreate that's {"task": {"id": ..., "subject": ...}}.
328 task = (tool_use_result or {}).get("task") or {}
329 if not task.get("id"):
330 return
331 self.tasks[task["id"]] = {**create, "status": "pending"}
332 self.display_progress()
333
334 async def track_query(self, prompt: str):
335 try:
336 async for message in query(
337 prompt=prompt,
338 options=ClaudeAgentOptions(
339 max_turns=20,
340 permission_mode="acceptEdits",
341 env={"CLAUDE_CODE_ENABLE_TODO_TOOLS": "1"},
342 ),
343 ):
344 if isinstance(message, AssistantMessage):
345 for block in message.content:
346 if isinstance(block, ToolUseBlock):
347 self.handle_tool_use(block)
348 if isinstance(message, UserMessage) and isinstance(message.content, list):
349 for block in message.content:
350 if isinstance(block, ToolResultBlock):
351 self.handle_tool_result(block, message.tool_use_result)
352 except Exception as error:
353 # A single-shot query() raises after yielding an error result,
354 # such as when the max_turns limit is hit.
355 print(f"Session ended with an error: {error}")
356
357
358 # Usage
359 async def main():
360 tracker = TaskTracker()
361 await tracker.track_query("Build a complete authentication system with todos")
362
363
364 asyncio.run(main())
365 ```
366</CodeGroup>
367
368## Related documentation
369
370* [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
371* [Agent SDK reference - Python](/docs/en/agent-sdk/python): the options, types, and tool documentation for the Python SDK
372* [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
373* [Give Claude custom tools](/docs/en/agent-sdk/custom-tools): define your own tools with the SDK's in-process MCP server
349374