Self-Correction Loops for Failed Workflows: Blind Retry Isn't Intelligence
The difference between retrying a failed step and helping a workflow understand why it failed, error classification, bounded self-correction, and where we draw the line and call a human.
Susmit Banerjee
Backend Engineer, Vectorbea
Building Vectorbea · Part 9
A running series on the design and engineering decisions behind Vectorbea's durable execution engine: from event history to approval gates to BYOK.
"Self-correcting agent" is one of those phrases that sounds impressive in a pitch and means something fairly mundane in implementation: when a step fails, instead of (or in addition to) retrying it unchanged, give the LLM information about why it failed and let it try a different approach. This post is about how we built that into Vectorbea, where it helps, where it's dangerous, and why "blind retry" and "self-correction" are not the same axis as "less intelligent" versus "more intelligent."
Blind retry has a place
To be clear upfront: plain retries, same inputs, same approach, maybe with backoff, are the right tool for a large class of failures. Transient network errors, provider rate limits, momentary downstream unavailability: these aren't problems with the approach, they're problems with timing, and retrying unchanged is exactly correct. Feeding "the API returned a 503" back into an LLM and asking it to "try a different strategy" would be solving a problem that doesn't exist while spending tokens to do it.
The first question we ask when a step fails, programmatically, before anything resembling "intelligence" gets involved, is: what kind of failure is this?
The first question to ask
Before deciding how to respond to a failure, classify what kind of failure it is. Most of the apparent intelligence in a self-correction system comes from routing the right failures into the loop and keeping everything else out of it.
Error classification
We classify failures into rough buckets, each of which implies a different response:
TRANSIENT : network blip, timeout, 5xx. Response: retry unchanged, with backoff.
RATE_LIMITED : provider quota hit. Response: defer and re-queue (see cost-budgets post).
INVALID_INPUT : the step's inputs don't satisfy its preconditions. Response: usually terminal;
self-correction rarely helps because the problem is upstream.
TOOL_ERROR : the tool/API returned a structured error the LLM could plausibly act on
(e.g. "ticket ID not found", "field X is required"). Response: candidate
for self-correction.
REASONING_ERROR, the LLM's output didn't satisfy a validation check (schema mismatch, failed
a guard condition). Response: candidate for self-correction.
UNKNOWN : doesn't fit a known pattern. Response: terminal, escalate to a human.Only TOOL_ERROR and REASONING_ERROR are routed into a self-correction loop. Everything else
either has a deterministic correct response (retry, defer) or is unlikely to be helped by giving
an LLM another shot at it.
Design decision
We made error classification an explicit, inspectable step in the failure path, not a hidden heuristic buried in a prompt. The classification is recorded in the event history alongside the failure, which means we can audit why the engine chose to self-correct versus retry versus escalate, and adjust the classification rules when we get them wrong.
What a self-correction loop actually does
When a step lands in TOOL_ERROR or REASONING_ERROR, we construct a follow-up prompt that
includes: the original task, the action that was attempted, the specific error returned, and an
instruction to either retry with corrected parameters or choose a different approach. The LLM's
response becomes a new attempt at the step, going through the same idempotency and checkpointing
machinery as any other attempt (see the retries post).
This is bounded, explicitly:
- A maximum correction attempt count per step (we default to 2, enough to recover from a malformed argument or a misremembered field name, not enough to spiral).
- A similarity check between successive attempts, if the LLM proposes essentially the same action that just failed, we don't spend another round-trip on it; we escalate instead.
- An overall cost ceiling tying back into the budget system, so a self-correction loop can't become the unbounded-spend scenario I described in that post.
Tradeoff
Bounding self-correction tightly means we'll occasionally give up on a problem an unbounded loop might eventually have solved. We think that's the right tradeoff for a system that takes real-world actions: a workflow that "eventually" gets it right after fifteen attempts and a noticeable chunk of someone's budget is not obviously better than one that tries twice, fails cleanly, and hands off to a human who can solve it in thirty seconds with context the system doesn't have.
When to hand off to a human
This is where self-correction loops connect back to approval gates ,
not as a pre-action checkpoint, but as a failure-recovery mechanism. When a step exhausts its
correction attempts, the engine doesn't just mark the run failed and walk away; depending on how
the workflow is configured, it can raise a HUMAN_REVIEW_REQUESTED event that pauses the run
(durably, the same as an approval gate) and surfaces the failure, including the full chain of
attempts and the LLM's own explanations of what it tried, to a person who can either fix the
underlying issue and resume the run, or decide it's not worth pursuing.
Lesson learned
The most valuable thing a self-correction loop can produce, even when it fails to solve the problem, is a clear account of what it tried and why. We initially threw away intermediate attempts and only kept the final error. Once we started preserving the whole chain in the event history, human handoff got dramatically faster, the person picking it up doesn't have to reconstruct what happened, they can read it.
Safety boundaries
A self-correcting loop is, by construction, a system that tries things autonomously when its first attempt didn't work. That's exactly the situation where you want the strongest version of the safety mechanisms described elsewhere in this series, not the weakest. Concretely: self-correction attempts still go through approval gates if the workflow has them configured on that step; they're still subject to budgets and rate limits; and the BYOK boundaries and idempotency guarantees apply identically to a self-corrected attempt as to a first attempt. We were careful not to build a "fast path" that bypasses any of these, the temptation to do so, in the name of letting the loop "just work," is real, and we think resisting it is one of the more important decisions in this whole area.
This is intentionally a conservative design. We'd rather a workflow occasionally stop and ask for help than confidently take the wrong action three more times in a row.
Related articles
Human Approval Gates in Agentic Systems
Modeling 'wait for a human' as a first-class workflow step, waiting states, timeouts, escalation, and why the audit trail has to be airtight.
Cost Budgets and Rate Limits for Agentic Workflows
How we estimate token costs before and during a run, enforce per-run and per-workspace budgets, apply rate limits, and build kill switches that actually stop a runaway workflow.
Retries, Resume, and Idempotency: The Unglamorous Core of Reliability
Step-level retries sound simple until you ask what happens when the step already had a side effect. Notes on idempotency keys, resumption, and duplicate tool calls in Vectorbea.