Module 6 of 30
Multi-Tool
Orchestration

M05 gave Claude one tool. Now it juggles many — in parallel, in sequence, with dynamic filtering and graceful failure. Five concepts that turn a chatbot into a real agent.

Track 2: Tool Use ⏱ ~15 min read · 5 concepts 6 / 30
Concept 1 of 5

Independent calls run side by side

Claude can return multiple tool_use blocks in a single response when it judges the calls to be independent. Your code then runs them concurrently and returns all the results in one message back to Claude. The total wall-clock time becomes the slowest single tool, not the sum of all of them.

The win is double: fewer API round trips and shorter latency. Three tools that take 200 ms, 350 ms, and 500 ms run in 500 ms total — a 2.1× speedup over sequential. At 10,000 requests a day that is over 1.5 hours of cumulative user wait time saved.

Concept 1 of 5

One sous chef vs. a real brigade

BEFORE: A kitchen where a single sous chef does every prep task in order — chops onions, then dices tomatoes, then minces garlic. Three tasks of 10 minutes each, all serial.

PAIN: Service stalls. Three independent 10-minute jobs take 30 minutes when they could have taken 10. Every dish that needs those ingredients sits cold while one person sequences work that should run side by side.

MAPPING: A real brigade has multiple stations cooking at once. Independent prep happens in parallel; the head chef (Claude) only enforces order where one task genuinely needs another's output. Parallel tool calls are the brigade in code.

Concept 1 of 5

One response, many tool_use blocks

  1. Claude emits multiple tool_use blocksWhen the calls don't depend on each other, a single assistant message contains several tool_use entries, each with its own unique id and arguments.
  2. Your code fans them outYou iterate the blocks and dispatch them concurrently — ThreadPoolExecutor in Python, Promise.all in Node.js. Claude does NOT run tools; your client always does.
  3. Bundle the resultsOnce every tool has finished (or errored), package all the tool_result blocks into a SINGLE user message, each one referencing its matching tool_use_id.
  4. Send back in one round tripClaude reads the bundled results and produces either a final answer or another batch of tool calls. One API call covered N tools.

If your code processes tool_use blocks serially, you lose the speedup — Claude proposed parallelism, but you collapsed it back to serial.

Concept 1 of 5

Pseudocode

The core fan-out/fan-in for one parallel batch:

# Claude returned MANY tool_use blocks at once
calls = response.tool_uses   # e.g., 3 of them

results = []
PARALLEL FOR EACH call IN calls:
  TRY:
    out = RUN call.name(call.input)
    results.append(tool_result(call.id, out))
  CATCH error:
    results.append(tool_result(call.id, error,
                               is_error=True))

# Send ALL results back in ONE user message
messages.append({"role": "user",
                 "content": results})

response = CALL Claude(messages, tools)

Two non-negotiables: (1) every tool_result must reference the right tool_use_id, (2) all results ship back in a single message, not one per tool.

Concept 1 of 5

Misconceptions

"Claude automatically parallelizes for me."
Claude proposes parallel calls by emitting multiple tool_use blocks. Your code still has to actually dispatch them concurrently. A naive for loop runs them serially and burns the entire latency win.
"Just parallelize everything for max speed."
Only when calls are truly independent. If Tool B needs Tool A's output, parallel execution feeds B empty input and you get a wrong answer fast. Trust the dependency graph, not the speedometer.

Independent → parallel. Dependent → sequential. Claude returns multiple tool_use blocks in one response when calls don't depend on each other. Run them concurrently in your client and ship the results back in one user message.

Parallel is a free 2–3× latency win. But only if YOUR code actually fans the calls out.

Concept 2 of 5

Output of A becomes input of B

A sequential chain is a series of dependent tool calls: search returns URLs, fetch_page takes one of those URLs, summarize takes the page text. Each step's tool_result shows up in the conversation, then Claude emits the next tool_use. One full API round trip per link in the chain.

This is exactly the agentic loop you built in M05 — just running for several iterations. Your client keeps looping while stop_reason == "tool_use" and stops only when Claude returns "end_turn" with a final text answer. The research-assistant agent in the desktop module uses this pattern end-to-end: search → fetch → summarize → cite.

Concept 2 of 5

An assembly line, not a free-for-all

BEFORE: Imagine trying to build a car by dumping all the raw materials — steel, rubber, glass — into a room and hoping a finished vehicle assembles itself. Without a defined order, nothing fits.

PAIN: You can't bolt on a windshield before the frame is welded; you can't paint the body before it's assembled. Doing steps out of order wastes materials and produces a broken result.

MAPPING: A sequential chain is the assembly line. Station A (search) hands a URL to Station B (fetch_page), which hands page text to Station C (summarize). Each stage transforms the data and passes it forward. Skip a station and the next one gets the wrong input.

