Agentic Loops
The agentic loop is the execution cycle behind every Claude agent. Master why stop_reason - not text parsing - is the only reliable signal for when to keep going or stop.
An agentic loop is the core execution cycle behind every Claude-based agent. It is deterministic control flow that you define in code - not a prompt trick, not a chatbot turn, and not a retry wrapper. Each pass through the loop calls the Messages API, inspects the model's stop_reason, and decides whether to run tools and continue or to return the final answer. Understanding this cycle precisely is the foundation of the entire CCAR-F exam.
The four-step lifecycle
Every iteration of the loop follows the same four steps. The loop repeats until the model signals it is finished:
- 1Send the request. Call the Messages API with the full conversation history: the system prompt, prior messages, and any tool results appended on previous iterations.
- 2Inspect `stop_reason`. Read the authoritative signal that tells you what to do next.
tool_usemeans Claude wants to call tools;end_turnmeans Claude is done. - 3Handle `tool_use`. Execute the requested tools, append their results to the conversation history as a new message, then resend the updated conversation.
- 4Handle `end_turn`. Extract Claude's final response and present it to the user - the loop terminates.
The stop_reason field is the single deterministic, unambiguous control signal for the loop. Never use text parsing, content-type checks, or iteration caps as your primary stopping mechanism. Treat any value other than end_turn as "not finished - investigate why."
A subtle but critical detail: tool results must be appended to the conversation history. If you execute a tool but forget to add its output back into messages, Claude cannot reason about the new information on the next iteration, and the loop stalls or repeats itself. This is one of the most common production bugs.
Beyond tool_use and end_turn
Production loops must handle more than the two common values. Any non-end_turn result means the turn is not cleanly complete:
| stop_reason | Meaning | How to handle |
|---|---|---|
| tool_use | Claude wants to call one or more tools | Execute tools, append results, continue |
| end_turn | Claude finished its response | Return the final answer, exit loop |
| pause_turn | A long-running server tool is still working | Resend to continue the operation |
| max_tokens | The response hit the token limit | Continue or handle truncation |
| refusal | Model declined (normal 200 response) | Surface refusal, do not retry blindly |
| stop_sequence | A custom stop sequence fired | Handle per your design |
Model-driven vs. pre-configured control
In a model-driven loop, Claude reads the task, weighs the available tools, and selects the appropriate one based on context. This adapts to unforeseen situations and allows flexible tool chaining. A pre-configured decision tree hard-codes the execution sequence - less flexible, but sometimes required for deterministic compliance in financial, security, or regulatory contexts (covered in lesson 1.4). The exam generally favors model-driven approaches for their adaptability.
import anthropic
client = anthropic.Anthropic()
messages = [{"role": "user", "content": "What is 47 * 89, then search for the result?"}]
for _ in range(20): # safety cap only, NOT the primary control
resp = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=1024,
tools=TOOLS,
messages=messages,
)
# The authoritative control signal:
if resp.stop_reason == "end_turn":
print(resp.content[-1].text)
break
if resp.stop_reason == "tool_use":
messages.append({"role": "assistant", "content": resp.content})
tool_results = []
for block in resp.content:
if block.type == "tool_use":
result = run_tool(block.name, block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": result,
})
# Tool results MUST be appended back to history:
messages.append({"role": "user", "content": tool_results})Checking response.content[0].type == "text" to decide the agent is done fails constantly - Claude routinely returns explanatory text ("Let me search your order") alongside a tool_use block in the same response. Seeing text first does not mean the agent finished. Inspect stop_reason instead.
Parsing the model's natural language (or the first content block) to guess whether it finished. Text like "Let me search that" ships alongside a tool_use block, so this stops the loop early or never.
if "done" in resp.content[0].text.lower():
break # brittle: text != completionBranch on `stop_reason`, the one deterministic control signal. end_turn means finished; treat every other value as "not done - investigate."
if resp.stop_reason == "end_turn":
break # authoritativeA cap like max 20 iterations is a fine guard against runaway agents, but it must never be your primary termination logic. If a task legitimately needs 12 steps, a cap of 10 truncates real work; if it finishes in 3, extra iterations waste tokens. Let stop_reason end the loop naturally.
One more trap: forcing tool_choice: "any" on every call. It guarantees Claude always calls a tool, which creates an infinite loop when the agent is genuinely finished and should have returned end_turn.
How the exam will try to trick you
The distractors below look right under time pressure — learn the tell.
- The trap
Decide the loop is finished by checking
response.content[0].type == "text".Correct answerBranch on the
stop_reasonfield - continue ontool_use, terminate onend_turn.Why: Claude routinely returns explanatory text alongside a
tool_useblock, so a text-first response does not mean it finished. - The trap
Use an iteration cap like stop after 10 loops as the primary way to end the loop.
Correct answerLet
stop_reasonend the loop; keep the cap only as a safety net against runaway agents.Why: A fixed cap either truncates legitimate multi-step work or wastes iterations after the task is already done.
- The trap
Parse natural-language phrases like I'm done or task complete to terminate.
Correct answerRely on the deterministic
stop_reasonsignal instead of the model's prose.Why: Natural language is ambiguous; only
stop_reasongives an unambiguous machine-readable completion signal. - The trap
Force
tool_choice: "any"so the agent never returns plain text.Correct answerLet the model signal completion naturally with
end_turn.Why: Forcing a tool call on every turn means the agent can never finish, producing an infinite loop.
Key takeaways
- The agentic loop is a four-step cycle: send request, inspect stop_reason, handle tool_use, handle end_turn.
- stop_reason is the only reliable, deterministic signal for loop control - never parse natural language or check content types.
- Tool results must be appended to conversation history or Claude cannot reason about them next iteration.
- Handle all stop_reason values (pause_turn, max_tokens, refusal, stop_sequence) - treat any non-end_turn value as "not finished."
- Iteration caps are safety nets against runaway agents, never the primary stopping mechanism.
- Forcing tool_choice: "any" causes infinite loops because the agent can never end its turn.
Frequently asked questions
What is an agentic loop in Claude?+
An agentic loop is the deterministic code-level cycle that runs a Claude agent: it calls the Messages API, checks the stop_reason, executes any requested tools and appends their results to the conversation, then repeats until stop_reason is end_turn.
Why should I use stop_reason instead of checking the response text?+
Because stop_reason is deterministic and unambiguous, while natural language is not. Claude often returns text alongside a tool_use block, so phrases like "task complete" or a text-first response do not reliably indicate the agent has finished.
What happens if I forget to append tool results to the conversation?+
Claude has no way to see the tool output on the next iteration, so it cannot reason about the new information. The loop typically stalls, re-requests the same tool, or produces an incomplete answer.