Study guides / CCDV-F / Domain 1

Applications & Integration · Lesson 5 of 11

1.5 - Wiring Tool Use into an Application Loop

Implement the request-execute-respond cycle that turns a Messages API call with tools into a working feature, not just a demo.

From an application's point of view, tool use is a contract: you describe tools with a name, description, and JSON Schema input_schema; Claude may return one or more tool_use blocks, each with its own id, name, and input object; your code executes the matching function and sends the result back as a tool_result content block, keyed to the original call by tool_use_id, inside a new user message. Miss that tool_use_id match and the model has no way to know which result answers which call, especially when it requested several tools in parallel — Claude can and does return multiple tool_use blocks in a single response when the tasks are independent of each other.

The description field on a tool definition is not decoration — it's the primary signal Claude uses to decide whether and when to call that tool, and a vague or misleading description is a common, entirely self-inflicted cause of a tool being called at the wrong time or not called when it should be. The input_schema should mark every field the tool genuinely needs as required; an optional field the model routinely omits produces malformed downstream calls that are easy to misdiagnose as a model problem when the actual issue is an underspecified schema.

tool_choice

tool_choice controls how much freedom Claude has: auto (default — decide whether to call a tool at all, and which one), any (must call some tool, but Claude picks which), tool with a name (force one specific tool), or none (disable tool calling for this turn even though tools are defined). Forcing a specific tool is a common pattern for structured extraction (Domain 2 covers this from the prompt-engineering side; here it's the wiring), where you don't want Claude free-texting an answer at all — the model's only valid move is to populate that tool's schema.

Handling errors inside the loop

A tool execution can fail — a downstream API times out, a lookup returns nothing, an input the model provided doesn't parse. The correct response is still a tool_result block, matched to the right tool_use_id, but with is_error: true and a content string describing what went wrong. This lets Claude reason about the failure and decide what to do next — retry with different input, try a different tool, or explain the problem to the user — instead of the application either crashing or silently swallowing the failure and leaving Claude to hallucinate a result it never actually got.

Common exam distractor

A tool_result must go in a new user message, not appended to the assistant's own turn. An answer that has the application append the result directly onto the assistant message is wrong — roles must still alternate correctly, and a tool result is something the application supplies, which makes it a user-role turn from the API's perspective even though no human typed it.

Replaying the assistant's own turn

When you send the follow-up call after executing a tool, the conversation history must include the assistant message exactly as Claude returned it — the full content array, including any text block that preceded the tool_use block, not just the tool call itself. Trimming that assistant turn down to only the parts your code cares about breaks the model's ability to see its own prior reasoning and can produce responses that contradict or repeat what it already said.

Parallel vs sequential tool calls

Claude decides on its own whether tasks are independent enough to request in parallel (multiple tool_use blocks in one response) or whether it needs one result before it can even formulate the next call (a single tool_use block, then, after seeing that result, another single-tool response). Your application loop needs to handle both shapes without assuming one or the other: code that only ever expects exactly one tool_use block per response will silently execute just the first of several parallel calls and never answer the rest, leaving the model waiting on tool_result blocks it will never receive. The robust pattern is to always collect every tool_use block in a response, execute all of them, and return a matching tool_result for each — whether that turns out to be one or several.

Designing the tool surface itself

A small number of well-scoped tools with clear, non-overlapping purposes is easier for Claude to select correctly than a large number of narrowly similar ones — two tools whose descriptions could both plausibly apply to the same request invite the model to guess, and a guess it gets wrong is a wasted round trip through the loop. Naming matters too: a tool named get_data tells the model almost nothing about when to use it, where get_customer_order_history tells it exactly. This is prompt-engineering territory as much as it is wiring, but it directly affects how reliably the loop in this lesson actually converges to a correct answer in a small number of iterations.

Exam traps

Practice question

Claude's response contains two tool_use blocks in the same turn, requesting a weather lookup and a currency conversion in parallel. What must the application's next message contain?

  • A A single tool_result block with both answers concatenated into one string.

    Each tool_use call needs its own tool_result block, individually matched by tool_use_id - concatenating loses that mapping.

  • B Two tool_result blocks, each with the tool_use_id of the call it answers, in one new user message. Correct

    This is exactly how the protocol expects parallel tool calls to be answered: one tool_result per tool_use, matched by id, in a single follow-up user message.

  • C Two separate API calls, one per tool result.

    Both results belong in the same follow-up message so Claude sees the full picture before continuing, not split across separate calls.

  • D An assistant message containing both results.

    Tool results come from the application, not the model, so they belong in a user message, not an assistant one.

Build exercise: Build a two-tool application loop end to end, including a failure path

Intermediate · 40 minutes

You'll practice:

  1. Define two simple tools (e.g. a calculator and a fake lookup function) with required fields marked in their input_schema, and send a prompt that needs both.

    A single-tool loop can hide id-matching bugs that only show up with more than one call in flight, and an underspecified schema hides itself until the model omits a field.

    You should see: A response with two tool_use blocks, each with its own id, and inputs that satisfy every required field.

    Hints
    1. What happens to a tool call if a field the tool actually needs isn't listed in the schema's required array?
    2. Mark every field the tool genuinely needs as required in input_schema - an optional field the model can omit will eventually get omitted, producing a malformed call your code has to guard against unnecessarily.
    3. const tools = [{
        name: "calculator",
        description: "Evaluates a mathematical expression and returns a numeric result.",
        input_schema: { type: "object", properties: { expression: { type: "string" } }, required: ["expression"] }
      }];
  2. Execute both tools in your code, then send one new user message containing two tool_result blocks, each tool_use_id matching the original call.

    This is the step where id-mismatch bugs actually happen in real code.

    You should see: A final response with stop_reason end_turn and text that correctly uses both results.

    Hints
    1. If Claude asks for a tool again instead of finishing, what's the first field you'd check for a mismatch?
    2. Log each tool_use_id you received alongside the tool_use_id you're sending back in each tool_result - a silent typo or swapped pairing is the most common cause of the model re-requesting a tool it thinks it never got an answer for.
    3. messages.push({ role: "assistant", content: response.content });
      messages.push({ role: "user", content: [
        { type: "tool_result", tool_use_id: call1.id, content: String(result1) },
        { type: "tool_result", tool_use_id: call2.id, content: String(result2) }
      ]});
  3. Deliberately make one tool execution fail (e.g. pass a malformed expression to the calculator) and return a tool_result with is_error: true instead of crashing or skipping it.

    This exercises the failure path the exam expects you to know, and shows Claude reasoning about a failure instead of hallucinating a result.

    You should see: Claude's next response acknowledges the failure - for example by explaining it couldn't complete that part, or by retrying with corrected input - rather than presenting a fabricated result as fact.

    Hints
    1. What two fields does a failed tool_result need beyond the normal ones?
    2. Set is_error: true on the tool_result block and put a human-readable explanation of the failure in its content - Claude uses both to decide what to do next.
    3. { type: "tool_result", tool_use_id: call.id, is_error: true, content: "Invalid expression: division by zero" }
  4. Wrap the whole thing in a while loop keyed on stop_reason, continuing on tool_use and exiting on end_turn, and confirm the assistant's full content array (not just the tool_use block) is present in the replayed history.

    This assembles everything into the actual production pattern, and checks the specific replay mistake - trimming the assistant turn - that silently degrades output quality.

    You should see: The loop runs to completion across both tool calls and the induced failure, and printing the message history shows the assistant's original text-plus-tool_use content intact at each step.

    Hints
    1. If Claude's response included a sentence of reasoning before the tool_use block, where does that sentence need to end up in your next request?
    2. Push response.content in its entirety onto the messages array for the assistant turn - don't filter it down to only the tool_use blocks before replaying it.
    3. while (true) {
        const response = await client.messages.create({ model, max_tokens, tools, messages });
        messages.push({ role: "assistant", content: response.content });
        if (response.stop_reason === "end_turn") break;
        // execute tools, push tool_result user message, loop
      }

Sources