The Messages API is stateless: every call is a self-contained request with no memory of previous calls. An agentic loop is the deterministic control flow you write in your own code that turns that stateless API into something that behaves like an autonomous agent - send a request, inspect what came back, act on it, and send again. It is not a prompting technique, not a retry wrapper, and not "keep chatting until the user stops." It is a loop with an explicit exit condition your code enforces.
The lifecycle, step by step
Four steps, repeated until the model signals it is finished:
- Send a request. The request includes the full conversation so far - system prompt, prior user/assistant turns, and any tool results already produced. Nothing is remembered server-side between calls; if it isn't in the
messagesarray you send, the model cannot see it. - Inspect
stop_reason. This field, not the content of the response, is what tells your code what to do next. - If the model requested tools, execute them and append the results. The assistant's response - including its
tool_useblocks - becomes a new assistant message in history. Your tool execution results become a new user message containing onetool_resultblock per tool call, matched bytool_use_id. Both must be appended before you call the API again. - If the model is finished, stop and surface the result. No more tool execution, no more calls - hand the final text to whatever consumes it.
Step 3 is where most home-grown loops break in practice. If you execute a tool but forget to append its result - or you append it in the wrong shape, or with a tool_use_id that doesn't match - the next call to the API will either fail outright or, worse, silently give the model no way to know what its own tool call returned. From the model's perspective in that second case, it asked a question and got nothing back.
Every stop_reason value, and how to handle it
A loop that only branches on tool_use vs. everything-else will misbehave the first time it hits a value it wasn't built for. The values you need to plan for:
end_turn- the model is genuinely done. This is the only value that means "stop the loop and show the result."tool_use- the model wants one or more tools executed. Continue the loop.max_tokens- the response was cut off because it hit the token limit you set. The content you have is a truncated partial answer, not a finished one. Handling it asend_turnmeans shipping a mid-sentence answer to a user.model_context_window_exceeded- the same truncation problem, but caused by the conversation filling the model's context window rather than yourmax_tokenssetting. Needs the same treatment asmax_tokens: don't display it as complete, and address the underlying context growth (Lesson 3.4).stop_sequence- the model hit a custom stop sequence you configured. This is an intentional, controlled stop, not an error, but it's also not the same signal as the model deciding on its own that it's finished.pause_turn- returned during long-running turns involving certain server-side tools. The correct handling is to simply continue the turn by sending the conversation back as-is; treating it as an error or as completion both produce wrong behavior.refusal- the model declined to continue on an otherwise normal, successful response. This is not truncation and not completion - surface it distinctly (e.g., to a human reviewer or a fallback path) rather than silently retrying or showing it as a normal answer.
Common exam distractor
Checking for the presence of text content, or pattern-matching phrases like "I'm done" or "task complete," as your termination signal is always wrong - on the exam and in production. Claude can emit explanatory text ("Let me check that order for you") in the very same response as a tool_use block requesting a tool call. A loop that sees text at content[0] and calls it finished will cut the task off mid-stream. Only stop_reason tells you what happens next; content type and content text are not reliable signals for anything about loop control.
Parallel tool calls and the bookkeeping they require
A single response with stop_reason: "tool_use" can contain more than one tool_use block - the model is allowed to request several independent tool calls in one turn instead of one at a time. Your loop has to execute all of them, not just the first, and it has to return exactly one tool_result block per tool_use_id in the follow-up user message. Skip one, and the next API call will error because the model is still waiting on a result it never got back. The order the results appear in doesn't need to match the order the calls were requested in, but every id must be accounted for.
Key concept
stop_reason is the only reliable, deterministic signal for loop control. A fixed iteration cap is a legitimate safety net against a runaway loop, but it is never the primary way the loop decides it's finished - that decision belongs to stop_reason alone.