Before you write a system prompt, write a state diagram.
Every agent that does real work is a stateful system. It starts in an initial state. User actions and tool results move it through transitions. It reaches terminal states: success, failure, waiting for input. Between those terminals are states that may be valid or invalid, expected or unexpected.
If you don't model the states explicitly, the agent will find them for you. In production. Usually via a user complaint.
What an agent state model contains
States: The discrete situations the agent can be in. Not "the agent is running" — that's not specific enough. The states are the points between meaningful transitions:
awaiting_user_inputfetching_account_dataprocessing_resultsawaiting_approvalexecuting_actioncompletedfailedtimed_out
Transitions: The events that move the agent from one state to another. A transition is an event + a guard condition + an action + a target state.
state: fetching_account_data
event: tool_result_received
guard: result.status == "success"
action: store_account_data()
next_state: processing_results
state: fetching_account_data
event: tool_result_received
guard: result.status == "error"
action: log_error()
next_state: failed
state: fetching_account_data
event: timeout
action: increment_retry_count()
next_state: failed [if retry_count >= max_retries]
next_state: fetching_account_data [if retry_count < max_retries]
Guards: Conditions that determine which transition fires when multiple transitions share the same event. Without guards, you can't handle the difference between a recoverable error and a fatal one.
Actions: Side effects that execute during a transition. Writing to state storage, sending a notification, incrementing a counter. Actions should be explicit and attached to transitions, not scattered through the agent's reasoning.
Terminal states: States where the agent stops. There should be explicit success and failure terminals. Implicit termination — the agent just stops — is a design gap. What state is the agent in when it stops? Should it be retried? Should the user be notified?
Why this matters for production reliability
Invalid states become visible before you ship. When you draw the state diagram, you can see which state combinations are possible. An agent that can reach executing_action from awaiting_approval — without the approval event firing — is a design error. You see it in the diagram. You don't see it until a production incident if you don't.
Error handling becomes explicit. Every state needs a defined response to the error event. What does fetching_account_data do when it gets a 500 from the API? What about a 429? What about a timeout? If these aren't explicit transitions, the agent will improvise. Improvisation in error paths is how you get silent failures.
Recovery logic becomes implementable. When the agent crashes and restarts, it needs to know what state it was in. If state is explicit and stored durably, you know exactly which state to resume from and which transitions to re-trigger. If state is implicit in the conversation history, recovery is guesswork.
Testing becomes systematic. A state machine can be tested exhaustively. For each state, test each transition: does the right event fire, does the right guard condition apply, does the correct next state result? You can reach 100% state coverage in tests if the states are explicit. You can't if they're not.
Implementing the state model
Persist state at every transition. The state machine's current state and relevant context should be written to durable storage at every transition. Not in memory. In a database.
class AgentStateMachine:
def transition(self, event: Event, data: dict):
current = self.load_state() # From DB
next_state = self.compute_transition(current.state, event, data)
if next_state is None:
raise InvalidTransitionError(f"No transition from {current.state} on {event}")
self.execute_actions(current.state, event, next_state, data)
self.save_state(next_state, data) # To DB, atomically
return next_state
Reject invalid transitions explicitly. When an event arrives in a state that has no transition for that event, raise an explicit error. Don't let the agent continue reasoning from an undefined position. An invalid transition is a bug — surface it loudly.
Use a state machine library. XState (JavaScript/TypeScript), python-statemachine (Python), Stateless (C#). These libraries enforce that your state model is complete, prevent invalid transitions, and provide visualization tools for the diagram.
Separate the state machine from the LLM. The state machine is the skeleton. The LLM fills in the reasoning within states. The model should not decide what state the agent is in — the state machine should. The model's output triggers events. Events cause transitions. The model doesn't know the state; it only knows what it needs to do within the current step.
The minimum viable state model
For a simple linear agent task, the minimum state model is:
INITIAL → RUNNING[step_1] → RUNNING[step_2] → ... → RUNNING[step_N] → COMPLETED
↓ (on error) ↓ (on error)
FAILED FAILED
Even this minimal model is more explicit than no model. It forces you to define what a step is, what failure means, and what completed means. From this minimum, you can add states for retry logic, approval gates, waiting states, and partial completion recovery.
The diagram you draw before writing the first prompt is the spec for every subsequent engineering decision. States that aren't on the diagram will be reached in production. Put them on the diagram.

