# Designing a Self-Healing Agent Loop: Automated Error Recovery and Retries
*When the code path is generated per-run by a stochastic model, your retry policy can’t be a static snippet. Here is how an SRE makes an agent classify failures, recover from them, and stop amplifying outages.*

## 1. Introduction: The Fragility of Autonomous Agents in a Brittle API World
Every engineer who has run an agent against real APIs knows the death scene: the tool times out, and the agent calls it again with the same arguments, again and again, while the upstream stays down. Nobody wrote a retry loop; the model *became* it.
A deterministic service survives production entropy because its error handling was written once, in code: backoff, jitter, idempotency keys, a circuit breaker. An agent has none of that by default. Its “error handling” is improvised at runtime, under token pressure, by a model that treats every failure as a puzzle rather than a signal to stop. **In an agentic system, the retry policy is generated per-run by the thing that is already failing.**
Production APIs punish this: p99 spikes, 429 rate limits, 5xx during deploys, 2 a.m. maintenance resets. The failure shapes are consistent: the **retry spiral** (identical calls until a ceiling or a human intervenes), the **confident drift** (after two errors the model stops calling tools and invents answers), and the **cascade** (one poisoned result invalidates every downstream step). These failures are behavioral, not technical — the system returns success while doing the wrong thing, or quietly burns budget.
The fix is not to prompt the model to “be more careful.” It is to move error handling out of the context window and into the execution machinery around the model: **classify the failure, pick a recovery strategy, and enforce limits the model cannot override.** That is the same architectural move the Model Context Protocol made for tooling: taking invariants out of prose and putting them into structure. A self-healing loop is what you get when the runtime cares about failure as much as the model cares about the task.
## 2. Error Classification: Model Errors vs. Transient API Failures
You cannot heal what you cannot classify — and you cannot classify with a model. Asking an LLM “what went wrong?” on a 500 costs tokens and produces prose instead of policy. **Classification belongs at the tool boundary, in the runtime**, where every tool wrapper returns a structured error envelope before the model ever sees the failure:
“`json
{
“error”: {
“class”: “transient”,
“code”: “RATE_LIMITED”,
“retryable”: true,
“retry_after_ms”: 30000,
“tool”: “crm.account.update”,
“hint”: “honor Retry-After; do not regenerate arguments”
}
}
“`
Three classes cover nearly every production failure, and each maps to one family of recovery:
– **Transient API failures** — 429s, 5xx, timeouts, resets. A retry *can* succeed: the request was fine, the infrastructure was not. Recovery is deterministic — exponential backoff with jitter, honor `Retry-After`, retry the identical payload under an idempotency key. Never ask the model to “improve” a request that was never the problem.
– **Contract failures** — 400/422 schema rejections, missing resources, expired auth. Also deterministic, in the opposite direction: retrying an identical payload is pure waste. Recovery means repairing the plan — fix the arguments, refresh the credential — then re-executing; a contract failure that recurs after repair is a stop-and-escalate signal.
– **Model errors** — wrong tool chosen, arguments that pass schema but violate business semantics, a plan that contradicts verified results. Not a network class; retrying is meaningless because the same model repeats the same decision. This is the expensive class, because detecting it needs a **verification step** — a read-only check that output matches the goal — not a status code.
Two rules keep it honest. Classify on code and envelope, never payload heuristics: a 500 with an `UPSTREAM_TIMEOUT` body is transient; a 200 with an empty result may be a model error. Treat the classifier as policy, not model judgment — version it with the tool schema, change it without shipping a prompt.
## 3. Recovery Patterns: Backtrack, Plan-Repair, and Tool-Switching
With errors classified, the executor dispatches to one of three patterns from a policy table. They are deliberately few: the runtime must reason about them, and the model must be constrained by them — let the model choose among *visible, eligible* actions; let the runtime decide what is visible and eligible.
**Backtrack — rewind, don’t replay.** Every completed step appends to a checkpoint: plan state, tool result, idempotency key. When an error invalidates the branch, the executor rewinds to the last validated checkpoint — you checkout a known-good state instead of performing surgery on a broken one. The SRE detail that makes this safe: **rewinding context does not unwind side effects.** An order created before the backtrack does not un-create itself. Every side-effecting tool must be idempotent (keyed by step) or compensated (a paired cancel call), or backtracking is just a fancier way to corrupt data.
**Plan-Repair — amend, don’t regenerate.** For contract failures and shallow model errors, the cheapest correct recovery is a surgical amendment of the *remaining* plan — fix the argument that failed validation, drop the step that contradicts verified results. Repair is bounded to two or three rounds with a token budget: an unbounded repair loop is the retry spiral in a nicer jacket. The model may only edit **unexecuted** steps (executed history is immutable, keeping the decision log replayable), and repaired steps pass the same schema and policy validation as the original.
**Tool-Switching — capability pluralism.** The same capability — update an account, enqueue a job — usually lives behind several tools: a synchronous REST tool, a queue-backed async variant, a read-only verification view. The loop needs a **fallback registry**: an ordered list of substitutes per tool, maintained by policy, not improvised mid-crisis. When the primary tool’s breaker is open or its retries are exhausted, the executor promotes the next tool in the registry. The model never “decides” to switch — the runtime simply filters the unavailable tool from its toolset. Switching becomes routing with deterministic preferability.
The dispatch loop tying the patterns together:
“`
while plan.has_steps() and run_budget.remaining():
result = executor.call(step.tool, step.args, idempotency_key=step.key)
if result.ok:
checkpoint(step, result); continue
error = classify(result)
if error.transient and step.attempts < max_retries: # backoff retry sleep(backoff(step.attempts, jitter=True)); step.attempts += 1; continue if breaker.is_open(step.tool): # tool-switching step.tool = policy.fallback_for(step.tool) if step.tool is None: escalate(error); break step.attempts = 0; continue if error.contract or error.model: # plan-repair plan, repaired = repair_plan(plan, error, repair_budget=2) if not repaired: # backtrack plan = last_valid_checkpoint().plan ``` The property that makes this self-healing rather than self-harming: **every path terminates** — retries have a max, repairs have a budget, backtracks land on validated state, and exhaustion ends in a structured escalation: a human handoff, never a silent success. ## 4. Circuit Breakers: Preventing Agentic Retry Storms on Downed Upstreams A single agent retrying a downed API is a nuisance; a fleet of them is an outage amplifier. When the upstream goes down, every concurrent run discovers it at once and starts its own retry campaign against a service that cannot recover under the load they generate. No individual agent misbehaves, yet the aggregate is a self-inflicted DDoS with a token bill attached: the **agentic retry storm**. The arithmetic is brutal. An upstream sheds load with 429s and a 30-second `Retry-After`; a disciplined client retries every 30 seconds, while a model improvising its own policy retries in two — fifteen times the request rate. Across a hundred concurrent runs, that is a 1,500× amplification of the exact load the outage was designed to shed. The breaker makes disciplined backoff an *enforced property of the runtime*, not a suggestion. Agentic breakers follow the classic tri-state pattern with specifics bolted on: - **Per-tool and per-upstream.** State is keyed by tool name and underlying endpoint so one failing tool doesn't poison unrelated ones. Open on five consecutive transient errors or a sustained error rate above ~40%. - **Open, half-open, closed.** While open, calls are refused immediately and the **planner never sees the tool** — it is filtered from the toolset, so the model spends zero tokens proposing calls the breaker rejects. After backoff, a half-open breaker admits one probe: success closes it, failure re-opens it longer. - **Fleet-wide, not per-pod.** In-process state means every instance trips its own breaker while the fleet keeps hammering. Share state via Redis or etcd so the platform presents one breaker per tool, and stagger half-open probes with jitter. - **Break on the right class.** Open on *transient* failures only. Contract and model errors will never succeed on retry; tripping on them only masks a broken schema or a drifting model behind "service unavailable." The same logic extends to the model layer: when a provider degrades in one region, break to a secondary model or region. The breaker does not care whether the thing being hammered is an ERP endpoint or an inference API — and because it enforces limits the model cannot see past, a behavioral problem becomes a solved infrastructure problem. ## 5. Observability: Tracking Recovery Success Rates and "Time to Heal" A self-healing loop that is not instrumented is just a loop with extra steps — you cannot tell whether it is healing errors or converting them into confident wrong answers. Recovery observability adds two metrics to decision logging; both deserve SLOs. **Recovery success rate (RSR)** — the share of erroring steps that resolve to a verified successful outcome without human intervention, broken down by error class and healing pattern. RSR is the loop's health signal: a falling transient-class RSR tracks upstream fragility and miscalibrated retry budgets; a rising model-error RSR means the loop heals model mistakes so efficiently that your eval suite stops noticing the model is drifting. Track per class, per tool, per prompt version — alert on deltas, not absolutes. **Time to heal (TTH)** — wall-clock from first failed attempt on a step to that step completing successfully, or to human handoff. TTH is the direct cost of the uncertainty the loop absorbs and the best single number for comparing agent designs: a retry healed in 400 ms is noise; a model error needing four replans and a backtrack is a design finding. Track p50 and p95 and gate releases on them like latency. Every recovery decision emits an event that makes the healing replayable: ```json { "ts": "2026-09-06T09:14:02.118Z", "trace_id": "7c2e…", "step": 7, "tool": "crm.account.update", "error_class": "transient", "error_code": "UPSTREAM_TIMEOUT", "recovery": { "pattern": "retry_then_switch", "attempts": 2, "fallback_tool": "crm.account.update.async" }, "healed": true, "ttheal_ms": 5400, "cost_usd": 0.031 } ``` Build supporting panels around them — retry counts by error code, repair-budget exhaustion, breaker opens per tool — plus the chart most teams skip: **cost of recovery versus cost of starting over.** When a plan is fourteen steps deep and three are poisoned, healing can cost more than killing the run and restarting fresh. Then write SLOs that hold the loop accountable. The ones we run in production: **≥ 95% of transient errors heal automatically with p50 TTH under five seconds; ≥ 90% of contract errors heal via plan-repair; escalation under 2% of erroring steps; zero silent successes after an exhaustion path.** Alert on RSR falling below target or TTH p95 breaching — never on raw error counts, because a healthy healing loop is supposed to see errors. ## 6. The OnlineCloud Standard: Building Agents That Survive Production Entropy None of this is exotic, and that is the point. A self-healing agent is not a smarter model; it is a runtime that treats the model as the least reliable component and engineers around it. The discipline reduces to a short list — the one we hold every engagement to: **Typed tools with clean error semantics** — every wrapper classifies failures into the envelope before the model sees them. **Idempotency as a precondition** — without an idempotency key, retries and backtracking are off the table. **Recovery policy as data** — retry budgets, fallback registries, breaker thresholds, and repair caps live in versioned configuration, never improvised in a system prompt. **Limits the model cannot override** — breakers, per-run dollar ceilings, and human checkpoints on irreversible actions are enforced in the runtime. **Instrumented from day one** — RSR, TTH, and escalation rate ship with the first vertical slice, not the postmortem. One practice separates evaluated systems from demos: **failure injection.** Part of the eval suite that defines "done" is a chaos pass — kill the upstream mid-run, ratelimit the agent, return a schema-valid but poisoned response, rotate a credential — and the agent must heal within its SLOs. If it cannot survive a simulated outage in week three, it will not survive a real one in month six. That is the difference between an agent that demos well and one your operations team can sleep through: the second has already failed, in a controlled environment, and gotten better at it. Production entropy is not a bug to be eliminated; it is the environment. Upstreams degrade, schemas drift, credentials rotate, and models change their minds. The agents worth running are designed for that world — classifying errors, healing within budget, escalating cleanly when they cannot. At OnlineCloud, we build those agents: US-based senior engineers shipping evaluated, observable agentic systems against Oracle, MS SQL, Postgres, Snowflake, and the CRMs and APIs your teams use every day, typically in 6–10 weeks, with dashboards and on-call owned by the people who built it. If your agents are one 429 away from a thousand-dollar spiral, [contact the OnlineCloud team](https://www.onlinecloud.dev/) and bring the workload you are tired of babysitting — we will show you a loop that heals itself.