Building agents with Claude: the agentic loop
Almost every architecture question about building an agent with Claude, or any tool-using model, reduces to a loop: a literal one, with a stopping condition, an execution boundary, and failure modes if you get either wrong. This guide covers what's in the loop, the decisions it forces, and where it breaks in practice.
1. What makes it agentic
A single API call to Claude (send a prompt, get a completion) is a function call: input in, text out, nothing in the world changed except what the caller does with the string. A system is agentic when the model's output can trigger an action (a tool call, a file edit, a web request, a database write) and the result of that action feeds into the next model call, forming a loop that runs until some condition says stop.
That feedback path is the whole distinction. A chatbot that calls a search tool once and writes an answer from whatever came back has one action and no chance to react to how it went. An agent might search, see the results don't answer the question, reformulate, search again, fetch a promising page, get a 404, try a cached version, and only then answer. No single step is exotic. Each step's outcome is visible to the model before it picks the next one.
This changes what you build. A single-shot integration is a prompt template and a response parser. An agentic system is a loop with state, a set of permitted actions, a way to detect when it's finished, and a way to stop it when it isn't finishing on its own. Most engineering effort in a production agent goes into the harness around the model, especially that last concern. Producing a good tool call is the part the model is generally reliable at.
2. Anatomy of one loop iteration
Strip away framework vocabulary and every iteration does the same four things in the same order:
- Assemble context. The model receives the conversation so far (the original task, every prior tool call and its result, any intermediate reasoning) plus definitions of the tools it may currently use.
- The model decides. Claude either responds directly (it can answer, or the task is done) or requests one or more tool calls, each naming a tool and its argument values.
- The harness executes. Your code runs the requested tool against the real system: it hits the API, queries the database, writes the file. The model never touches the outside world; it only asks your code to.
- The result rejoins the context. The tool's output is appended to the conversation as a tool result, and the loop returns to step 1.
Tool-using APIs share this message shape: the model's turn includes a tool-use request naming a tool and its input parameters, and the next turn is a tool-result message carrying what your code got back, correlated to that request. Field names vary by API and version. Design around the stable part: name, arguments, matching result.
# pseudocode — illustrative shape, not a specific SDK's exact syntax
loop:
response = call_model(conversation, tools=available_tools)
if response.stop_reason == "end_turn":
return response.text # model chose to respond directly — done
for each tool_use in response.tool_calls:
result = execute_tool(tool_use.name, tool_use.input) # your code, not the model
conversation.append(tool_result(tool_use.id, result))
# loop continues with the new tool results now in context
Two things here are easy to get wrong. First, the model can request several tool calls in one turn (see parallel versus sequential execution in the companion guide on tool use), and the harness must return a result for every one of them before the next model call. Second, "the model chose to respond directly" and "the task is finished" are different facts. The next section covers why.
3. Stopping conditions
The model's "I'm done" signal is necessary but not sufficient to end the loop. You need it so the model can conclude a task and hand back an answer; a loop that only stops on an external timeout is broken. But if it's the only check, the model's judgment of its own progress is all that separates a working agent from a runaway one, and that judgment fails in ways specific to agentic execution.
The most common failure in practice is the model stuck retrying variations of the same failing action. A tool call errors, the model reasonably retries with slightly different arguments, that fails too, and nothing structural prevents a dozen more attempts at the same broken call. None of it triggers the model's "I'm done" logic, because from its perspective each attempt is plausible progress.
A working stopping condition layers a few checks on top of the model's signal:
- A hard iteration cap. The loop stops after N iterations regardless of what the model says. This backstop makes every other failure mode bounded.
- A wall-clock timeout. Separate from the iteration count, because a handful of slow tool calls can exhaust a time budget long before an iteration budget.
- Repetition detection. Compare the current tool call (name plus arguments, or a normalized form) against recent calls in the session. Treat a near-exact repeat, especially of a call that already failed, as a signal to stop and escalate.
- A resource or cost ceiling. Total tokens, total tool calls, or total dollars of API and downstream-tool cost, whichever the deployment needs to bound.
Design note: put these checks in the harness, not the prompt. The model will generally try to honor "stop after a few tries if something keeps failing," but that instruction is advisory. The model has no iteration counter unless your code passes one in, and a long or unusual trajectory can slip past a check that exists only as text. Make the hard cap a loop condition in code that model output cannot override.
Decide what happens when a stopping condition fires. Hitting the iteration cap is a different outcome from the model finishing, and the caller (a user or downstream system) must be able to tell them apart: one is a completed result, the other is incomplete and needs a human to read the transcript and decide what's next.
4. Tool design inside the loop
The loop is only as good as the tools it chooses between. This site's guide on the ten disciplines of governed agentic DevSecOps covers tool design in depth for coding agents; its discipline 5, tool design, applies directly to general-purpose loops too. Narrow, well-typed tools with predictable output beat a single do-anything tool. A tool with a clear purpose and constrained arguments is easier for the model to select correctly, easier for a reviewer to audit from the execution log, and easier to wrap in authorization logic that doesn't have to reason about an unbounded space of requests.
Inside a loop, narrow tools also make stopping conditions tractable. "The model is retrying the same failing action" is a well-defined comparison (same tool name, same or near-identical arguments) when tools are narrow and typed. With a generic command executor, "the same action" might appear as different command strings that do the same thing, or similar strings that don't.
5. Human-in-the-loop gates
Some tool calls should run the moment the model requests them; others need a human sign-off first. Decide which is which deliberately instead of defaulting to either extreme.
A loop with no gates is fast, and for low-consequence actions (reading a file, querying a read-only API, searching a knowledge base) that speed is the point. Gating a read adds no safety and makes the agent harder to use. A loop that gates everything fails the other way: an approver facing a steady stream of low-stakes requests learns to click approve without reading, and the gate stops doing anything.
Draw the boundary by consequence and reversibility, regardless of which system the tool touches. A call that's easy to undo (most reads, most staging writes, anything with a working rollback) can run without a pause. A call that's expensive or impossible to reverse (a production deploy, an irreversible delete, an external communication, a financial transaction) is where approval earns its cost, because catching a mistake beforehand is worth most where undoing it afterward is hardest.
Where the gate sits matters as much as what it covers. Pause after the model has chosen a specific action with specific arguments: the harness intercepts the tool call before executing it and shows the reviewer the exact proposed call. Approving earlier, before there's a concrete action, gives the reviewer nothing to evaluate. By the time a human weighs in, the loop should have narrowed "what should happen" to one inspectable call.
6. Observability inside the loop
A transcript of what the model said (its reasoning, its answer, the text of its tool requests) is a different record from what happened when those tool calls executed. Early agent deployments often conflate the two, and the gap shows up when something has gone wrong and someone needs to reconstruct what the agent did.
The transcript can show the model saying it looked something up and got a particular answer. It doesn't show which tool ran, with which exact arguments, against which system, returning which raw result, at what time, in which iteration. Those facts live in the tool execution. An incident review shouldn't have to take the model's description on faith, however accurate it usually is.
An agent loop needs its own execution log, parallel to the transcript: one row per tool call with the tool name, the exact arguments requested, the exact result returned, the timestamp, the iteration number, and which stopping condition or approval gate fired and why. That log answers "what did the agent do" without relying on the model's account of itself, and it's the data the repetition and cost checks in section 3 run on.
7. Worked example
Task: "Find last month's AWS spend for the staging account, compare it to the prior month, and if it's up more than 15%, open a ticket summarizing the likely cause."
Iteration 1. Context: the task and three tool definitions, get_cost_report(account, month), list_recent_resources(account, month), and create_ticket(title, body). The model has no data, so it requests get_cost_report(account="staging", month="2026-08"). The harness runs it against the billing API and appends the result, a total dollar figure.
Iteration 2. With August's total in context, the model requests July for comparison: get_cost_report(account="staging", month="2026-07"). The two calls are independent, so a harness with parallel tool execution could have issued both in iteration 1. Whether that optimization is worth the added complexity is a judgment call.
Iteration 3. With both totals, the model computes the increase (say 22%). It crosses the 15% threshold, so the model requests list_recent_resources(account="staging", month="2026-08") to find a cause before writing the ticket. A single-shot call could never take this step: the decision to look for a cause exists only because the model saw the comparison first.
Iteration 4. The resource list shows a new, unusually large instance type that appeared mid-month. The model requests create_ticket(title="Staging AWS spend up 22% — new instance type detected", body=...). This is the kind of action section 5 says to gate: a side effect other people see, and cheap to review. A well-designed loop pauses here, shows the reviewer the exact ticket text and the data behind it, and executes only after approval.
Iteration 5. After approval (or immediately, in an ungated version), the harness creates the ticket, appends the ticket ID, and the model responds with a final summary. Its end-turn signal now matches what happened, because every fact in the summary traces to a tool result recorded independently in the execution log.
The hard cap from section 3 matters if things go differently. If get_cost_report had failed with a transient error at iteration 1, a naive retry loop could spend five or six iterations re-requesting the same call before reaching the comparison step. A repetition check catches that pattern well before a cap tuned for a five-iteration task would fire.
For the tool-level decisions this example skips (writing tool descriptions that steer which call the model picks, handling a tool call that fails), see Claude tool use and function calling architecture.