State Machines vs Free-Form Agent Loops
Wondering whether to let your agent roam free or lock it into defined states? An agent state machine trades flexibility for control. This case study shows when that trade pays off.
STATES = {
"start": {"verify_identity"},
"identity": {"pull_credit"},
"credit": {"check_income"},
"income": {"run_risk_rules"},
"risk": {"issue_decision"},
"decided": set(), # terminal
}
def agent_loop(state):
current = "start"
while STATES[current]:
allowed = STATES[current]
action = model_decide(state, tools=[t for t in tools if t.name in allowed])
result = run_tool(action)
state = update_state(state, action, result)
current = next_state(current, action) # advance per transition table
return synthesize_decision(state)Should you let your agent roam free, deciding each step from scratch, or lock it into a defined set of states with allowed transitions between them? It's a question that surfaces the moment a free-form agent does something you explicitly didn't want — skips a required step, acts out of order, or takes an action that only made sense two states ago. An agent state machine answers by constraining the loop to legal moves. This case study follows a team that switched, what it fixed, and what it cost.
The Problem the Fintech Team Faced
The team ran an agent that processed loan applications. It needed to verify identity, pull credit, check income, run risk rules, and issue a decision — a sequence with hard requirements, because issuing a decision before checking income isn't just wrong, it's a compliance violation. Their agent was a free-form loop: on each step it decided what to do next given the state.
Most of the time it worked. But "most of the time" is not acceptable when the failures are compliance violations. Roughly 4% of runs did something out of order — issued a preliminary decision before all checks completed, or skipped a verification the rules required. Each of these was a reportable problem. The free-form loop's flexibility, which was an asset for handling varied applications, was a liability for enforcing a required process.
The core issue was that nothing structurally prevented an illegal action. The agent was instructed to follow the sequence, and usually did, but instruction is not enforcement. A free-form loop can always, in principle, take any action its tools allow, and across enough runs the improbable illegal action eventually happens.
⚡ Pro tip: The question isn't whether your agent usually follows the process — it's whether it can violate the process. If an out-of-order action is merely unlikely rather than impossible, then at production volume it will happen, and how often depends only on your traffic. For anything with hard requirements, you need structural enforcement, not just instruction.
The Wrong Approach
Their first fix was to add stronger instructions — more emphatic prompt language about following the sequence, warnings about the consequences of skipping steps. This reduced the violation rate from 4% to about 1.5%, which felt like progress but was actually the trap: it made the problem rare enough to lull them while leaving it fundamentally unsolved. A 1.5% compliance violation rate is still a compliance violation rate.
The second attempt was a pile of guard conditions scattered through the code —
if not income_checked: raiseThe lesson buried in that second attempt is important: they didn't fail because a state machine was the wrong idea — they failed because they built one implicitly instead of explicitly. Scattered guards are a state machine whose states and transitions exist only in the tangled interactions of the checks. All the rigidity, none of the readability. The whole value of an explicit agent state machine is that it takes the process rules that would otherwise hide in scattered conditionals and lifts them into one visible structure you can read, audit, and change in a single place.
⚡ Pro tip: If you find yourself adding "did we already do X?" guards before more than two or three actions, stop and make the state machine explicit. Those guards are a state machine trying to be born. Writing the states and transitions out as data is less work than maintaining the scattered guards, and it turns an invisible tangle into something a new engineer can understand in a minute.
The Correct Approach
The fix was to make the state machine explicit: define the legal states, the allowed transitions between them, and let the agent choose actions only from what the current state permits. The process became data, not scattered logic.
STATES = {
,[object Object],: {,[object Object],},
,[object Object],: {,[object Object],},
,[object Object],: {,[object Object],},
,[object Object],: {,[object Object],},
,[object Object],: {,[object Object],},
,[object Object],: ,[object Object],(), ,[object Object],
}
,[object Object], ,[object Object],(,[object Object],):
current = ,[object Object],
,[object Object], STATES[current]:
allowed = STATES[current]
action = model_decide(state, tools=[t ,[object Object], t ,[object Object], tools ,[object Object], t.name ,[object Object], allowed])
result = run_tool(action)
state = update_state(state, action, result)
current = next_state(current, action) ,[object Object],
,[object Object], synthesize_decision(state)What this does: It defines each state's legally allowed actions and only offers the model tools permitted in the current state, so an out-of-order action is not merely discouraged but impossible — the agent literally cannot call
issue_decisionThe key shift is that the agent still uses judgment within each state — how to verify a particular identity, how to interpret a credit result — but it cannot leave the rails of the required process. Flexibility where it helps, enforcement where it matters.
[object Object], ,[object Object],(,[object Object],):
transitions = {
(,[object Object],, ,[object Object],): ,[object Object],,
(,[object Object],, ,[object Object],): ,[object Object],,
(,[object Object],, ,[object Object],): ,[object Object],,
(,[object Object],, ,[object Object],): ,[object Object],,
(,[object Object],, ,[object Object],): ,[object Object],,
}
,[object Object], transitions[(current, action.name)]What this does: It encodes the allowed process as an explicit transition table, so the sequence lives in one readable place instead of being smeared across scattered guard conditions — making the whole flow auditable at a glance.
Results and What Changed
Out-of-order violations went to exactly zero, because they became structurally impossible rather than merely discouraged. This was the whole point, and for a compliance-critical process, moving from 1.5% to 0% is worth a great deal even if nothing else improved.
But other things improved too. The explicit state machine made the process auditable — a regulator or a new engineer could read the states and transitions and understand exactly what the agent was allowed to do, which the scattered-guards version made impossible. Debugging got easier because every run could be described by the state path it took. And onboarding new requirements became a matter of editing the transition table rather than hunting for the right place to add another guard.
The cost was real flexibility loss. The agent could no longer handle genuinely novel application types that didn't fit the defined states — an unusual case that needed a step outside the sequence now hit a wall. The team accepted this because their process genuinely was fixed, but it's the crux of the tradeoff: a state machine is exactly as flexible as its states, and no more.
They handled the flexibility loss with a deliberate escape hatch: any application the agent state machine couldn't process cleanly got routed to a human queue rather than forced through the wrong path. This turned the rigidity from a failure mode into a feature — the machine handled the 96% of standard cases with perfect compliance, and the genuinely unusual 4% went to a person instead of getting a bad automated decision. The key insight was that a constrained agent doesn't have to handle everything; it has to handle its defined cases perfectly and hand off the rest honestly.
⚡ Pro tip: Pair every state machine with a clean "doesn't fit" exit that routes to a human. The danger of a rigid structure isn't the cases it handles — it's the cases it almost handles, where it forces a square peg through a round hole. An explicit escape hatch means the machine can be strict about its defined path precisely because anything off that path leaves the machine entirely rather than getting mangled by it.
⚠️ Common mistake: Reaching for a state machine when your process isn't actually fixed. State machines shine on workflows with genuine hard constraints and a stable set of steps. Forcing one onto an open-ended, exploratory task fights the task the whole way — you spend your time adding states for cases you didn't anticipate, recreating the free-form flexibility you gave up, but now with more ceremony. Match the structure to whether your process is truly constrained.
How to Apply This to Your Situation
Start by asking whether your agent's task has hard requirements — steps that must happen, orders that must hold, actions that are illegal in certain states. If yes, a state machine is worth considering. If your task is open-ended exploration with no fixed process, a free-form loop is the better fit and a state machine will just get in the way.
If you do go with a state machine, keep the states coarse. Model the real process constraints, not every micro-step. Over-fine states recreate the rigidity problem and make the machine as hard to maintain as the scattered guards you replaced. And leave room for judgment within states, so you keep the model's flexibility exactly where it doesn't threaten the process.
⚡ Pro tip: Let the state machine constrain which actions are legal, but let the model decide freely among the legal ones. The mistake that makes state machines feel painful is over-specifying — dictating not just what's allowed but exactly how to do it, which strips away the model's judgment. The right division is structure for the process, freedom for the execution: the machine says "you may verify identity now," the model figures out how to verify this particular identity.
Three teams show the spectrum. A healthcare-intake agent used a state machine to enforce that consent was captured before any data collection, a hard legal requirement. A customer-support agent stayed free-form, because its task — answering varied questions — had no fixed process to enforce and a state machine only slowed it down. And a payments agent used a hybrid: a state machine around the money-moving steps, free-form reasoning everywhere else, putting structure exactly where the stakes were highest.
Next Steps
Decide whether your process is genuinely fixed. If it is, make the state machine explicit — legal states, a transition table, tools gated by state — so violations become impossible rather than unlikely. If it isn't, keep the free-form loop and don't add ceremony you'll fight. For many agents, the answer is a hybrid: a state machine around the critical constrained steps, freedom everywhere else.
The state and transition definitions, and the state-gated tool selection, are reusable structures across constrained agents. A prompt and snippet library like PromptABCD is a useful place to keep your state-machine scaffolding and the prompts that run inside each state, so your next constrained agent gets enforceable structure without you rebuilding the machinery each time.
Continue Reading
Save the prompts from this post
PromptABCD is a free prompt manager. Paste, organize, and reuse your best AI prompts — no more hunting through chat history.
