Handling tool_use Requests and Returning tool_result Blocks
Execute the requested tool and return a matching tool_result content block.
Summary
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.
Recipe
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:
- Any request where you passed a non-empty
toolslist and need to actually honor atool_useresponse. - Turns where Claude might request more than one tool at once - the loop above handles all of them before replying.
- Anywhere a tool call can plausibly fail (a flaky API, a bad lookup) and you want Claude to see the failure instead of your process crashing.
- Building a general-purpose "run this conversation until Claude stops asking for tools" helper you'll reuse across endpoints.
Working Example
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:
- A
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. - A dispatch table (
TOOL_FUNCTIONS) that mapsblock.nameto a real Python function, withexecute_toolraising a clean error for any name it doesn't recognize. - Two tools requested in the same user question, both resolved and returned together in a single
tool_result-bearingusermessage. - A
try/exceptaround every tool execution, so one bad ticker symbol (get_stock_priceraisingValueError) doesn't take down the weather lookup running alongside it.
Deep Dive
How It Works
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.contentis a list of content blocks. Atool_useturn can contain a leadingtextblock (Claude "thinking out loud") plus one or moretool_useblocks - always filter forblock.type == "tool_use"rather than assuming a fixed shape.- Each
tool_useblock carries.id(a unique identifier for this specific call),.name(which tool), and.input(the arguments, already parsed into a Python dict that matches yourinput_schema). - The assistant message you append to
messagesmust beresponse.contentexactly as returned, not a reconstruction - it's what preserves thetool_useblock'sidso the follow-uptool_resultcan reference it correctly. - The
tool_resultblocks always live inside auser-role message, never anassistant-role one, even though nothing about them was typed by a human.
Structuring tool_result Content
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} |
Python Notes
# 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"]Parameters & Return Values
| 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. |
Gotchas
- Matching on raw JSON text instead of
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 readblock.input- the SDK has already parsed it into a Python dict for you. - Appending only the text, not the full
response.content, as the assistant turn. If you extractresponse.content[0].textand push just that string back intomessages, thetool_useblock'sidis lost and the next request fails validation. Fix: appendresponse.contentverbatim:messages.append({"role": "assistant", "content": response.content}). - Splitting multiple tool_result blocks across separate user messages. When Claude requests two tools in one turn, sending two follow-up
usermessages (one per result) instead of one message with two blocks produces a malformed conversation. Fix: collect everytool_resultinto one list and send them together:messages.append({"role": "user", "content": tool_results}). - Leaving a
tool_useblock unanswered. If Claude requests three tools and you only return twotool_resultblocks (say, because the third silently failed and you swallowed the exception), the nextmessages.createcall rejects the request. Fix: wrap every tool call intry/exceptand always append atool_result- on failure, setis_error: Trueinstead of skipping it. - Forgetting
is_erroron a failed tool call. Returning the exception's string as ordinarycontentwithoutis_error: Truemakes 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": Truewhen the tool raised or returned a failure status. - Not handling an unrecognized
block.name. If Claude calls a tool name your dispatch code doesn't have a branch for, an unguardedif/elifchain falls through and either does nothing or raises an unhandledKeyError. Fix: make the "unknown tool" case an explicit branch that returns atool_resultwithis_error: True, asexecute_tooldoes in the Working Example above. - Passing a non-serializable object as
content. Returning a custom class instance, adatetime, or a database row object directly astool_result["content"]fails when the request is serialized. Fix: convert tool output to a string or plain JSON-serializable structure before building thetool_resultblock.
Alternatives
| 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 |
FAQs
How do I know when Claude wants me to run a tool?
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.
Is block.input already parsed, or do I need to json.loads() it myself?
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.
Why does the assistant message need response.content instead of just the 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.
What role does the tool_result message use - assistant or user?
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.
What happens if Claude asks for two tools in the same turn?
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.
What should content be inside a tool_result block?
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.
What do I do if my tool function raises an exception?
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.
Can I skip returning a tool_result if the tool call doesn't make sense?
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.
What if Claude requests a tool name I don't recognize?
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.
Do I need to keep passing tools= on the follow-up messages.create call?
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.
Why does tool_use_id matter so much?
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.
Should tool execution happen sequentially or can I run tools in parallel?
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.
Related
- Tool Use Basics - the full 10-example walkthrough this page's round trip is drawn from
- Defining Tool Schemas with name, description, and input_schema - writing the
input_schemathat produces reliableblock.inputvalues - Building a Multi-Turn Tool Use Loop - generalizing the
whileloop shown here into a reusable helper - Executing Parallel Tool Calls in a Single Turn - more on batching multiple tool_result blocks together
Stack 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.