Best Practices for Agent State-Machine Router Design
The best state machines are boring to read and expensive to
break. Start with explicit error states. A machine that only
models the happy path — idle, running, done — has nowhere to go
when something fails, so the runtime falls back to implicit
behavior you cannot reason about. Add a
failed state, a
retry state, and a
terminal aborted state,
and every failure now has a defined, observable place to land.
That single habit converts unhandled exceptions into ordinary
state transitions.
Name states as nouns and events as verbs or past-tense triggers.
running,
awaiting_approval, and
failed describe
conditions; start,
approve, and
retry describe the
triggers. Consistent naming makes the transition table
self-documenting and makes the generated DOT graph readable
without a legend. Vague names like
step3 or
phase_b make every future
review harder and every failure harder to trace.
Encode guard conditions in the transition, not the state. A
single event like
submit should route
differently depending on whether validation passed, which is a
guard (if valid → review, else → revision), not a separate event
per outcome. Modeling guards as states instead of conditions
explodes the state count and buries the real logic. In the
transition-table model, guards are documented alongside the edge
— "submit [valid]" — keeping the machine small and the condition
explicit.
Choose one start state and make it obvious. The evaluator defaults to the first state in your transition list, but you should name your intended entry point explicitly so the graph shows it clearly. Reachability analysis is meaningless without an agreed origin, and a machine reviewed by two engineers with two different assumed starts produces two different conclusions. State the start in the machine's documentation and in the analyzer input.
Keep every state reachable and every terminal deliberate. Run the analysis, then treat each warning as a design question: is this unreachable state dead code to remove, or a missing edge to add? Is this dead-end a genuine conclusion or an error state I forgot to wire to recovery? The evaluator's warning table turns these into a checklist you can clear one item at a time, and a clean analysis is your machine's health certificate.
Document transitions with the event that owns them. Each edge should be attributable to a real trigger in your system — an API response, a user action, a cron event, a tool result — not a speculative "just in case" edge. Edges without a firing event are untestable claims. When you review the transition table, ask "what code path actually raises this event?" and delete any edge you cannot answer for.
Version and review the machine like code. The transition table is a specification; put it in version control beside the implementation, and require a review whenever it changes. The DOT output doubles as the review diagram — paste it into the pull request so reviewers see the graph diff, not just a list of strings. Machines that are reviewed as artifacts accumulate fewer surprises than machines that evolve in someone's memory.
Finally, couple the machine to observability. Log every state entry and event, and emit a metric for time spent per state. For AI agents this is especially valuable — a retry loop that spins is invisible without state telemetry, and the cycle warning from the analyzer is a preview of exactly the incident metrics will reveal. A machine you can see in production is a machine you can trust.
The disciplines are few and mutually reinforcing: explicit error states, noun/verb naming, guards on edges, an agreed start, verified reachability, attributable events, reviewed diffs, and state telemetry. Practice them together and your state machines — and the agents they govern — become models of clarity and reliability.
Document every state and event in one canonical place. The transition table is the natural home: it is compact, it is what the analyzer reads, and it doubles as the spec that reviewers and runtime implementers consult. When a new engineer asks how the agent reaches a particular outcome, the answer should be a pointer to the table, not a verbal explanation that drifts from the code. Keep the vocabulary — state names, event names — consistent across the table, the code, and the diagrams, because the moment naming diverges is the moment analysis stops being trustworthy. A single source of truth for the machine's topology is the cheapest documentation investment a project can make.
Version the machine the way you version code. Because the transition table is data, it can live in version control, be reviewed in a diff, and be reverted when a change misbehaves — and every change to routing behavior should arrive as a reviewed change to the table, not as an edit that bypasses review. Before a change is merged, run the analyzer on the new table and confirm the reachability, cycle, and dead-end reports are still clean; a machine whose analysis fails review is a machine that should not ship. Over time, the table accumulates history that explains why the agent routes the way it does — the record of decisions that a verbal handoff never preserves. That history is what makes the state machine maintainable long after the original designer has moved on.