How do you return a tool_result to Claude after a tool_use block?
answer
- The API keeps no session state
- Append the whole content array
- A user message, not a tool role
- Ids must match the previous assistant turn
- Failures come back as results too
basics
~20 sAppend the assistant's entire content array to messages, then add a new user message whose content holds a tool_result block. That block carries tool_use_id matching the call's id, plus the result content. Then call the API again.
solid answer
~40 sThe Messages API is stateless, so continuing a tool call means rebuilding the conversation. When a response comes back with `stop_reason: "tool_use"`, you append the assistant turn to `messages` using the **full** `response.content` array — the `tool_use` block has to survive, not just the text. You then append a message with `role: "user"` whose content contains a `tool_result` block: `{"type": "tool_result", "tool_use_id": <the id from the tool_use block>, "content": "..."}`. The `tool_use_id` must match the id of a call in the immediately preceding assistant turn, or the API rejects the request. If your function threw, still send the block and set `"is_error": true` with the message. Then call the API again with the grown `messages` and loop until the response no longer stops on `tool_use`.
code
python · 51 linesimport anthropic
client = anthropic.Anthropic()
tools = [{
"name": "get_stock",
"description": "Look up units in stock for a SKU. Call this for any stock question.",
"input_schema": {
"type": "object",
"properties": {"sku": {"type": "string", "description": "Product SKU"}},
"required": ["sku"],
},
}]
def get_stock(sku: str) -> str:
return f"{sku}: 42 units"
messages = [{"role": "user", "content": "How many of SKU-9 are left?"}]
for _ in range(5): # hard iteration cap
response = client.messages.create(
model="claude-opus-5", max_tokens=1024, tools=tools, messages=messages
)
if response.stop_reason != "tool_use":
break
messages.append({"role": "assistant", "content": response.content})
results = []
for block in response.content:
if block.type != "tool_use":
continue
try:
output = get_stock(**block.input)
results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": output,
})
except Exception as exc:
results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": str(exc),
"is_error": True,
})
messages.append({"role": "user", "content": results})
print(response.content)go deeper
Know that the result goes back in a user message as a tool_result block, and that tool_use_id has to be copied from the call. Say plainly that the API stores nothing between requests.
Walk the four steps of one iteration and explain why the full assistant content array must be appended. Be ready to contrast the block-based shape with a role:'tool' message.
Show the operational picture: error results with is_error, iteration caps, and the quadratic input-token growth that makes trimming tool output and caching the prefix worth real effort.
Own the loop's failure modes and budget — what a tool is allowed to return, when results get cleared or summarised, and the policy for approval gates on side-effecting calls.
## The API remembers nothing Every `POST /v1/messages` call is independent. There is no session, no thread id, no server-side handle on a pending call. The only reason Claude knows a tool was requested and answered is that you resend the whole exchange. That single fact explains every rule below. ## The four steps of one loop iteration **1. Detect.** The response arrives with `stop_reason: "tool_use"` and its `content` array holds one or more blocks of `type: "tool_use"`, each with an `id`, a `name`, and an `input` object of arguments. There may also be a `text` block ahead of them where Claude narrates what it is about to do — that is normal and you keep it. **2. Append the assistant turn verbatim.** Push `{"role": "assistant", "content": response.content}` — the whole array of block objects, not `response.content[0].text`. Flattening to a string is the classic bug: the `tool_use` block disappears, and your next request contains a `tool_result` answering a call that no longer exists in the history. The API returns a 400 for that mismatch. **3. Execute and answer in a user turn.** Run the function named by `name` with the `input` arguments, then append `{"role": "user", "content": [{"type": "tool_result", "tool_use_id": block.id, "content": result}]}`. The `content` of a `tool_result` may be a plain string or a list of content blocks — which is how you hand back an image a tool produced rather than a description of it. The role is genuinely `user`; Anthropic has no `tool` role. **4. Re-request and repeat.** Call the API again with the grown `messages`. Claude reads the result and either calls another tool — repeat — or ends the turn with prose. Loop until `stop_reason` is no longer `tool_use`, with an iteration cap so a model that keeps re-calling a broken tool cannot spin forever. ## tool_use_id is the join key Each `tool_result` names exactly one call. The id is opaque and server-generated; you copy it, never construct it. It must correspond to a `tool_use` in the immediately preceding assistant message — you cannot answer a call from three turns ago, and you cannot leave one unanswered while answering another. Mismatch, omission or a fabricated id all produce a 400. ## Errors are results, not exceptions When your function raises, the wrong move is to drop the block, retry silently, or replace it with a chatty user message. Send the `tool_result` with `"is_error": true` and a message the model can act on — "City not found: 'Berln'. Check spelling." Claude typically corrects the argument and retries, or explains the failure to the user. An error result keeps the conversation structurally valid; a missing one breaks it. ## Not OpenAI's shape OpenAI answers a call with a message of `role: "tool"` carrying `tool_call_id`. Anthropic uses a `user` message carrying a `tool_result` **content block** with `tool_use_id`. Different role, different field name, different nesting level. Porting a loop between the two is a rewrite of this step, not a rename. ## Cost shape of the loop Because you resend everything, input tokens grow with every iteration: turn five re-sends turns one through four plus all their tool results. A ten-step agent loop is dominated by input cost, not output. This is why long agent conversations pair naturally with caching over the stable prefix, and why bulky tool results — a whole HTML page, an unfiltered query dump — are expensive twice over: once when returned, and again on every subsequent turn. Trim what a tool returns to what the model actually needs. ## SDK helpers The Anthropic SDKs ship a tool runner that drives this loop for you: you register functions, and it detects calls, executes them, appends results and re-requests until Claude stops. It still exposes per-iteration hooks for approval gates, error interception and result modification. Writing the loop by hand is worth doing once so you understand the message shape; in production, reach for the runner unless you need control it does not expose.
- What breaks if you append only the assistant's text instead of the whole content array?The `tool_use` block vanishes from the history. Your next request then contains a `tool_result` whose `tool_use_id` matches nothing in the preceding assistant turn, and the API returns a 400. Even if it were accepted, Claude would have lost the record of what it asked for and what arguments it chose, so the result would arrive with no context to attach it to.
- A tool call throws a timeout. What do you send back and why not just retry the request?Send the `tool_result` block with `is_error: true` and a short message such as "Upstream timed out after 5s". Retrying the whole API request re-bills the entire prompt and gives Claude no new information. Returning the error keeps the message history valid and lets the model decide — retry with a narrower query, fall back to another tool, or tell the user. You can retry the *function* yourself before giving up; that is a separate choice.
- Why does input cost grow non-linearly over a long Claude tool loop, and what do you do about it?Every iteration resends the whole conversation, so tokens accumulate quadratically across turns: step N re-bills steps 1..N-1 plus their results. The levers are trimming what tools return so bulky payloads do not persist, caching the stable prefix, and clearing or summarising old tool results once they no longer inform the next decision.
- How do you stop a loop that keeps calling the same tool forever?Cap iterations explicitly — the loop condition should be `stop_reason == "tool_use"` *and* a counter under some maximum. On hitting the cap, either return a `tool_result` telling the model to stop and summarise, or send a final turn with `tool_choice` set to none so it must answer in prose. Never rely on the model to notice it is looping.
saying these in an interview costs you the question
- Sends a message with role tool, as in OpenAI's API
- Appends only response.content[0].text to the history
- Invents or reuses a tool_use_id instead of copying it
- Drops the tool_result entirely when the function throws
- Assumes the API tracks the pending call server-side