Concept 2 of 5

The agentic loop, one link at a time

  1. Send the user message + toolsClaude returns stop_reason: "tool_use" with one tool call — e.g., search("Claude AI").
  2. Execute, append, resendRun the tool, append its tool_result to the message history, send the whole conversation back. Claude now sees the URLs.
  3. Claude picks the next linkIt reads the search results and emits the next tool_use: fetch_page(url=...). Output of A became input of B.
  4. Loop until end_turnRepeat: execute, append, resend. The loop ends when stop_reason flips to "end_turn" and Claude returns a text answer.
  5. Watch the token costEvery round trip resends the full conversation history. A 5-step chain means 5 API calls with growing message arrays. Long chains get expensive — M08 covers context management.

The desktop research-assistant ties this all together: web_searchfetch_pagesummarize_textformat_citation in one continuous loop.

Concept 2 of 5

Pseudocode

The full chain loop — this is also the research-assistant agent from the desktop walkthrough:

# messages starts with the user's question
messages = [user_question]
iterations = 0

WHILE iterations < MAX_TURNS:
  response = CALL Claude(messages, tools)
  messages.append(response)        # keep history

  IF response.stop_reason == "end_turn":
    RETURN response.text     # done

  # Each link of the chain runs here
  results = []
  FOR EACH call IN response.tool_uses:
    TRY:
      out = RUN call.name(call.input)
      results.append(tool_result(call.id, out))
    CATCH error:
      results.append(tool_result(call.id, error,
                                 is_error=True))

  messages.append({"role": "user",
                   "content": results})
  iterations += 1

RAISE "chain hit MAX_TURNS"   # safety stop

Always cap iterations. Without MAX_TURNS a confused chain can loop forever and torch your token budget.

Concept 2 of 5

Misconceptions

"Sequential is always slower than parallel."
Only true for independent calls. If Tool B needs A's output, you must wait. Forcing parallelism breaks correctness. Latency matters; correctness matters more.
"The loop will end on its own."
It might not. Bad descriptions, ambiguous schemas, or repeated errors can trap Claude in a tool-call loop. Always set a hard MAX_TURNS (8–15 is reasonable) and surface it as a clear failure when hit.

One round trip per dependency. The agentic loop sends history, executes the next tool, appends its result, and resends — until stop_reason == "end_turn".

Cap iterations to keep costs and runaway loops in check. Long chains compound token cost on every step.

Concept 3 of 5

Descriptions decide accuracy

Claude picks a tool by reading the name, description, and parameter schema of every tool you send, then matching them against the user's intent and the conversation so far. The single biggest lever you control is the description. "searches stuff" leaves Claude guessing. "Search the web. Returns top 5 results with title, URL, and snippet. Use for current events or factual lookups." tells Claude exactly when to reach for it.

The other big lever is tool count. Selection accuracy is ~97% at 3 tools and ~95% at 5. It drops to 88% at 8, 76% at 12, and 61% at 18. A focused 4-tool agent beats a sprawling 18-tool agent almost every time.

Concept 3 of 5

The hardware store with no labels

BEFORE: A hardware store where every tool sits in an unmarked cardboard box — no labels, no descriptions, just numbered bins. You need to drive a small screw into a circuit board.

PAIN: You grab random boxes, try tools that don't fit, strip the screw, and waste an hour on a two-minute job. Worse, you might pick a power drill and destroy the board.

MAPPING: Tool descriptions are the labels. "searches stuff" is an unlabeled bin. "Phillips-head #2 screwdriver, for small electronics" is the label that makes the right pick obvious. Every clear description tilts the odds toward Claude reaching for the correct tool.

Concept 3 of 5

The four signals Claude weighs

  1. Tool name + descriptionThe primary signal. Claude reads every tool's name and description and asks "which one matches what the user wants?" Spell out WHAT, WHEN, and what it RETURNS.
  2. Parameter schemasWell-defined JSON Schemas (typed properties, required fields, per-property descriptions) help Claude generate correct arguments. Vague schemas produce wrong inputs.
  3. The user requestClaude maps natural-language intent to capabilities. "What's the weather?" maps to get_weather; "convert 72°F" maps to calculate.
  4. Conversation contextIf web_search just returned URLs, fetch_page becomes more likely than send_email. Recent results bias the next pick.
Concept 3 of 5

Writing a description Claude will trust

# Every description should answer 3 questions

WHAT does the tool do?
  # strong verb + concrete object
  "Search the web for current information."

WHEN should Claude reach for it?
  # trigger conditions vs. sibling tools
  "Use for recent events or factual lookups,"
  "NOT for internal company data (use query_db)."

WHAT does it return?
  # shape, size, format
  "Returns top 3 results: title, URL, snippet."

