Designing reliable AI workflows
Most agent failures are not model failures. They are workflow failures. Here is the shape we keep arriving at when the output has to be right.
Every team we work with starts in the same place. A prompt that works in a notebook, a demo that impresses the room, and then a long tail of production incidents that nobody can quite explain. The model did something odd. A tool call returned a shape nobody expected. A retry doubled a side effect. The instinct is to reach for a better model. That is rarely the fix.
The fix is to stop treating the model as the system and start treating it as one component inside a workflow that has to be reliable whether or not the model is.
Separate deciding from doing
The single most useful boundary in an AI workflow is the line between decisions and actions. The model is good at deciding: classifying, planning, drafting, choosing between options. It should not also be the thing that performs the action. When the model both decides to refund a customer and calls the refund API, you have no seam to test, no place to enforce policy, and no way to replay what happened.
We push every action behind a typed interface that the workflow owns. The model proposes an action as data. The workflow validates it, checks it against policy, executes it, and records the result. If the proposal fails validation, the model gets a structured error back and tries again. Nothing leaves the boundary unless it parsed.
This sounds like ceremony until the first time a model hallucinates an account identifier. With the boundary in place, that is a rejected proposal and a log line. Without it, it is an incident.
Make every step idempotent
Retries are the price of talking to anything over a network, and language model APIs fail more often than most. If a step can be retried, it will be, and if it is not idempotent you will eventually send the email twice or create the record twice.
The pattern is old and it still works. Give every action an idempotency key derived from the workflow run and the step, hand that key to the downstream system where it supports one, and where it does not, record the intent before acting and check it before acting again. Treat the model call itself as a step with a key too. Caching the response for a given key makes replay deterministic, which matters more than it sounds.
Parse, don't validate, at every boundary
Model output is untrusted input. It arrives as text, even when you asked for JSON, and it can be wrong in ways that look right. The temptation is to validate it with a few checks and pass the raw thing along. Instead, parse it into a domain type at the boundary and let the rest of the workflow rely on that type.
Concretely: a schema for every tool call and every structured response, a parser that produces either a typed value or a typed error, and downstream code that only ever sees the typed value. When the schema changes, the compiler tells you every place that needs to care.
Budget the loop
Agent loops without a budget are an outage waiting for a trigger. Cap the number of steps. Cap the total tokens. Cap wall-clock time. When any cap is hit, stop and hand the run to a human with everything it has so far. A workflow that gives up cleanly is worth far more than one that keeps trying until the bill arrives.
Record enough to replay
When a run goes wrong, the question is always what did the model see and what did it do. If you cannot answer that from your logs, you cannot fix it. Record the full prompt, the full response, every tool proposal, every validation result, and every action outcome, keyed by run and step. It is more data than you think you need and exactly as much as you will want at three in the morning.
We wrote about the specific fields in What we log when an agent fails. Replay is the reason threadlock exists.
The shape
Put together, the workflow looks like this:
- A typed input arrives and is parsed.
- The model is asked to decide, with a bounded context.
- Its proposal is parsed into a typed action or rejected with a typed error.
- The action runs behind an idempotent interface and its result is recorded.
- The loop continues under a budget, or stops and escalates.
None of this is specific to a model or a vendor. It is the same discipline that makes any distributed system trustworthy, applied to a component that happens to be probabilistic. The model gets to be clever. The workflow has to be boring.