Loop Engineering for Agentic Development: Rules, Patterns, and a Worked Example
An AI agent is not a single call to a model — it is a loop. The model observes, decides, acts with a tool, sees the result, and goes again until the job is done. Loop engineering is the discipline of designing and tuning that cycle so it converges on a correct answer instead of drifting, spinning forever, or confidently declaring a wrong result "done." This guide walks the full picture: what the agent loop is, the rules that keep it healthy, the patterns you compose from those rules, when to express a loop as a prompt versus as code, and a fully worked example that maps every design choice back to the rule it satisfies.
What the Agent Loop Actually Is
At its core, every agent runs the same cycle — often called the ReAct loop (Reason + Act):
observe → think → act → observe result → repeat
Each turn, the model sees the current state, chooses a tool, runs it, and feeds the result back into the next turn. The loop continues until the task is done or a stop condition fires. Here is the entire spine in Python against the Claude API — everything else in this article is tuning layered on top of it:
import anthropic
client = anthropic.Anthropic()
TOOLS = [{
"name": "run_bash",
"description": "Run a shell command, return stdout+stderr.",
"input_schema": {
"type": "object",
"properties": {"cmd": {"type": "string"}},
"required": ["cmd"],
},
}]
def run_bash(cmd: str) -> str:
import subprocess
r = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=60)
return (r.stdout + r.stderr)[:4000] # truncate — context is a budget
def agent_loop(task: str, max_steps: int = 25):
messages = [{"role": "user", "content": task}]
for step in range(max_steps): # stop condition #1: step cap
resp = client.messages.create(
model="claude-opus-4-8",
max_tokens=4096,
tools=TOOLS,
messages=messages,
)
messages.append({"role": "assistant", "content": resp.content})
if resp.stop_reason != "tool_use": # stop condition #2: model is done
return "".join(b.text for b in resp.content if b.type == "text")
results = []
for block in resp.content:
if block.type == "tool_use":
try:
out = run_bash(**block.input)
except Exception as e:
out = f"ERROR: {e}" # error recovery — don't crash the loop
results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": out,
})
messages.append({"role": "user", "content": results})
raise RuntimeError("hit max_steps — no convergence") # stop condition #3
Four lines already carry most of the engineering: max_steps prevents an infinite spin, stop_reason != "tool_use" lets the model signal completion, [:4000] protects the context window, and the try/except turns a tool failure into feedback the model can recover from rather than a crash. Everything below is about doing these four things well and adding the pieces a toy loop leaves out.
The key mental model: agent quality is loop quality, not just model intelligence. The same model wrapped in a better loop produces dramatically better results.
The 12 Rules of Loop Engineering
These are the constraints that separate a robust loop from one that burns tokens and lies about being finished.
1. Always define a stop condition
Every loop must terminate. Provide at least three exits: success (the goal is verifiably met), cap (a maximum number of rounds or steps), and budget (a ceiling on tokens or time). A loop with no stop condition will spin forever — this is the single most common failure.
2. Feed the loop a ground-truth signal
A loop converges on feedback. Real signals — a compiler error, a failing test, a linter, a diff — beat the model's own opinion of its work. With no external signal, the loop drifts and eventually hallucinates that it is done.
3. Each round must converge
Round N should be measurably better than round N-1. Track progress. If the loop oscillates (fixing A breaks B, fixing B breaks A), detect it and break out. When improvement flatlines, that diminishing return is itself a stop condition.
4. Verify before you trust "done"
The model claiming success is not the same as success. Gate completion behind a check. A separate critic beats self-checking, because a model shares its own blind spots. Make verification adversarial: prompt the checker to refute the result, not confirm it.
5. Treat context as a budget, not a dump
Curate what enters the window each round. Prune stale tool output, summarize when the window fills, and offload durable state to disk (a todo.md, a notes file). Spawn a sub-agent to absorb a noisy subtask and hand back one clean answer.
6. Failures feed back — they never crash
A tool error should be caught, formatted as text, and returned to the model so it can re-plan. One failed call must not kill the loop.
7. Separate the actor from the critic
Whoever does the work should not be the one who judges it. A model grading its own output is biased toward passing. Split the roles.
8. Isolate noisy subtasks
A wide search or a large file dump belongs in a sub-agent whose context absorbs the mess. The main loop stays lean, which protects convergence and keeps the window affordable.
9. Prefer a pipeline over a barrier
When processing many items, let each flow through the stages independently. Only introduce a barrier (wait for all results) when a downstream stage genuinely needs every upstream result at once — deduplication, a global count, an early exit. Otherwise a barrier wastes wall-clock time.
10. Deduplicate across rounds
If the loop rediscovers the same issue every round, the count never drops and it never converges. Keep a seen set and dedupe against everything seen so far, not just the current round.
11. Log what you dropped
If a loop bounds coverage — top-N only, sampling, no retries — say so. Silent truncation reads as "covered everything" when it did not.
12. Scale the loop to the task
A small task deserves a tight loop with a couple of agents. A large audit, migration, or research task justifies a wide fan-out and several verification votes. Do not over-engineer the trivial, and do not under-engineer the hard.
The one-line core: terminate, converge, verify, curate. Miss any one and the loop fails in a predictable way.
Patterns You Compose From the Rules
The rules are primitives. Real loops combine them into recognizable shapes.
| Pattern | What it does | Use when |
|---|---|---|
| Single loop | One agent grinds until done | Simple, linear task |
| Fan-out (parallel) | Split independent work across agents, run concurrently, merge | Wide coverage — e.g. review 10 files with 10 agents |
| Pipeline | Each item flows through stages with no barrier between them | Multi-stage work where items are independent |
| Plan-then-execute | Plan first, then loop over each step | Task benefits from an upfront decomposition |
| Judge panel | N agents solve the same problem from different angles; judges score; synthesize from the winner | The solution space is wide |
| Loop-until-dry | Keep spawning finders until K consecutive rounds surface nothing new | Unknown-size discovery (bugs, edge cases) |
| Actor–critic loop | Act → a separate critic checks → redo if it fails | Output quality matters and can be judged |
Two verification patterns are worth calling out because they defend against the subtlest failure — a loop that converges on a stable wrong answer:
- Adversarial verify: spawn several independent skeptics per finding, each told to refute it, and kill the finding if a majority succeed.
- Perspective-diverse verify: give each verifier a different lens (correctness, security, does-it-reproduce). Redundant checkers share blind spots; diverse checkers do not.
Prompt or Code? You Rarely Need to Write a Loop by Hand
Loop engineering is a set of ideas, not a language. You can express a fully engineered loop in plain English — the rules simply become instructions. In a harness like Claude Code, describing the loop in a prompt is enough for the great majority of tasks; the orchestration is handled for you.
| Prompt (English) | Code (workflow script) | |
|---|---|---|
| Effort | Low — just describe it | Write and debug a script |
| Control | The harness infers structure | You pin the exact flow |
| Determinism | Varies run to run | Same every run |
| Scale | A handful of agents | Dozens, with a concurrency cap |
| Resume | No | Yes, cached |
| Best for | Most tasks, one-offs | Migrations, audits, repeatable production jobs |
The rule of thumb: reach for a prompt first. Drop to code only when you need an exactly repeatable flow, a large fan-out, resume-after-crash, or a job you will run on a schedule.
A "loop command" is not loop engineering. Some tools ship a scheduling primitive — for example, Claude Code's
/loopcommand re-runs a prompt on a fixed interval (/loop 5m /review) or lets the model self-pace. That is a time loop: repeat the same task every N minutes, typically to poll something that changes on its own (a CI run, a deploy, a queue). Loop engineering is a different axis entirely — it is about the convergence cycle inside a single task: observe → act → verify → repeat until the work is correct. A scheduled command repeats because the clock ticked; an engineered loop repeats because the result is not good enough yet. You can run an engineered loop with no scheduler at all, and you can schedule a command that contains no engineered loop. Don't conflate the two.
A Worked Example: A UI/UX Polish Loop
Consider a common request: "Act as a senior UI/UX reviewer — look at the pages, and where something is wrong or could be polished, report it to a senior frontend engineer to fix. Then review again and report again, polishing further..."
That describes a loop, but it is not yet engineered. As written it has no stop condition ("polishing further" is infinite), a vague quality signal ("polish"), no deduplication (it will re-report fixed issues every round), and no verification (it trusts the engineer's claim of "fixed"). Here is the same idea written as a hardened prompt, followed by a table mapping each clause to the rule it enforces.
Act as a SENIOR UI/UX reviewer auditing these pages: home.html, checkout.html.
Work with a SENIOR FRONTEND ENGINEER (separate role) who applies fixes.
LOOP each round:
1. REVIEW — find concrete issues only. For each: cite the selector/element,
the exact problem, and why it hurts UX (accessibility, hierarchy, spacing,
contrast, responsiveness). No vague "make it nicer" — every issue must be
verifiable against a standard or a specific broken behavior.
2. FIX — hand each NEW issue to the FE engineer. They edit the file, return the diff.
3. VERIFY — a separate check on each fix: actively TRY to prove the fix did NOT
work. Default to "NOT fixed" if uncertain. Mark fixed only if genuinely resolved.
Also confirm the fix did not break another page or element.
4. Record: issues closed this round, issues still open, any fix that broke
something else.
STOP when ANY of these hit:
- No new issues found in a full review round (converged), OR
- 3 rounds completed, OR
- Issue count stops dropping round-over-round (diminishing return — stop, report why).
GUARDS:
- Don't re-report issues already fixed and verified. Track what's closed.
- If fixing issue A re-opens or breaks issue B (oscillation), STOP that pair,
flag it for a human decision — do not loop A<->B.
- Never claim "all done" unless the final review round found zero issues.
OUTPUT each round: fixed (verified) | still open | broke-something | round number.
FINAL: total closed, total still open, any oscillations flagged, why the loop stopped.
| Clause in the prompt | Rule it enforces | Why it matters |
|---|---|---|
| "senior UI/UX reviewer" + named pages | R7 actor≠critic, scope bound | Sets a distinct critic role and stops scope creep |
| "cite the selector… no vague 'make it nicer'" | R2 ground truth | Forces measurable issues so the loop can tell if it improved |
| "hand each NEW issue to the FE engineer" | R7 + R8 | Separate fixer role; each issue is an isolated unit |
| "actively TRY to prove the fix did NOT work" | R4 adversarial verify | Catches fake or partial fixes instead of trusting a claim |
| "confirm the fix did not break another page" | R3 regression check | Prevents progress in one place from causing damage elsewhere |
| "STOP when… 3 rounds / no new issues / count stops dropping" | R1 + R3 | Three real exits, including diminishing-return |
| "don't re-report issues already fixed" | R10 dedupe | Keeps the issue count meaningful so convergence is visible |
| "if A breaks B… do not loop A<->B" | R3 oscillation guard | Breaks the sneaky infinite loop a round cap alone won't catch |
| "never claim 'all done' unless… zero issues" + final audit | R11 honest reporting | Reports remaining work truthfully instead of faking completion |
The engineered version is the same idea as the naive request — the engineering is entirely in the guards that were missing.
The Non-Obvious Parts
The rules keep a loop healthy; these are the judgment calls, failure modes, and operational realities that are easy to overlook even once you know the rules. They fall into three groups.
Judgment: whether and how tightly to loop
- Know when not to loop. For a genuine one-shot task, or one with no verifiable signal, a loop only adds cost, latency, and spin risk. Loop only when iteration genuinely improves the result and you can measure the improvement.
- Keep a human in the loop. Irreversible actions (deleting data, deploying, spending money) and genuinely ambiguous judgments ("is this good enough?") should pause and escalate rather than push through.
- Tune the loop granularity. Verifying after every tiny step is slow and expensive; verifying only at the very end lets the loop drift far before you catch a problem. Match the checking cadence to how risky each step is — cheap, reversible steps can batch; expensive or irreversible ones deserve their own gate.
Failure modes to defend against
- Garbage in, polished garbage out. A loop is only as good as its round-one input. A wrong goal or wrong target gets refined into a confident wrong answer. Fix the seed before you trust the loop.
- State poisoning compounds across rounds. A bad or hallucinated output in an early round becomes the input to the next round, where it compounds. This is why verification belongs before state carries forward, not only at the end — catch the corruption before it propagates.
- Convergence is not correctness. A loop can settle on a stable wrong answer, and redundant verifiers that share a blind spot will happily agree on it. Diverse verification lenses are the defense.
- Guard against prompt injection. A loop that reads external content (web pages, user files) has an injection surface that grows with its length. Treat fetched content as data, never as instructions, and never let it rewrite the loop's own rules.
Operating the loop
- Watch cost per round, not just total. Log what each round spends and does. In a healthy loop the cost per round falls as it converges — rising or flat cost across rounds is a signal the loop is thrashing, not progressing. A blind loop is impossible to debug once it starts to spin.
- Expect non-determinism. The same prompt can produce a different result on each run, because the model is stochastic. This matters for reproducibility, testing, and explaining "why was today's run different." A coded workflow with resume is more repeatable than a pure prompt; if you need determinism, lean toward code and pin what you can.
- Degrade gracefully. When the cap is hit but the goal is not met, return the best result so far plus an honest list of what remains — not an empty failure.
Takeaways
- An agent is a loop; loop quality, not raw model intelligence, is what you engineer.
- The four things that matter most: terminate, converge, verify, curate.
- You almost never need to write loop code — a prompt that spells out the stop condition, the ground-truth signal, the verification step, and the deduplication is loop engineering.
- The difference between a naive "keep polishing" request and a production-grade loop is entirely in the guards: an explicit stop, an adversarial verifier, cross-round dedup, an oscillation breaker, and an honest final report.