Claude Certification Guides
CCDV-F/Domain 89 min read

Lesson 8.2

Handling Tool Calls and Results

The exact wire format of the tool_use and tool_result cycle, parallel calls, error reporting, and the edge cases that break tool loops in production.

Defining a tool is half the contract; the other half is executing what Claude asks for and reporting back in the format the API expects. Get the shapes slightly wrong and you receive a 400; get the semantics wrong and the model learns bad habits within a single conversation. The exam tests the request and response JSON precisely.

What Claude sends

When Claude decides to use a tool, the response has stop_reason: "tool_use" and one or more tool_use blocks in content, often preceded by a short text block:

{
  "role": "assistant",
  "stop_reason": "tool_use",
  "content": [
    {"type": "text", "text": "I'll check that order for you."},
    {"type": "tool_use",
     "id": "toolu_01A09q90qw90lq917835lq9",
     "name": "get_order_status",
     "input": {"order_id": "ORD-10422", "include_history": false}}
  ]
}

Three fields matter: id (unique per call, you will echo it), name (which of your tools), and input (already-parsed JSON matching the schema). Never match on the serialised string of input; read the object.

What you send back

Append the assistant message exactly as received, then a user message whose content is tool_result blocks:

{
  "role": "user",
  "content": [
    {"type": "tool_result",
     "tool_use_id": "toolu_01A09q90qw90lq917835lq9",
     "content": "{\"status\": \"shipped\", \"carrier\": \"DHL\", \"eta\": \"2026-10-21\"}"}
  ]
}

tool_use_id must match the id of the block it answers. content can be a string or an array of text and image blocks, so a screenshot tool can return a picture. Then call the API again with the extended history; Claude continues from the result.

Parallel tool calls

Claude may request several tools in one response. Run them (concurrently if independent) and return all results in a single user message, in any order:

if resp.stop_reason == "tool_use":
    messages.append({"role": "assistant", "content": resp.content})
    calls = [b for b in resp.content if b.type == "tool_use"]
    outputs = await asyncio.gather(*(run_tool(c) for c in calls))
    messages.append({"role": "user", "content": [
        {"type": "tool_result", "tool_use_id": c.id, "content": out}
        for c, out in zip(calls, outputs)
    ]})

Splitting results across several user messages, or interleaving text between them, is a common bug: the API requires each tool_use to be answered in the very next user message, and returning results one at a time teaches Claude to stop batching calls.

Reporting failures

When a tool fails, do not throw out of the loop and do not fabricate a result. Return a tool_result with is_error: true and a message that helps the model recover:

results.push({
  type: "tool_result",
  tool_use_id: call.id,
  content: `Order not found. Valid IDs look like ORD-12345.`,
  is_error: true,
});

Claude typically apologises, retries with corrected input, or asks the user. For permission denials, return a clear "the user declined this action" result rather than silently skipping, so the model does not assume the action happened.

Edge cases that break loops

SituationSymptomCorrect handling
stop_reason: "max_tokens" with a tool_use presentTruncated inputDo not execute; raise max_tokens and retry the turn
A tool_result with no matching tool_use in the previous assistant turn400Keep assistant and result messages adjacent and complete
Tool returns megabytesContext blows up, cost spikesTruncate or summarise inside the tool
Model requests an unknown tool nameYour dispatcher crashesReturn is_error: true with "unknown tool"
stop_reason: "pause_turn"Server tool pausedResend the conversation unchanged
Input fails validationWrong types despite schemaUse strict: true, or validate and return is_error

Streaming tool calls

In a stream, a tool_use block arrives as content_block_start with the id and name, followed by input_json_delta events carrying partial_json fragments, then content_block_stop. Concatenate the fragments and parse once the block stops. The SDK's get_final_message() / finalMessage() does this for you and yields parsed tool_use blocks.

Letting the SDK run the cycle

The tool runner in each SDK executes this entire cycle: it calls the API, runs your functions, builds tool_result blocks with the right IDs, and loops until end_turn. It is the recommended path when you do not need custom control flow, but the stop-reason checks above remain your responsibility; the runner does not stop on max_tokens or refusal for you.

Caching the cycle

Each turn appends an assistant message and a user message, so an agent conversation grows fast. Put a cache breakpoint on the last block of the newest message (or use automatic caching) so previous turns, including all earlier tool results, are read from cache.

Exam focus

  • A tool call is a tool_use block with id, name and parsed input, and the response has stop_reason: "tool_use".
  • The reply is a user message of tool_result blocks, each with the matching tool_use_id; all results for one turn go in one message immediately after the assistant turn.
  • Failures are returned with is_error: true and a helpful message, never by aborting or inventing results.
  • tool_result.content can be text or an array of text and image blocks.
  • Never execute a tool call from a response that stopped on max_tokens; the input may be truncated.
  • In streams, input_json_delta fragments are concatenated and parsed only after content_block_stop.

Check yourself

5 questions

  1. 1.Which field in a tool_result block links the result to the call Claude made?

  2. 2.A tool handler throws a timeout. The developer returns: {"type": "tool_result", "tool_use_id": id, "content": "Error: upstream timeout"} What improvement does Anthropic recommend?

  3. 3.Claude returns three tool_use blocks in one assistant message. The developer sends three consecutive user messages, each containing one tool_result. What happens?

  4. 4.A take_screenshot tool needs to return an image to Claude. How can a tool_result carry it?

  5. 5.What does tool_choice: {"type": "none"} do?