When you tell an AI agent to stop after 20 turns, who does the counting? Loop engineering is the design of the control structure around repeated AI agent runs: what starts each run, the hard limits that end it, the check that proves the work is done, and when a person takes over. In Claude Code, in auto mode, you can type /goal all tests in test/auth pass or stop after 20 turns and walk away. The agent keeps working, turn after turn, without you. After each turn a second, smaller model reads the conversation and decides whether the goal is met. Claude Code's documentation for goals is exact about that "20 turns": Claude reports its progress against the clause, and the evaluator judges it from the conversation. A model reads the limit.
That is a sensible design for an interactive tool, where you are at the keyboard and can press Ctrl+C. In a loop that runs at 2 a.m. with nobody watching, it is not enough. A limit that a model reads is a request. A limit that your code counts is a control. Most of loop engineering follows from that difference.
Loop engineering is deciding, in code, what starts an agent, what proves its work is done, and what stops it when that proof never comes.
Our notes use one stack for agent systems: the harness is everything around the model inside one agent run (tools, sandbox, permissions, state and hooks) and hosts the inner agent loop; the graph orchestrates steps and runs; and loop engineering is the outer layer that starts repeated runs, proves "done" and stops them.
If you read only this:
- The term. Since June 2026, "loop engineering" has meant designing the system that prompts an agent for you: what starts each run, how the result is checked, what carries over, and when a person steps in. The agent loop inside each run is its building block.
- Hard exits. Every run needs limits that code enforces: turns, tokens or spend, and time. The defaults differ a lot: 10 model calls in the OpenAI Agents SDK, 500 in Google's Agent Development Kit (ADK), and no limit in the Claude Agent SDK.
- Done is a claim. When an agent says it has finished, check that outside the agent, in code where you can, and stop the agent from editing the check.
- Retries need new information. Another attempt helps only when it carries new evidence, such as a different failing test. The same failure twice means stop and escalate.
Loop engineering, defined
Addy Osmani named the practice in his post Loop Engineering on 7 June 2026, defining it as "replacing yourself as the person who prompts the agent": you design the system that does the prompting instead. On 30 June Anthropic, in Getting started with loops, described loops as "agents repeating cycles of work until a stop condition is met". A workshop paper by Lulla and colleagues, Loop Engineering: Building Blocks, Adoption, and Impact (JAWs@ASE 2026, revised on arXiv on 22 October 2026), compares it with three earlier practices by the unit each one is concerned with:
| Practice, in the paper's comparison | Unit of concern | Example artifacts |
|---|---|---|
| Prompt engineering | One instruction | Prompts, prompt templates |
| Context engineering | One model call | AGENTS.md, CLAUDE.md, rules |
| Harness engineering | One agent loop | Skills, subagents, hooks, MCP configuration |
| Loop engineering | Repeated agent loops | Schedules, stop conditions, state files, run logs, budgets, verifier agents |
Two loops are involved, and it helps to keep them apart.
The agent loop is the inner one. The same paper defines it as the model's repeated cycle of calling tools and acting on the results until a stop condition ends it. Its research roots are older than the term. ReAct (Yao and colleagues, 2022) had a model interleave reasoning steps with actions such as searches. Anthropic's 2024 essay Building effective agents described agents as "typically just LLMs using tools based on environmental feedback in a loop".
The outer loop is what the new term names: what starts each agent loop, how its result is verified and saved, and when it runs again. Its simplest form appeared in July 2025, when Geoffrey Huntley, in Ralph Wiggum as a "software engineer", described a technique that, "in its purest form", is a Bash loop: while :; do cat PROMPT.md | claude-code ; done. Each pass starts the agent with a fresh context and leaves its progress in the files.
Putting those together, here is the definition this note uses. Loop engineering is the design of the control structure around repeated agent runs: the trigger that starts a run, the hard limits that end it, the check that decides whether the work is done, the state that carries over to the next run, and the point where a person takes over. The inner loop's exits are part of that design, so this note covers both loops.
The term is contested. In a discussion the paper reviews, practitioners called loops "a renamed cron job". On the trigger alone, that is fair: schedules and event hooks are old. What is new is that the worker repeated by the schedule is non-deterministic, so the checks around it carry the weight.
The running example. Picture a nightly loop whose job is to upgrade one outdated dependency in a repository, keep the test suite green and open a pull request. It is a hypothetical, not a client story. It needs every part of the definition: a trigger, exits, a check that can be gamed, state between nights, and a person for upgrades it cannot handle.
The agent loop: one way to continue, three ways to end
Inside one run, your code calls the model, runs any tools it asks for, and calls it again. The Claude Messages API makes the exits explicit. As Anthropic's stop reasons documentation sets out, every response carries a stop_reason, and the installed SDK (anthropic 2.3.0) lists seven values. They fall into four groups.
- Continue.
tool_usemeans the model wants a tool run. This is the only value that should send the loop round again. - Claim.
end_turnmeans the model finished its response. For an agent, that is its claim to be done. - Failure.
max_tokens,refusalandmodel_context_window_exceededmean the response was cut off, declined or truncated. Report these as failures, never as success. - Cap. Your own turn, token and time limits. The API does not know about them; only your loop can enforce them.
Two values sit outside those groups. stop_sequence fires only when you set your own stop strings. pause_turn comes from the API's own server-side tool loop (for tools such as web search), which by default stops after 10 iterations and asks you to send the content back to continue.
Here is the inner loop for the nightly example, with all three caps in code. run_tool is your function that executes a tool call and returns its output.
import time
import anthropic
client = anthropic.Anthropic()
MODEL = "claude-opus-5-5" # placeholder; the current example, Oct 2026
def agent_loop(task, tools, run_tool, max_turns=25, token_budget=400_000, max_seconds=1200):
"""One agent loop. Returns (why_it_stopped, messages)."""
messages, used, start = [{"role": "user", "content": task}], 0, time.monotonic()
for _ in range(max_turns):
if used >= token_budget:
return "token_budget", messages
if time.monotonic() - start >= max_seconds:
return "time_limit", messages
r = client.messages.create(model=MODEL, max_tokens=8000, tools=tools, messages=messages)
used += r.usage.input_tokens + r.usage.output_tokens # with prompt caching, add the cache_* fields too
messages.append({"role": "assistant", "content": r.content})
if r.stop_reason != "tool_use":
# end_turn is a claim of done, not proof. max_tokens, refusal and
# model_context_window_exceeded are failures to report, not successes.
return r.stop_reason, messages
results = [{"type": "tool_result", "tool_use_id": b.id, "content": run_tool(b.name, b.input)}
for b in r.content if b.type == "tool_use"]
messages.append({"role": "user", "content": results})
return "turn_limit", messagesThe function returns a reason, not a verdict. Even end_turn goes to a verifier. Note one subtlety: the budget is checked before each call, so the last call can take spend past token_budget. The Claude Agent SDK's budget cap behaves the same way, as the next section shows.
Hard limits: what "max turns" means in each SDK
Frameworks run the inner loop for you, and each has its own exits. They share a name and little else. The SDK rows were checked against the installed packages, because the documentation is silent on some defaults.
| Framework (version checked) | Setting | What it counts | Default | When the limit is hit |
|---|---|---|---|---|
| Claude Agent SDK 0.2.174, Python | max_turns and max_budget_usd on ClaudeAgentOptions | Turns (each response with tool calls, plus one for the final text-only response); estimated spend in US dollars | No limit | The result message has subtype error_max_turns or error_max_budget_usd; a single-shot query() then raises |
| OpenAI Agents SDK 0.24.0, Python | max_turns on Runner.run | Model calls | 10 | Raises MaxTurnsExceeded, unless an error_handlers entry returns a controlled final output |
| Google ADK 2.13.0, Python | RunConfig.max_llm_calls | Model calls in one run | 500, overridable by the ADK_MAX_LLM_CALLS environment variable; 0 or less means unlimited | Raises LlmCallsLimitExceededError |
Claude Code /goal | A clause in the condition, such as "or stop after 20 turns" | Judged by the evaluator model from the conversation | None | The evaluator judges the clause met and the goal clears |
Three things follow from the table.
Know your default. The same intent, "do not run forever", gives you 10 calls, 500 calls or no ceiling, depending on the import. A limit copied from one SDK to another changes meaning on the way.
Budgets overshoot by design. The Claude Agent SDK compares spend with the cap after each response arrives, so spend can pass the cap by up to one response, plus whatever running subagents spend before they stop. Its documentation says to leave headroom, and recommends a budget as a good default for production agents.
Google's loop primitive has moved. In ADK 2.13.0, LoopAgent, which the ADK loop workflow page describes as repeating sub-agents until max_iterations or until one of them sets escalate, is marked deprecated in favour of graph-based workflows. Graph orchestration is the subject of our graph engineering note. For the agent loop itself, max_llm_calls is the ADK control, and model and tool callbacks are where you add checks. ADK is part of Gemini Enterprise Agent Platform, which Google's Agent Platform overview describes as an evolution of Vertex AI.
Verification: done is a claim, so check it outside the agent
Agents stop early, and they say they are finished when they are not. Anthropic's engineering team, writing about effective harnesses for long-running agents that work across many sessions, described failure modes that included these: Claude "declares victory on the entire project too early", and it marks features as done without proper testing. Asking the same model to re-check itself does not reliably help. Huang and colleagues, in Large Language Models Cannot Self-Correct Reasoning Yet (ICLR 2024), found that models "struggle to self-correct their responses without external feedback", and that performance sometimes falls after they try.
A checker that only rereads its own work is not checking. The fix is to move the decision outside the agent, as high up this ladder as the task allows:
- Code. Tests, a build, a schema check, a diff. It is deterministic, cheap and cannot be talked round. Our note on checking AI-written documentation shows how far deterministic checks can go.
- A separate model with fresh context. Better than self-assessment, but it can only judge what it can see. The
/goalevaluator, for example, does not run commands or read files: it judges only what Claude has already shown in the conversation. So the condition must name evidence that appears in the transcript, such as "npm test exits 0". - A person. For work whose correctness neither code nor a model can see.
Then protect the check from the agent. An agent asked to make the tests pass has an easy route: change the tests. Anthropic's long-running harness meets a version of this problem with a prompt: it tells its agent that removing or editing the tests in its feature list is unacceptable. Separately, the /goal documentation suggests adding constraints such as "no other test file is modified". A prompt is still a request. In the nightly example, the gate itself rejects any run that changed, committed or added anything under tests/ or a root conftest.py, compared with the commit the run started from. Pytest settings in pyproject.toml are one more route, so review that diff too.
Retries and reflection: only with new evidence
Reflection, a model critiquing its own output and trying again, is the other half of many loops. The research behind it is specific about where the gain comes from. Reflexion (Shinn and colleagues, 2023) had agents keep reflective text in an "episodic memory buffer" between attempts, driven by feedback that could be "scalar values or free-form language", from "external or internally simulated" sources. Huang's result is the other side of it. They studied intrinsic self-correction on reasoning tasks, with no signal from outside the model, and there the extra attempts did not reliably help.
So the rule for the outer loop is simple. Give the next attempt the evidence from the last one, such as the failing test names and assertion messages. Stop when the evidence stops changing. Claude Code applies a version of this to /goal: if Claude keeps answering the evaluator without using any tools for several turns, it stops the loop and hands control back to you.
How you compare the evidence matters. When we tested the nightly snippet below against a real repository, comparing pytest's raw output never detected a stall, because the output includes the run time and so differs on every run. Compare a fingerprint of the failure, such as the failing test IDs and messages, not the log.
The outer loop: trigger, state and escalation
The outer loop for the nightly example fits on one screen. agent_loop is the function above; TOOLS and run_tool are your tool definitions and executor.
import json, pathlib, subprocess
STATE = pathlib.Path("loop-state.json") # the agent forgets; the repo keeps this
GUARDED = ["tests/", "conftest.py"] # where an agent could weaken the check
def git(*args):
return subprocess.run(["git", *args, "--", *GUARDED], capture_output=True, text=True).stdout
def verified(base):
"""The gate runs in code: the tests pass AND nothing guarded changed since `base`."""
tests = subprocess.run(["pytest", "-q", "-rfE"], capture_output=True, text=True)
# Compare with the starting commit, not HEAD, so a committed test edit counts; list new files too
# (git skips ignored files, so keep __pycache__/ in .gitignore or pytest's own cache trips the gate).
untouched = not git("diff", "--name-only", base) and not git("status", "--porcelain", "--untracked-files=all")
evidence = [l for l in tests.stdout.splitlines() if l.startswith(("FAILED", "ERROR"))]
if not untouched:
evidence.append("REJECTED: test files were changed or added")
return tests.returncode == 0 and untouched, "\n".join(evidence)[-1500:]
def nightly_run(max_attempts=3):
state = json.loads(STATE.read_text()) if STATE.exists() else {"log": []}
base = subprocess.run(["git", "rev-parse", "HEAD"], capture_output=True, text=True).stdout.strip()
for attempt in range(max_attempts):
recent = "\n".join(e["evidence"] for e in state["log"][-2:])
task = f"Upgrade one outdated dependency and keep the tests green.\nLast results:\n{recent}"
why, _ = agent_loop(task, TOOLS, run_tool)
ok, evidence = verified(base)
state["log"].append({"attempt": attempt, "agent_said": why, "passed": ok, "evidence": evidence})
STATE.write_text(json.dumps(state, indent=2))
if ok:
return "verified" # commit, open a pull request, a person reviews it
if attempt and evidence == state["log"][-2]["evidence"]:
break # no progress: the same failure twice in a row
return "escalate" # a person decides, with the log attachedEach part maps to one piece of the definition.
- Trigger. A scheduler calls
nightly_run: a CI cron job, or in Claude Code a cloud scheduled task created with/schedule, which runs in the cloud, or/loop, which reruns a prompt on an interval only while the session stays open, for up to seven days. - Hard limits. The worst case for one night is known before it runs: three attempts, each capped at 400,000 tokens, so 1.2 million tokens plus at most one model call per attempt, whose input can be a full context window.
- The check.
verified()decides, not the agent. What the agent said is logged next to the result, so you can see how often its claims were wrong. - State. Each attempt starts with fresh context and reads the last results from disk. Osmani's point is that a loop's memory has to live on disk, not in the context window. Anthropic's long-running harness does the same with a progress file and a JSON list of features, each with a
passesfield. Within a single long run, the Claude Agent SDK compacts older history automatically, which is why persistent rules belong in a file such as CLAUDE.md rather than in the first prompt. - Escalation. When the loop gives up, a person gets the log. Deciding which actions need a person at all is the subject of our note on autonomy tiers and human approval.
Review capacity is a limit too. A loop that opens pull requests faster than people can read them turns the human gate into a rubber stamp. Practitioners in the paper's review raised exactly this. Size the loop to the reviewers you have.
What the evidence shows so far
The practice is spreading, but the evidence is thin. Lulla and colleagues' loop engineering paper (arXiv version 3, 22 October 2026, section 4.1) started from 36,710 actively maintained open-source GitHub repositories, drawn in March 2026 from an earlier dataset of AI coding tool configurations. On 19 August 2026 they searched each one's committed files, and the history of candidate files, for loop artifacts. After removing 65 repositories that were no longer reachable and 158 duplicates, 36,487 were analysed. The searches pointed to 256 repositories, which the authors inspected for evidence in archived GitHub Actions runs and agent output that an autonomous agent loop had actually run. They confirmed loops in 217, or 0.59% of the repositories analysed. So the study measured committed loop configuration plus signs of execution, for eight coding tools, in one sample. The authors say the results "do not establish prevalence across all open-source software".
Claude Code was the tool in 189 of the 217. Grouped by the trigger behind most of their archived runs, 180 ran on repository events (mostly automatic pull-request reviews), 21 on a schedule and 15 on both; the remaining one ran until its GitHub Actions workflow broke. Nearly every one committed the configuration that starts the loop. The file searches found no committed loop state files on the default branches scanned. In the 256 inspected repositories, annotations (read from workflow definitions, prompts and run histories) recorded state carried across runs in 35, a goal or stop condition in 55, a spending or turn limit in 101, a verifier in 76 and a cost record in 10. Those annotations were made with an AI coding tool, and the loop labels were checked by one author, so treat the figures as indicative. In their review of the practitioner literature, the authors found the claimed benefits large but based on self-reports and anecdotes.
Honest limits
- No benchmark for your loop. The research results cited here (ReAct in 2022, Reflexion in 2023) were measured on earlier models and tasks. They support the design principles, not any number for your system. Measure task success and cost on your own runs.
- A gate is only as good as what it checks. A loop optimises for the gate you wrote, not the goal you meant. Tests that do not cover a behaviour will pass without it.
- Caps are blunt. A turn cap can cut off a run one step from success. Keep the session or state so a person, or a later run with a larger budget, can resume rather than restart.
- Not every task has a checkable end. Where neither code nor a second model can see whether the work is right, the loop reduces to an agent preparing work for a person. That is still useful; it is just not unattended.
- What we tested. We ran the outer loop's gate and stall logic against a real Git repository and pytest, with a scripted stand-in for the model. We checked every SDK name above against the installed packages. We did not call a live model for this note.
Takeaways
- Loop engineering designs the system around agent runs: trigger, hard limits, check, state and escalation.
- Count limits in code. A limit a model reads is a request; check what your SDK's default actually counts.
- Treat "done" as a claim. Verify it outside the agent, with code where possible, and protect the check from the agent.
- Retry only with new evidence, and compare a fingerprint of the failure, not the log.
- Size the loop to its reviewers. Unread pull requests are not oversight.
Reread-me checklist
- Write the stop condition as something code can check, and name the evidence it needs.
- Set turn, token or spend, and time limits in code, and look up your SDK's default.
- Make the verifier separate from the agent, and make it reject edits to the verifier's own inputs.
- Pass the last attempt's evidence to the next one, and stop when that evidence repeats.
- Keep state on disk, log what the agent claimed next to what the check found, and compute the worst-case spend per trigger.
- Name the person who receives escalations, and check they have time to review what the loop produces.
What's next
Loops that branch, wait for approval or resume after a crash need explicit orchestration: graphs, checkpoints and durable state, which our note on graph engineering covers. Before reaching for it, make one linear loop trustworthy: one trigger, one gate, one log.
Here is the one thing to do this week. Take a loop you already run, write down its exit condition, and then write down how a run could satisfy that condition without doing the work. If you would like a second pair of eyes on the answer, send it to us.
Further reading
- Loop Engineering: Building Blocks, Adoption, and Impact, Lulla et al., arXiv: The first study of loops in real repositories, with the layer comparison above and an honest account of what its numbers can and cannot show.
- Effective harnesses for long-running agents, Anthropic: A worked design for agents that span many sessions, including the progress file, the feature list and the early "victory" failure mode.
- Large Language Models Cannot Self-Correct Reasoning Yet, Huang et al., ICLR 2024: The evidence behind moving the verifier outside the agent, and why a retry needs feedback from outside the model.
- Loop Engineering, Addy Osmani: The short post that named the practice, and a clear case for keeping a loop's memory on disk.
- LLM rate limits, retries and switching models, AELSHA™ NEHEMVIX: When an agent should wait, retry, switch to a fallback model or pause, decided in code from real 429 responses.
SDK defaults, limits and loop commands change from release to release, so check your installed version and the vendor's own documentation before you rely on them.