# Anti-patterns — never ship these
BAD: "database tool"           # no verb
BAD: "queries data"            # no scope
BAD: "helper for stuff"        # no anything

# Hard rule: cap toolset at 5–6 per request
IF tools.count > 6:
  SPLIT into specialized subagents

Tool descriptions are the new prompts. Treat the description with the same care you give a system prompt — it's what makes Claude pick the right button.

Concept 3 of 5

Misconceptions

"More tools = more capable agent."
The opposite, past 5–6 tools. Selection accuracy drops sharply, each schema burns 200–500 input tokens, and overlapping descriptions create ambiguity. A focused 4-tool agent beats a sprawling 18-tool one.
"Claude always picks the right tool."
Claude is good, not infallible. Ambiguous descriptions, overlapping capabilities, and misleading parameter names all cause misselection. Description engineering is the highest-leverage thing you can do for agent reliability.

Few tools, sharp descriptions. Cap each request at 4–6 tools. Make every description spell out WHAT it does, WHEN to use it (vs. siblings), and WHAT it returns.

If you outgrow 6 tools, distribute them across specialized subagents (covered in M14) rather than piling them onto one giant tool list.

Concept 4 of 5

Send only the tools this request needs

Every tool you put in the tools array gets serialized into input tokens and read by Claude before it picks one. Dynamic tool registration means building a different tools array per request — filtered by the user, the task category, or the current step of a workflow. A billing question doesn't need shipping tools; an admin tool shouldn't be visible to a regular user's research query.

Three wins: cost (fewer tokens per call), accuracy (Claude picks better from 4 focused options than 20 scattered ones), and security via least privilege (dangerous tools only appear when the caller is authorized).

Concept 4 of 5

The surgical nurse curating the tray

BEFORE: A surgeon walks into the OR and finds every instrument the hospital owns laid out on the tray — orthopedic saws, dental drills, eye-surgery lasers, and the cardiac tools they actually need. Hundreds of instruments, all within reach.

PAIN: The surgeon wastes time scanning past irrelevant tools, risks grabbing the wrong instrument under pressure, and the correct scalpel is buried under equipment for a different specialty.

MAPPING: Dynamic tool registration is the surgical nurse who curates the tray — only cardiac instruments for a heart surgery. In code, you filter the tools array per request based on the task. Fewer tools, less scanning, more accurate picks, fewer tokens.

Concept 4 of 5

The registry pattern

  1. Tag every toolGroup tools by category in a registry — e.g., data (search, fetch, query_db), comm (send_email, send_slack), admin (delete_user, modify_perms).
  2. Classify the requestUse the user role, the task type, or a quick router LLM call to decide which categories the request actually needs.
  3. Build the array per callPull only the matching tools from the registry. A research query gets the data tools; an admin task gets data + admin; a regular user never sees admin.
  4. Send the trimmed listPass the filtered array to tools in the API call. Repeat the filter on every iteration of the loop — the right toolset can change mid-conversation.

A typical tool definition is 200–500 tokens. Filtering 20 tools down to 5 saves ~3,000–7,500 input tokens per call. At 10K calls/day that is real money and noticeably faster responses.

Concept 4 of 5

Pseudocode

# A tagged registry: tool -> category
REGISTRY = {
  "web_search":    {category: "data",  schema: ...},
  "fetch_page":    {category: "data",  schema: ...},
  "query_db":      {category: "data",  schema: ...},
  "send_email":    {category: "comm",  schema: ...},
  "delete_user":   {category: "admin", schema: ...},
}

FUNCTION pick_tools(user, request):
  cats = ["data"]                # default

  IF request.intent == "send_message":
    cats.append("comm")
  IF user.role == "admin" AND request.is_admin_task:
    cats.append("admin")

  RETURN [t.schema FOR t IN REGISTRY.values()
                   IF t.category IN cats]

# Use it in the loop
tools = pick_tools(user, request)
response = CALL Claude(messages, tools)

Re-run pick_tools() on every loop iteration if the task changes shape mid-conversation (e.g., research turns into "now email this to the team").

Concept 4 of 5

Misconceptions

"Dynamic registration is premature optimization."
For 3 tools, yes. For a 15–20 tool production agent, it is essential. Twenty tools at 200–500 tokens each is 4,000–10,000 wasted tokens per call, plus the accuracy hit from too many options.
"Filter once at startup and you're done."
The right toolset is per-request, sometimes per-iteration. A user can pivot from "summarize this page" to "now send the summary to my manager" mid-conversation. Re-evaluate the filter inside the loop.

Curate the tray, every time. Build the tools array per request from a tagged registry — cost, accuracy, and least privilege all improve at once.

Same registry, different views. The full menu lives in code; only the relevant slice ever reaches Claude.

