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.
The 5 concepts of multi-tool orchestration
Tap any concept to jump to its 5-card cluster.
- 1Parallel Tool CallsWhen and why Claude fires multiple tools at once
- 2Sequential ChainsWhen the output of A becomes the input of B
- 3Tool SelectionHow Claude picks the right tool from the menu
- 4Dynamic RegistrationFilter the tool list per request — cost, accuracy, security
- 5Error HandlingWhen tools break: is_error, retries, circuit breakers
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.
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.
One response, many tool_use blocks
- Claude emits multiple tool_use blocksWhen the calls don't depend on each other, a single assistant message contains several
tool_useentries, each with its own uniqueidand arguments. - Your code fans them outYou iterate the blocks and dispatch them concurrently —
ThreadPoolExecutorin Python,Promise.allin Node.js. Claude does NOT run tools; your client always does. - Bundle the resultsOnce every tool has finished (or errored), package all the
tool_resultblocks into a SINGLE user message, each one referencing its matchingtool_use_id. - 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.
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.
Misconceptions
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.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.
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.
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.
The agentic loop, one link at a time
- Send the user message + toolsClaude returns
stop_reason: "tool_use"with one tool call — e.g.,search("Claude AI"). - Execute, append, resendRun the tool, append its
tool_resultto the message history, send the whole conversation back. Claude now sees the URLs. - 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. - Loop until end_turnRepeat: execute, append, resend. The loop ends when
stop_reasonflips to"end_turn"and Claude returns a text answer. - 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_search → fetch_page → summarize_text → format_citation in one continuous loop.
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.
Misconceptions
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.
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.
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.
The four signals Claude weighs
- 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.
- Parameter schemasWell-defined JSON Schemas (typed properties, required fields, per-property descriptions) help Claude generate correct arguments. Vague schemas produce wrong inputs.
- The user requestClaude maps natural-language intent to capabilities. "What's the weather?" maps to
get_weather; "convert 72°F" maps tocalculate. - Conversation contextIf
web_searchjust returned URLs,fetch_pagebecomes more likely thansend_email. Recent results bias the next pick.
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.
Misconceptions
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.
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).
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.
The registry pattern
- 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). - Classify the requestUse the user role, the task type, or a quick router LLM call to decide which categories the request actually needs.
- 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.
- Send the trimmed listPass the filtered array to
toolsin 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.
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").
Misconceptions
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.
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.
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.
Three layers of recovery
- Per-tool try/catchWrap every tool call in error handling. On failure, return a
tool_resultwithis_error: trueand a short, descriptive message ("Timeout 30s contacting orders-api"). Never throw out of the loop. - 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.
- Circuit breaker for persistentsTrack consecutive failures per tool. After N (typically 3–5), "open" the circuit — remove that tool from the next call's
toolsarray. After a cooldown (~60s), half-open and try once to see if it recovered. - 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.
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.
Misconceptions
{"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.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
tools array per request based on the user's role and the task category. A research query gets data tools only; admin tools never reach a regular user.tool_result with is_error: true and a short message ("Timeout after 30s contacting orders-api"). Claude can then try a fallback tool, retry with different inputs, or apologize gracefully. Returning an empty result ({"results":[]}) silently lies to Claude — it concludes "no data on this topic" instead of "the tool is broken, try another path." That's the #1 silent failure mode in agent code.Open the full module on desktop
The desktop version walks through a 5-tool research-assistant agent: parallel web_search, sequential fetch_page → summarize → format_citation, a tagged tool registry, exponential backoff, and a circuit breaker — in both Python and Node.js.
Module 6 of 30 · Track 2: Tool Use