Skip to main content

Agentic loops

The agentic loop is the core execution pattern of every Claude-powered autonomous agent. Mastering it precisely — not approximately — is required to pass Domain 1.

The loop in detail

┌─────────────────────────────────────────────┐
│ 1. Build messages array │
│ [system prompt, conversation history, │
│ tool definitions] │
└────────────────┬────────────────────────────┘


┌─────────────────────────────────────────────┐
│ 2. Call Claude API │
│ client.messages.create({...}) │
└────────────────┬────────────────────────────┘


┌─────────────────────────────────────────────┐
│ 3. Check stop_reason │
│ │
│ "end_turn" ──────────────────► TERMINATE │
│ "tool_use" ──────────────────► step 4 │
│ "max_tokens" ─────────────────► handle │
└────────────────┬────────────────────────────┘
│ tool_use

┌─────────────────────────────────────────────┐
│ 4. Append assistant message to history │
│ messages.push({role: 'assistant', ...}) │
└────────────────┬────────────────────────────┘


┌─────────────────────────────────────────────┐
│ 5. Execute each tool call │
│ for each tool_use block in response │
└────────────────┬────────────────────────────┘


┌─────────────────────────────────────────────┐
│ 6. Append tool results as user message │
│ messages.push({role: 'user', │
│ content: [{type:'tool_result',...}]}) │
└────────────────┬────────────────────────────┘

└──────────────► back to step 2

Complete Python implementation

import anthropic

client = anthropic.Anthropic()

def run_agent(prompt: str, tools: list) -> str:
messages = [{"role": "user", "content": prompt}]

while True:
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=4096,
tools=tools,
messages=messages
)

# ALWAYS check stop_reason — never check text content
if response.stop_reason == "end_turn":
return response.content[0].text

if response.stop_reason != "tool_use":
# Handle max_tokens, stop_sequence, etc.
raise RuntimeError(f"Unexpected stop_reason: {response.stop_reason}")

# Append Claude's response (including tool call requests) to history
messages.append({"role": "assistant", "content": response.content})

# Execute all requested tool calls
tool_results = []
for block in response.content:
if block.type == "tool_use":
result = execute_tool(block.name, block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": str(result)
})

# Append tool results — loop continues
messages.append({"role": "user", "content": tool_results})

Controlling the loop

Turn budget

# Cap by number of tool-use turns
for attempt in query(prompt=task, options=ClaudeAgentOptions(max_turns=10)):
...

# Cap by cost
for attempt in query(prompt=task, options=ClaudeAgentOptions(max_budget_usd=0.50)):
...

Why max_turns is a safety cap, not a primary stop mechanism

Setting max_turns=10 as your only stop condition is an anti-pattern. The correct primary stop is stop_reason === "end_turn". max_turns is a safety net for runaway loops on open-ended prompts.

Multiple tool calls in one response

Claude can request multiple tool calls in a single response. Your loop must handle all of them before returning results:

# Claude may return multiple tool_use blocks in one response
for block in response.content:
if block.type == "tool_use":
# Execute ALL tool calls before appending results
tool_results.append(execute_tool(block.name, block.input))

# Append ALL results together in one user message
messages.append({"role": "user", "content": tool_results})

The order-of-operations rule

Always append the assistant message BEFORE the tool results. This maintains proper role alternation in the conversation history. The API requires user → assistant → user → assistant... alternation.

# ✅ Correct order
messages.append({"role": "assistant", "content": response.content}) # First
messages.append({"role": "user", "content": tool_results}) # Second

# ❌ Wrong — never skip appending the assistant message
messages.append({"role": "user", "content": tool_results}) # Missing assistant turn!

Official documentation