Concept 5 of 5

Fail loud, recover fast, never crash the loop

The more tools you chain, the higher the chance one of them breaks — a 404, a timeout, a permission denial, a rate limit. The rule is: never let a tool error throw out of the loop. Catch it, package it as a tool_result with is_error: true, and hand it back to Claude. Claude can then reason about a fallback — switch to web_search, ask the user, or apologize gracefully.

For repeated failures, layer on two patterns: exponential backoff (1s, 2s, 4s, max 3 attempts) for transient issues like timeouts, and a circuit breaker that disables a tool after N consecutive failures so the agent stops burning tokens on a broken endpoint.

Concept 5 of 5

The relay race with a backup plan

BEFORE: A relay race where the team has no recovery protocol — four runners, one baton, and if anyone trips, the whole team is disqualified. No substitutes, no backup baton, nothing.

PAIN: The second runner twists an ankle at the handoff. The baton hits the ground, the team freezes, and they forfeit a race they were winning. One failure cascades into total failure.

MAPPING: Multi-tool workflows face the same risk. The fix is the backup plan: is_error: true tells Claude "this runner is down, hand off to another." Retries cover ankle-tweaks (transient slips). The circuit breaker pulls a runner who has fallen three times in a row out of the rotation entirely.

Concept 5 of 5

Three layers of recovery

  1. Per-tool try/catchWrap every tool call in error handling. On failure, return a tool_result with is_error: true and a short, descriptive message ("Timeout 30s contacting orders-api"). Never throw out of the loop.
  2. Exponential backoff for transientsNetwork blips, rate limits, 503s — retry with growing delays (1s, 2s, 4s) up to a hard cap (typically 3 attempts). Stop and report if it still fails.
  3. Circuit breaker for persistentsTrack consecutive failures per tool. After N (typically 3–5), "open" the circuit — remove that tool from the next call's tools array. After a cooldown (~60s), half-open and try once to see if it recovered.
  4. Let Claude adaptStructured error info is enough for Claude to pivot — "fetch_page failed, I'll try web_search instead." The model is good at this only when you give it real signal, not silent empty results.

Critical distinction: empty result ({"results":[]}) means "I checked, nothing matched." Error (is_error: true) means "I couldn't even check." Mixing them up is a silent, hard-to-debug failure.

Concept 5 of 5

What to do when a tool fails

tool_call_failed(tool, error):

  IF error is transient:
    # 5xx, timeout, rate limit, network blip
    IF attempt < 3:
      wait(2 ** attempt)         # 1s, 2s, 4s
      RETRY
    ELSE:
      breaker[tool] += 1
      RETURN tool_result(is_error=True,
                          msg="transient, gave up after 3")

  IF error is permanent:
    # 4xx, auth failure, validation error
    breaker[tool] += 1
    RETURN tool_result(is_error=True,
                        msg=error.detail)
    # let Claude pivot to alternative tool

  IF breaker[tool] >= 3:
    # circuit OPEN — pull tool for ~60s
    REGISTRY.disable(tool, cooldown=60)
    notify_user(f"{tool} unavailable, working around it")

  # NEVER do this:
  # return empty result on real error ->
  # Claude thinks "no data" instead of "broken"

Retries handle this call. Circuit breakers handle the pattern across calls. Use both together: retry transients within a call, breaker the persistent ones across calls.

Concept 5 of 5

Misconceptions

"If a tool fails, just return an empty result."
Empty ({"results":[]}) tells Claude "I checked, nothing there." Error (is_error: true) tells Claude "I couldn't even check." Different conclusions, different next moves. Mixing them is the most common silent failure in agent code.
"Just retry 100 times — it'll work eventually."
Brute-force retries burn tokens, time, and downstream API quota. If it's down, it's down. Cap retries at 3 with exponential backoff for transients; trip a circuit breaker on persistent failures and route around the broken tool.
"Claude will recover from any error automatically."
Only if you give it structured error data AND an alternative path exists. If the only data source is the broken tool, Claude can apologize but can't conjure facts. Always design fallbacks where the stakes are real.

Catch, label, route around. Wrap every tool, return is_error: true with a clear message, retry transients with backoff (max 3), and trip a circuit breaker after 3–5 consecutive failures.

The whole point: errors flow back to Claude as data, not exceptions. Data is something Claude can reason about; an exception just kills the loop.

Tap a card to reveal the answer

Open the full module on desktop

The desktop version walks through a 5-tool research-assistant agent: parallel web_search, sequential fetch_pagesummarizeformat_citation, a tagged tool registry, exponential backoff, and a circuit breaker — in both Python and Node.js.

Open M06 on Desktop → Full code · Hands-on lab · ~70 min

Module 6 of 30 · Track 2: Tool Use