Handling tool_use Requests and Returning tool_result Blocks
Execute the requested tool and return a matching tool_result content block.
Search across all documentation pages
Execute the requested tool and return a matching tool_result content block.
When stop_reason on a Message comes back as "tool_use", Claude has paused mid-turn to ask your code to run something. It hasn't answered the user yet - it's waiting on you.
Your job is a fixed five-step handoff: find the tool_use blocks in response.content, run the matching Python function with block.input, append the assistant's response.content back onto messages verbatim, wrap each tool's output in a tool_result block keyed by tool_use_id, and call messages.create again.
This page is about doing that handoff correctly and defensively - reading block.input as the parsed dict the SDK gives you (never re-parsing or string-matching raw JSON), handling every tool_use block in a turn (not just the first one), and reporting failures with is_error instead of letting an exception kill the request.
Get this loop right once and it becomes a small, reusable function that every tool-using request in your application can call through.
Quick-reference recipe card - copy-paste ready.
if response.stop_reason == "tool_use":
messages.append({"role": "assistant", "content": response.content})
tool_results = []
for block in response.content:
if block.type != "tool_use":
continue
try:
result = execute_tool(block.name, block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": result,
})
except Exception as e:
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": str(e),
"is_error": True,
})
messages.append({"role": "user", "content": tool_results})
response = client.messages.create(
model="claude-sonnet-5",
max_tokens=1024,
tools=tools,
messages=messages,
)When to reach for this:
tools list and need to actually honor a tool_use response.import anthropic
client = anthropic.Anthropic()
weather_tool = {
"name": "get_weather",
"description": "Get current weather for a location. Call this when the user asks about current conditions.",
"input_schema": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "City and state, e.g., San Francisco, CA",
}
},
"required": ["location"],
},
}
stock_price_tool = {
"name": "get_stock_price",
"description": "Get the latest price for a stock ticker symbol.",
"input_schema": {
"type": "object",
"properties": {
"ticker": {
"type": "string",
"description": "Stock ticker symbol, e.g., AAPL",
}
},
"required": ["ticker"],
},
}
tools = [weather_tool, stock_price_tool]
def get_weather(location: str) -> str:
# Replace with a real weather API call.
return f"72F and sunny in {location}"
def get_stock_price(ticker: str) -> str:
# Replace with a real market-data API call.
if not ticker.isalpha():
raise ValueError(f"'{ticker}' is not a valid ticker symbol")
return f"{ticker.upper()} is trading at $184.32"
TOOL_FUNCTIONS = {
"get_weather": get_weather,
"get_stock_price": get_stock_price,
}
def execute_tool(name: str, tool_input: dict) -> str:
if name not in TOOL_FUNCTIONS:
raise ValueError(f"Unknown tool: {name}")
return TOOL_FUNCTIONS[name](**tool_input)
def run_turn(messages: list) -> list:
"""Send messages, handle any tool_use round trip, return the updated list."""
response = client.messages.create(
model="claude-sonnet-5",
max_tokens=1024,
tools=tools,
messages=messages,
)
while response.stop_reason == "tool_use":
messages.append({"role": "assistant", "content": response.content})
tool_results = []
for block in response.content:
if block.type != "tool_use":
continue
try:
result = execute_tool(block.name, block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": result,
})
except Exception as e:
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": str(e),
"is_error": True,
})
messages.append({"role": "user", "content": tool_results})
response = client.messages.create(
model="claude-sonnet-5",
max_tokens=1024,
tools=tools,
messages=messages,
)
messages.append({"role": "assistant", "content": response.content})
return messages
if __name__ == "__main__":
conversation = [
{
"role": "user",
"content": "What's the weather in Austin, TX, and what's AAPL trading at?",
}
]
conversation = run_turn(conversation)
final_text = next(
block.text for block in conversation[-1]["content"] if block.type == "text"
)
print(final_text)What this demonstrates:
while response.stop_reason == "tool_use": loop, so the handler keeps resolving tool calls until Claude produces a final text answer, not just one round trip.TOOL_FUNCTIONS) that maps block.name to a real Python function, with execute_tool raising a clean error for any name it doesn't recognize.tool_result-bearing user message.try/except around every tool execution, so one bad ticker symbol (get_stock_price raising ValueError) doesn't take down the weather lookup running alongside it.response.stop_reason == "tool_use" means Claude stopped generating specifically to hand control back to you - it is not an error state and not the final answer.response.content is a list of content blocks. A tool_use turn can contain a leading text block (Claude "thinking out loud") plus one or more tool_use blocks - always filter for block.type == "tool_use" rather than assuming a fixed shape.tool_use block carries .id (a unique identifier for this specific call), .name (which tool), and .input (the arguments, already parsed into a Python dict that matches your input_schema).messages must be response.content exactly as returned, not a reconstruction - it's what preserves the tool_use block's id so the follow-up tool_result can reference it correctly.tool_result blocks always live inside a user-role message, never an assistant-role one, even though nothing about them was typed by a human.content value | When to use it | Example |
|---|---|---|
| A plain string | Most tool outputs - text, JSON-as-string, short summaries | "72F and sunny in Austin, TX" |
| A list of content blocks | The tool's output includes multiple parts, such as text plus an image | [{"type": "text", "text": "..."}, {"type": "image", "source": {...}}] |
An error string with is_error: True | The tool raised or the underlying call failed | {"content": "Ticker not found", "is_error": True} |
# Never string-match raw JSON to figure out what Claude asked for.
# Claude 4-family models can escape Unicode or forward slashes differently
# across responses, so raw-text matching against block.input is fragile.
# Always use the SDK's already-parsed dict:
for block in response.content:
if block.type == "tool_use":
# block.input is a dict - index it directly, don't json.loads() it
location = block.input["location"]| Field | Type | Description |
|---|---|---|
type | str | Always "tool_result" for this block. |
tool_use_id | str | Must equal the .id of the tool_use block being answered. |
content | str or list[dict] | The tool's output - a string, or a list of content blocks. |
is_error | bool (optional) | Set True when the tool raised or otherwise failed; omit or set False on success. |
block.input. Some teams try to regex or string-match the tool call before parsing it. Claude 4-family models can vary how they escape Unicode or forward slashes in generated JSON, so this breaks intermittently. Fix: always read block.input - the SDK has already parsed it into a Python dict for you.response.content, as the assistant turn. If you extract response.content[0].text and push just that string back into messages, the tool_use block's id is lost and the next request fails validation. Fix: append response.content verbatim: messages.append({"role": "assistant", "content": response.content}).user messages (one per result) instead of one message with two blocks produces a malformed conversation. Fix: collect every tool_result into one list and send them together: messages.append({"role": "user", "content": tool_results}).tool_use block unanswered. If Claude requests three tools and you only return two tool_result blocks (say, because the third silently failed and you swallowed the exception), the next messages.create call rejects the request. Fix: wrap every tool call in try/except and always append a tool_result - on failure, set is_error: True instead of skipping it.is_error on a failed tool call. Returning the exception's string as ordinary content without is_error: True makes Claude treat a stack trace or error message as legitimate data, which it then may repeat back to the user as if it were a real answer. Fix: always set "is_error": True when the tool raised or returned a failure status.block.name. If Claude calls a tool name your dispatch code doesn't have a branch for, an unguarded if/elif chain falls through and either does nothing or raises an unhandled KeyError. Fix: make the "unknown tool" case an explicit branch that returns a tool_result with is_error: True, as execute_tool does in the Working Example above.content. Returning a custom class instance, a datetime, or a database row object directly as tool_result["content"] fails when the request is serialized. Fix: convert tool output to a string or plain JSON-serializable structure before building the tool_result block.| Alternative | Use When | Don't Use When |
|---|---|---|
| Manual round-trip loop (this page) | You want full control over execution order, retries, and per-tool error handling | The tool set is large and dynamic enough that a managed loop would save real boilerplate |
SDK-managed agent loop (client.beta.messages.tool_runner, beta) | You want the SDK to drive the create-execute-resubmit cycle for you instead of hand-rolling the while loop | You need custom logic per tool - different retry policies, logging, or non-standard result shapes |
| Third-party agent framework | You need built-in memory, retrieval, or multi-agent orchestration layered on top of tool use | You want minimal dependencies and full visibility into every request sent to the API |
| No tool use at all | The task only needs generation and no live data or side effects | The answer depends on real-time data, user-specific state, or an action the model can't perform on its own |
Check response.stop_reason == "tool_use" after every messages.create call. If it's true, response.content contains at least one tool_use block to handle before you have a final answer.
It's already parsed. block.input is a Python dict matching your input_schema - no decoding step is needed, and you should never fall back to matching the raw response text.
The tool_use block's id lives inside response.content. If you push only extracted text into messages, that id is gone, and the tool_result you send next has nothing valid to reference.
user. Even though a tool_result is generated by your code rather than typed by a person, the API models it as the next user turn in the conversation.
Loop over every block in response.content, executing each tool_use block you find, and collect all the resulting tool_result blocks into a single list. Send that list as one user message's content - not as two separate messages.
Usually a plain string. It can also be a list of content blocks (for example, text plus an image) when the tool's output has more than one part.
try:
result = execute_tool(block.name, block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": result,
})
except Exception as e:
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": str(e),
"is_error": True,
})Return a tool_result either way, with is_error: True on failure, so Claude can see what happened and adapt.
No. Every tool_use block in the assistant turn needs a matching tool_result in the next user message, even if that result is just an error string explaining why the call couldn't be completed.
Treat it like any other failure: return a tool_result with is_error: True and a message like "Unknown tool: <name>", instead of letting an unhandled branch crash your loop.
Yes. Claude may need to call a tool again based on the result you just returned, so the follow-up request should include the same tools list as the original call.
It's the only thing that pairs a tool_result with the specific tool_use block it's answering. A mismatched or missing tool_use_id produces a malformed conversation the API will reject.
The API doesn't care about execution order - it only requires that every tool_use block gets a tool_result back in the same follow-up message. You can execute multiple tools concurrently (for example, with a thread pool) as long as you still gather all the results into one user message before calling messages.create again.
input_schema that produces reliable block.input valueswhile loop shown here into a reusable helperStack versions: Written against the Claude model lineup current as of ~June 2026 - Claude Fable 5, Claude Opus 4.8, Claude Sonnet 5 (the default), and Claude Haiku 4.5 - and the official
anthropicPython SDK (latest 0.x release). Model names, SDK versions, and pricing move quickly - verify current specifics at platform.claude.com/docs before relying on them.
Reviewed by Chris St. John·Last updated Jul 19, 2026