I Gave an LLM a State Machine and It Got Boring (In a Good Way)
Agent frameworks sell autonomy: let the model decide what to do next, every step, forever. Constrain that same agent to a finite state machine and something better than magic happens. It becomes predictable.
The Agent Loop's Dirty Secret
Strip away the marketing and most "autonomous agents" are a while loop: prompt the model, get back a tool call or a final answer, execute the tool, feed the result back in, repeat. The model is fully in charge of what happens next at every single iteration. That's the pitch — reasoning replaces control flow.
It's also why these things are so hard to debug in production. When an agent gets stuck in a loop, calls the wrong tool twice in a row, or "decides" to retry a failed payment three extra times, you're not debugging code, you're debugging a probability distribution. There's no line to set a breakpoint on. The control flow is the model's judgment, and the model's judgment doesn't leave a stack trace.
State Machines Solved This Problem Before LLMs Existed
Distributed systems engineers have had a name for "a process that moves through a known set of stages, with
explicit legal transitions between them" for decades: a finite state machine. An order goes from
pending to paid to fulfilled to shipped. It cannot jump
from pending to shipped. Every transition is enumerated, every illegal transition
is rejected by construction, and the current state is always inspectable — you can point at exactly where
the process is and exactly what it's allowed to do from there.
The insight worth stealing back for agents: most of what an agent actually needs to do isn't open-ended reasoning about "what should happen next in the universe," it's reasoning within a much smaller question — "given I'm in this state, which of these three legal transitions applies here." That's a dramatically easier problem, and it's one you can constrain the model into instead of hoping it discovers on its own.
What This Looks Like in Practice
Instead of one big system prompt with every tool available at every step, define states with a small, fixed set of legal transitions:
const states = {
triaging: { allowedTransitions: ["needsInfo", "resolvable", "escalate"] },
needsInfo: { allowedTransitions: ["triaging"] },
resolvable: { allowedTransitions: ["resolved", "escalate"] },
escalate: { allowedTransitions: ["resolved"] },
resolved: { allowedTransitions: [] }
};
function nextState(current, modelDecision) {
if (!states[current].allowedTransitions.includes(modelDecision)) {
throw new IllegalTransitionError(current, modelDecision);
}
return modelDecision;
}
The model still makes the call — it's still reasoning, still handling ambiguity, still the smart part of the
system. What it no longer does is invent a fourth option nobody accounted for. If it tries to jump straight
from triaging to resolved without ever passing through resolvable,
the transition gets rejected before it does anything, the same way a malformed order status update gets
rejected by a well-built order service. The failure is now a validation error with a stack trace, not a
weird support ticket three days later.
Why "Boring" Is the Feature
This costs you something real: the agent can no longer surprise you with a creative multi-step plan that wasn't in the state diagram. For a research assistant exploring an open-ended question, that's a genuine loss — you probably don't want to cage that kind of task in a rigid FSM.
But for the agents actually running in production doing things like triaging support tickets, processing refunds, or routing incidents, "creative multi-step plan you didn't anticipate" isn't a feature, it's an incident. You want the boring, legible version: a known set of states, known legal transitions, and a model making narrow decisions within guardrails you can actually test. The FSM doesn't make the agent less intelligent. It makes the parts of the system you're responsible for operating exhaustively enumerable again — and enumerable is what lets you sleep through the night instead of watching a dashboard.
The Actor Model Angle
Take this one step further and each state becomes an actor: a unit that owns its own state, processes one message at a time, and only communicates with other actors through well-defined messages — no shared mutable state, no ambiguity about who's allowed to change what. This is exactly the discipline that made Erlang systems resilient enough to run telecom switches for decades before "AI agent" was a phrase anyone used. An agent built from a handful of well-defined actors, each responsible for one state's worth of decision-making, inherits that same resilience: one actor crashing doesn't corrupt shared state, because there wasn't any to corrupt.
None of this is new engineering. It's fifty-year-old distributed systems theory, wearing an LLM as a very convincing decision function inside each state. The novelty was never supposed to be in the plumbing.
FAQ
Doesn't constraining an agent to a state machine defeat the purpose of using an LLM at all?
Not if the LLM is still doing the part that's actually hard — interpreting ambiguous input and deciding which of the legal transitions applies. The state machine constrains what happens after the decision, not the reasoning that produces it.
What kinds of agents shouldn't be built this way?
Open-ended research or exploration agents, where the value comes specifically from discovering plans and steps you didn't anticipate, don't benefit from a rigid FSM. Reserve this pattern for agents doing bounded, repeatable operational work where predictability matters more than creativity.
How is this different from a regular workflow engine like Temporal or Step Functions?
It isn't fundamentally different — those tools are, in effect, production-grade implementations of exactly this idea. The point of the article is that the same discipline they enforce for regular services is worth applying deliberately to LLM agents, rather than assuming agents need a different, looser paradigm.
What happens when the model tries an illegal transition?
The transition is rejected before any side effect happens, and you can retry with a corrective prompt, fall back to a default transition, or escalate to a human — the same options you'd have for any other validation failure. The key benefit is that the failure is caught and explicit, not silently acted on.