Seven Agent Loop Patterns and When Each One Fails: ReAct, Maker-Checker and More
A practical catalog of agent loop designs, from plan-act-verify to human escalation, with guidance on when each fits, when to avoid it, and how to choose.
This article is published in English.
Sources: Agentic Loops & Design Patterns
Nobody should judge an agent by how clever it sounds. What counts is behavior when traffic is heavy, when something breaks and when an auditor comes asking. That is the ground where loop design patterns outweigh any single prompt. Loops work best when handled as architectural primitives: each one addresses a particular class of problem, carries known trade-offs and ought to be chosen on purpose. The patterns below come with Claude-flavored examples and with the situations in which each one should be avoided.
1. Plan, act, verify
This is the foundational pattern. The agent plans one small step, takes one action, and then checks whether that action brought it closer to a clearly defined goal. It strikes a balance between flexibility and control.
// Pseudo-code: Node/TypeScript orchestrating Claude as an agent
async function planActVerifyLoop(ticket) {
let iteration = 0;
const maxIterations = 5;
while (iteration < maxIterations) {
iteration++;
// 1. PLAN: ask Claude for the next step
const plan = await claude.chat({
model: "claude-3-opus",
messages: [
{
role: "user",
content: `You are a coding agent working on ticket ${ticket.id}.
Goal: Make tests pass for this ticket without changing public APIs.
Current context:
${ticket.description}
${ticket.latestFailureLog}
What is the single most useful next action?`
}
]
});
// 2. ACT: execute the suggested action if it's in the allowed action space
const action = parseAction(plan);
const result = await executeAction(action); // run tests, edit file, etc.
// 3. VERIFY: use tests as verification
const verification = await runTests(ticket.testSuite);
if (verification.allPassing) {
return { status: "done", iterations: iteration };
}
// Attach the failure output back into ticket context
ticket.latestFailureLog = verification.failureOutput;
}
return { status: "budget_exhausted" };
}
In the sketch, Claude handles the micro-planning of the next action, while the loop itself owns verification through tests and enforces an iteration cap. That division prevents the system from announcing success just because the model feels confident.
Good fits are multi-step engineering tasks with measurable progress, rollout scripts and configuration changes that have to settle into a verified working state, and internal tools where strong checks such as tests or schema validation can be built in.
Poor fits include purely creative work like marketing copy or long-form writing, subjective decisions where verification would only stand in for taste, and real-time flows that can't afford several iterations on the critical path.
2. ReAct (reason, then act)
ReAct interleaves short bursts of thought with single actions. Rather than charting the full route up front, the agent studies the newest state, selects one tool, uses it, and then thinks again. That makes it a natural choice for exploratory or diagnostic work, where each fresh observation can legitimately reshape the plan.
# Pseudo-code: Python coordinator orchestrating Claude tool calls
def react_loop(incident):
iteration = 0
max_iterations = 6
while iteration < max_iterations:
iteration += 1
# REASON: Claude decides what to inspect next
reasoning = claude.chat(
model="claude-3-opus",
messages=[
{
"role": "user",
"content": f"""
You are an SRE assistant triaging a payment incident.
Incident summary:
{incident.summary}
Recent metrics:
{incident.latest_metrics}
Logs snippet:
{incident.logs_snippet}
Decide one next diagnostic action from:
- CHECK_METRICS
- CHECK_LOGS
- CHECK_DB_HEALTH
- SUMMARIZE_FINDINGS_AND_RECOMMEND_ACTION
Explain your reasoning briefly and output JSON with 'action' and 'target'.
"""
}
]
)
action = parse_json(reasoning)
if action["action"] == "SUMMARIZE_FINDINGS_AND_RECOMMEND_ACTION":
return claude.chat(... ) # final summary + recommended steps
# ACT: run the selected diagnostic
observation = run_diagnostic(action, incident)
# Update incident state for the next reasoning step
incident.update_with_observation(observation)
Each diagnostic step stays purposeful, and Claude sees fresh context after every action.
Use it for incident diagnosis and root cause analysis, for complex support workflows where each answer changes what to inspect next, and for security investigations in which evidence arrives piecemeal and incomplete.
Skip it when the plan is stable and the pipeline obvious, as in ETL-style document processing, when throughput is high enough that per-step reasoning gets too costly, or when the action space is so narrow that extra deliberation adds nothing.
3. Reflect and revise
Here two phases, generation and critique, are followed by a revision step. It makes a sensible default for anything that needs review before it reaches a customer, a regulator or a production system. The important point is separation: the critic can be another prompt, a cheaper model, or even a rules engine.
async function reflectReviseLoop(draftInput: string) {
// 1. GENERATE
const draft = await claude.chat({
model: "claude-3-opus",
messages: [
{
role: "user",
content: `
Write a customer-facing email explaining a declined payment due to suspected fraud.
Constraints:
- empathetic but clear
- no admission of fault
- no promises about future approvals
Context:
${draftInput}
`
}
]
});
// 2. CRITIQUE (using a cheaper model as checker)
const critique = await claude.chat({
model: "claude-3-haiku",
messages: [
{
role: "user",
content: `
You are a compliance checker.
Review the following email for:
- policy violations
- misleading statements
- over-commitments
Output a JSON with:
- issues: list of strings
- safe: boolean
Email:
${draft.content}
`
}
]
});
const review = JSON.parse(critique.content);
if (review.safe) {
return draft.content;
}
// 3. REVISE
const revised = await claude.chat({
model: "claude-3-opus",
messages: [
{
role: "user",
content: `
You wrote this email:
${draft.content}
Compliance issues:
${review.issues.join("\n")}
Rewrite the email to resolve all issues while preserving intent.
`
}
]
});
return revised.content;
}
This pattern suits customer messages on sensitive subjects such as fraud, chargebacks or regulatory change, internal postmortems that may be audited, and any content where policy compliance outranks style.
It doesn't suit low-risk internal brainstorming, where critique adds friction and no real safety, very short chat answers where a second pass is overkill, or latency-critical flows like in-transaction messaging where a single shot has to do.
4. Draft, test, fix
Engineering teams often favor this loop, since the outcome is objective and lives outside the model: the test suite goes green or it stays red. Each round the agent proposes a change, runs the tests and reads what failed. It then patches the problems and repeats, stopping once the system is in a known good state or an iteration ceiling is reached.
async function draftTestFixLoop(issue: Issue) {
const maxIterations = 4;
let iteration = 0;
while (iteration < maxIterations) {
iteration++;
// DRAFT: Claude proposes code changes
const patch = await claude.chat({
model: "claude-3-opus",
messages: [
{
role: "user",
content: `
You are an autonomous coding agent.
Ticket:
${issue.title}
${issue.body}
Current failing tests:
${issue.failingTests}
Propose a minimal patch as a diff that makes tests pass
without changing public APIs.`
}
]
});
applyPatchToGitRepo(patch.content);
// TEST: run CI locally or via API
const testResult = await runCi(issue.branchName);
if (testResult.success) {
// FIX_DONE: open a PR with the diff
await openPullRequest(issue, patch.content);
return { status: "done" };
}
// FEEDBACK: update failingTests for next iteration
issue.failingTests = testResult.failureSummary;
}
return { status: "needs_human_review" };
}
It works on low-risk, well-contained tickets labeled as ready for an agent, on internal tooling where rollback is trivial, and on non-critical paths that can tolerate the latency and cost.
Avoid it for any change that moves money directly through core transaction paths, for schema changes, migrations and cross-cutting refactors, and for regulated logic where an automated edit could break policy.
5. Critic and builder
One agent builds and another checks. In payments this resembles the maker-checker control already used for high-risk operations, which makes it an easy case to put to risk and compliance teams.
It fits anywhere a second pair of eyes is already expected, automation touching production-adjacent systems such as configuration, routing or notifications, and high-stakes reports or dashboards read by leadership.
It doesn't fit trivial tasks where extra review costs more than it protects, latency-sensitive processes that must answer in one step, or pipelines where weak rules would force the checker to wave through anything that looks good enough.
6. Retry with memory
Most naive loops simply try again. A retry-with-memory loop records the failure states and uses them to avoid repeating the same mistake.
def retry_with_memory_loop(task, max_attempts=3):
failures = []
for attempt in range(1, max_attempts + 1):
# Ask Claude to consider past failures before deciding the next move
decision = claude.chat(
model="claude-3-opus",
messages=[
{
"role": "user",
"content": f"""
You are handling a task with a flaky external API.
Task:
{task.description}
Past failures:
{failures}
Decide whether to:
- RETRY_API
- FALLBACK_TO_CACHE
- ESCALATE_TO_HUMAN
Explain briefly and output JSON: {{ "choice": "...", "reason": "..." }}
"""
}
]
)
choice = parse_json(decision)
if choice["choice"] == "RETRY_API":
result = call_api(task)
elif choice["choice"] == "FALLBACK_TO_CACHE":
result = use_cache(task)
else:
return {"status": "escalated", "failures": failures}
if result.success:
return {"status": "success", "attempts": attempt}
failures.append(result.error_summary)
return {"status": "max_attempts_exhausted", "failures": failures}
Suitable targets are flaky integrations, rate-limited APIs and external systems outside your control, document parsing with inconsistent formatting, and non-critical workflows where retries are acceptable.
Unsuitable ones are core transaction flows where a retry may duplicate effects, operations that aren't idempotent so a retry isn't known to be safe, and scenarios where failure must be immediate and visible and not silently retried.
7. Human escalation
This is the guardrail pattern for anything that touches real money, real customers or regulatory exposure. The loop runs on its own up to a point, then hands off cleanly once confidence falls or risk rises.
async function triageLoop(ticket: Ticket) {
const autoActions = ["LABEL", "ROUTE_TO_QUEUE", "REQUEST_MORE_INFO"];
const decision = await claude.chat({
model: "claude-3-opus",
messages: [
{
role: "user",
content: `
You are a support triage agent in a payments company.
Ticket:
${ticket.body}
Decide one of:
- LABEL (low risk)
- ROUTE_TO_QUEUE (medium risk)
- ESCALATE_TO_HUMAN (high risk / unclear)
Return JSON with:
- choice
- risk_level
- rationale
`
}
]
});
const choice = JSON.parse(decision.content);
if (choice.choice === "ESCALATE_TO_HUMAN") {
await createHumanTask(ticket, choice.rationale);
return { status: "escalated" };
}
// LABEL or ROUTE_TO_QUEUE are automated but bounded
await applyAutomatedTriage(ticket, choice);
return { status: "auto_treated" };
}
Use it for any automation that can shape customer experience or perceived fairness, for flows where partial automation helps but full autonomy is risky, and for early deployments that need to learn before controls loosen.
Leave it out of fully internal, low-risk tasks where humans would merely add noise, back-office batch work where human review can't scale, and situations where escalation would swamp people without improving outcomes.
Choosing in practice
When a new agent opportunity appears, "how do I prompt this?" is the wrong first question. Four better ones are:
- What signal verifies the result: tests, schemas, rules, metrics or human judgment?
- How many steps does the task realistically take?
- What happens when the system fails?
- Who is affected if it gets things wrong?
The answers point toward a pattern:
- Strong, binary verification suggests draft, test, fix.
- An exploratory path suggests ReAct.
- Concerns about policy and tone suggest reflect and revise, or critic and builder.
- A flaky environment calls for retry with memory.
- High risk means building in human escalation from the start.
Loop engineering turns out to be less about clever orchestration and more about disciplined thinking about systems.