Sending tool results
Claude asked for a tool. You know the name it wants, you know the arguments, you've appended the full multi-block assistant message to your history. Now you have to actually run the function, package up the result, and hand it back in the one specific shape Claude is expecting — a tool_result block in a user message, keyed to the original tool_use ID. This lesson walks through every piece.
Running the tool function
The tool_use block Claude returned carries an input field — a dict of arguments already parsed from JSON into Python. Your function takes keyword arguments. Python's ** unpacking bridges the two:
tool_use_block = response.content[1] # assuming text block at index 0
tool_name = tool_use_block.name
tool_input = tool_use_block.input
output = get_current_datetime(**tool_input)
# => '15:04:22'
**tool_input unpacks the dict into keyword arguments, so {"date_format": "%H:%M:%S"} becomes date_format="%H:%M:%S" at the call site. This is why you wrote the tool function with named parameters in the first place — it makes the tool dispatch this clean.
In a real dispatcher you'd look up the function by tool_name rather than hard-coding get_current_datetime. We'll build that lookup in the next lesson.
The tool_result block
You return tool output to Claude using a tool_result block placed inside a user message. That's deliberate — tool results come from "the user's side of the wire" even though they were generated by your server, because the user turn is the channel through which external information enters the conversation.
The block has three fields:
tool_use_id— must match theidof thetool_useblock you're answering. This is the connection that lets Claude correlate the result with the request.content— the output from your tool, serialised as a string. Simple values become strings directly; dicts getjson.dumps()-ed.is_error—Trueif the tool call failed,False(or omitted) otherwise.
Assembling the follow-up message:
messages.append({
"role": "user",
"content": [{
"type": "tool_result",
"tool_use_id": tool_use_block.id,
"content": "15:04:22",
"is_error": False,
}]
})
Note that content for a user message is now a list, not a string — the same list-of-blocks shape you saw on assistant messages. Each block is a dict (or a SDK object) with a type field discriminating between text, tool_result, and other kinds.
Handling multiple tool calls in one response
Claude can request more than one tool call in a single response. If you ask "What's 10 + 10 and what's 30 + 30?" with an add(a, b) tool available, Claude may return two tool_use blocks in a single assistant message — one per calculation.
Each tool_use gets a unique id, and you must return one tool_result block per id. The order doesn't have to match — Claude correlates them by id — but every id has to get a result. Missing one will break the follow-up request.
Concretely:
tool_use_blocks = [
b for b in response.content if b.type == "tool_use"
]
tool_result_blocks = []
for block in tool_use_blocks:
output = run_tool(block.name, block.input)
tool_result_blocks.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": json.dumps(output),
"is_error": False,
})
messages.append({
"role": "user",
"content": tool_result_blocks,
})
One user message, a list of result blocks, one per outstanding tool_use. Clean.
The follow-up request
Now send it back to Claude:
final = client.messages.create(
model=model,
max_tokens=1000,
messages=messages,
tools=[get_current_datetime_schema],
)
You must still include the tools list on the follow-up request, even though you're not expecting Claude to call anything else. Claude needs the schemas to understand the tool references it sees in the conversation history. Drop the tools parameter and the API will error out because the history references tools that aren't currently declared.
Claude's final response will be a normal assistant message with a text block containing the answer — "The current time is 15:04:22" — composed from the user's original question and the data your tool returned.
Serialising the content field
The content field on a tool_result block is a string. Your tool probably returned a richer shape — a dict, a list, a dataclass — and you need to flatten it before it goes into the block.
- Simple strings: pass through unchanged.
- Numbers, booleans, None: wrap in
str(). - Dicts and lists:
json.dumps(...)— this is the common case. - Dataclasses or Pydantic models: convert to dict first (
asdict()or.model_dump()), thenjson.dumps().
Claude is perfectly capable of reading JSON-formatted tool results — it's trained to. A serialised dict comes across as structured data without any extra hinting on your part. But it has to be a string at the wire level; nested objects inside the content field will fail validation.
Error results
When a tool raises, you still return a tool_result block — but with is_error: True and the error message in content:
try:
output = get_current_datetime(**tool_use_block.input)
result = {
"type": "tool_result",
"tool_use_id": tool_use_block.id,
"content": json.dumps(output),
"is_error": False,
}
except Exception as e:
result = {
"type": "tool_result",
"tool_use_id": tool_use_block.id,
"content": f"Error: {e}",
"is_error": True,
}
Two things happen when Claude sees is_error: True:
- It knows the call failed rather than returning nonsense, so it won't pass bogus data into the final answer.
- It reads the error message and can reason about retrying — which is why the error validation habit from two lessons ago matters. Informative errors let Claude recover; uninformative errors make it hallucinate.
You've now completed one full round-trip
User message → assistant (text + tool_use) → user (tool_result) → assistant (final answer). That's one complete tool use cycle. Everything from here is extending the pattern: multiple tools, multiple turns in a loop, streaming, first-party tools. The round-trip shape stays the same.
Key Takeaways
- 1Extract tool input from response.content[i].input and call your function with **input to unpack the dict into keyword arguments.
- 2Return tool output in a tool_result block placed inside a user message — that is the channel through which external data enters the conversation.
- 3A tool_result block has three fields: tool_use_id matching the original request, content serialised as a string, and is_error for failure signalling.
- 4Claude can request multiple tools in one response — process each tool_use, build a tool_result for each id, and send them all back in one user message.
- 5Include the tools list on the follow-up request too — Claude needs the schemas to resolve the tool references in history.
- 6On errors, still return a tool_result block with is_error True and an informative error message — Claude reads errors and can recover from them.