Claude Tool Use: Building Multi-Step Agent Workflows
When people search for "Claude tool use multi step agent workflow," they're usually trying to solve one of two problems: either Claude keeps stopping after a single tool call when it should keep working toward a goal, or they're not sure how to structure the loop that lets Claude call multiple tools in sequence to complete a task. Both come down to the same thing — understanding the request/response cycle that drives agentic behavior with tool use.
This article walks through how Claude's tool use actually works across multiple steps, the loop structure you need to build, and common failure modes (infinite loops, dropped context, malformed tool inputs) along with how to fix them.
How Multi-Step Tool Use Actually Works
Claude doesn't execute tools itself. When you give it a set of tool definitions, it decides when a tool is needed, returns a tool_use content block with the tool name and input, and then stops generating. Your application code is responsible for:
- Running the actual tool (a function call, API request, database query, etc.)
- Sending the result back to Claude as a
tool_resultblock - Letting Claude continue — which might mean calling another tool, asking a follow-up, or producing a final answer
A "multi-step agent workflow" is just this loop repeated until Claude returns a final text response with no further tool calls. There's no special "agent mode" flag — it's a pattern you implement around the API.
The Basic Loop Structure
Here's the shape of a minimal agent loop in JavaScript:
async function runAgent(userMessage, tools, toolHandlers) {
let messages = [{ role: "user", content: userMessage }];
while (true) {
const response = await callClaude(messages, tools);
messages.push({ role: "assistant", content: response.content });
const toolCalls = response.content.filter(
(block) => block.type === "tool_use"
);
if (toolCalls.length === 0) {
return response; // final answer, no more tools needed
}
const toolResults = await Promise.all(
toolCalls.map(async (call) => {
const result = await toolHandlers[call.name](call.input);
return {
type: "tool_result",
tool_use_id: call.id,
content: JSON.stringify(result),
};
})
);
messages.push({ role: "user", content: toolResults });
}
}
The key details that trip people up:
- Every
tool_useblock needs a matchingtool_result, matched bytool_use_id. Missing one will cause the next request to fail or behave unpredictably. - Tool results go back as a user-role message, not assistant. This confuses a lot of first-time implementers.
- The full message history accumulates. Each loop iteration appends to the array — Claude needs the whole conversation, not just the latest exchange, to reason about what it's already done.
If you're calling the API directly, the request/response contract is documented at /docs/messages and the tool schema specifics at /docs/tools.
Handling Parallel Tool Calls
Claude can request multiple tools in a single turn — for example, checking weather in three cities at once. Your loop should handle an array of tool_use blocks, not assume there's only one. The example above already does this with Promise.all, which also keeps latency down when tools are independent of each other.
If one tool depends on the output of another, Claude generally won't request them in parallel — it'll wait for the first result before deciding on the next call. You don't need to detect dependencies yourself; just execute whatever Claude asks for in that turn and feed results back.
Stopping Conditions and Guardrails
The biggest practical risk in multi-step workflows is runaway loops: Claude calls a tool, gets a result it doesn't like, calls the same tool again with slightly different input, and repeats. A few guardrails that matter in production:
- Max iteration count. Cap the loop at a reasonable number (10-20 is typical for most workflows) and return a fallback response if exceeded.
- Timeout per tool call. A hanging tool (a slow API, a stuck database query) will stall the whole agent. Wrap tool handlers in timeouts and return an error result Claude can react to.
- Error results, not exceptions. If a tool fails, send back a
tool_resultwith an error message rather than crashing the loop. Claude can often recover — retry with different input, fall back to another tool, or tell the user it couldn't complete the task.
try {
const result = await toolHandlers[call.name](call.input);
return { type: "tool_result", tool_use_id: call.id, content: JSON.stringify(result) };
} catch (err) {
return {
type: "tool_result",
tool_use_id: call.id,
content: `Error: ${err.message}`,
is_error: true,
};
}
Designing Tools for Reliable Multi-Step Behavior
How you define tools affects how well Claude chains them:
- Narrow, single-purpose tools work better than one giant tool with twenty optional parameters. Claude reasons more reliably about
search_ordersandget_order_detailsas separate tools than oneorder_toolwith amodefield. - Descriptive tool descriptions matter more than schema strictness. Tell Claude when to use a tool, not just what parameters it takes.
- Return structured, minimal data. Dumping a full API response as a tool result wastes context and can distract Claude from the actual answer. Trim to what's relevant.
Running This in Production
If you're building this loop against Claude directly, you're managing API keys, retry logic on transient errors, and usage tracking yourself. If you're routing it through SubToAPI, the loop structure is identical — same tool_use/tool_result contract — but you get an sub_live_... application key, streaming support for long-running agent turns, and usage metadata per request so you can see exactly how many tool round-trips a given workflow took. That last part is useful once you're running agents in production: multi-step workflows can quietly use far more tokens than a single-turn chat, and visibility into per-step cost matters. See /docs/quickstart for setup and /pricing for plan details, or /signup to start a free trial.
For streaming agent responses — useful when a step involves a long tool result or final answer — check /docs/streaming.
Questions
Does Claude automatically keep calling tools until a task is done? No. Claude decides when a tool is needed and stops after requesting it. Your application code must run the tool, send the result back, and call Claude again — repeating this loop is what creates multi-step agent behavior.
How do I prevent an agent loop from running forever? Set a maximum iteration count, add timeouts around tool execution, and treat tool failures as tool_result errors (not exceptions) so Claude can react instead of the loop crashing or hanging indefinitely.
Can Claude call multiple tools in one step? Yes. A single response can contain several tool_use blocks when the calls are independent of each other. Your loop should process an array of tool calls per turn, not assume only one.