A free-form loop starts small: send the request to the model, run the tool it chooses, and return the result for the next round. Trouble starts when the agent must pause for approval, retry only one step, block a write, or explain why it reached a terminal state.

A state machine helps when the flow has phases that need to be visible and transitions that can be rejected. It does not make the model deterministic. It puts the model in specific places and lets the runtime control what can happen before and after each call.

If your first question is where to store checkpoints and resume an execution, start with the comparison of queues and workflows for AI agents. This article covers a different layer: how to represent the flow that the checkpoint must save.

Diagram comparing a free-form agent loop with a state machine that moves through plan, tool, review, and done states.

Short answer

  • Keep the free-form loop when the path is exploratory and external effects are small.
  • Model states when you have phases, pauses, permissions, or invalid transitions.
  • Let the model decide where uncertainty exists. Let code decide what already has a contract.
  • Persist state and test transitions. A diagram alone does not protect execution.

What does a state machine add to an agent?

A state machine describes a finite set of states and the transitions allowed between them. In AWS Step Functions, "Learn about state machines in Step Functions", states can run tasks, make choices, wait, create branches, or end an execution. The next step is not a suggestion hidden in a prompt. It is part of the workflow contract.

For an agent, think in five pieces:

  • State: what the runtime knows about the execution now.
  • Transition: the event that can move the execution to another state.
  • Guard: the condition that authorizes or rejects the transition.
  • Effect: a tool call, write, or approval request.
  • Terminal: the success, failure, or blocked state that ends the flow.

The architectural gain is this boundary. The model can classify an intent, propose parameters, or summarize a tool result. The runtime still needs to decide whether that result authorizes a state change and whether the matching tool is available in that phase.

What signs show that a free-form loop has become fragile?

Move to explicit states when the same kind of error must be prevented mechanically, rather than described only in a system prompt. Four signs show up often: the loop repeats a step without a clear limit, a tool call has an external effect, a person must approve the next step, or support needs to reconstruct the path after a failure.

The LangGraph guide to workflows and agents separates workflows, with predetermined paths, from agents, which define their process and tool use dynamically. The choice is not moral or binary. One system can use an agent to interpret input and an explicit workflow to apply the result.

Use this practical test:

Question If the answer is yes
Can a step charge, delete, publish, or change data? Put a guard before the effect.
Must the flow wait for a person or external event? Model a waiting state.
Should a failure resume from a known point? Persist state and a checkpoint.
Does a known rule determine the valid path? Make the transition in code.
Is the task still exploring an unknown path? Keep an agentic section, with a limit.

Do not turn every conversation into a graph. A simple classifier, lookup, and final answer may be easier to operate as a function. A state machine earns its cost when it makes a risk or obligation visible.

How should you model agent states and transitions in TypeScript?

Use a discriminant field so each state carries only the data that makes sense in that state. The TypeScript Handbook, "Narrowing" explains that a discriminated union can be refined through a literal property. This does not validate model output by itself, but it stops code from treating a review state as if it already contained a final result.

The example below is illustrative. It does not call a provider, persist data, or replace input validation. Its purpose is to make illegal transitions hard to hide inside a while loop.

type AgentState =
  | { kind: "planning"; request: string }
  | { kind: "tool"; request: string; toolName: "lookup"; input: string }
  | { kind: "review"; request: string; result: string }
  | { kind: "done"; request: string; result: string }
  | { kind: "blocked"; request: string; reason: string };

type Event =
  | { kind: "plan-ready"; toolName: "lookup"; input: string }
  | { kind: "tool-finished"; result: string }
  | { kind: "approved" }
  | { kind: "rejected"; reason: string };

function transition(state: AgentState, event: Event): AgentState {
  switch (state.kind) {
    case "planning":
      if (event.kind !== "plan-ready") throw new Error("invalid transition");
      return { ...state, kind: "tool", toolName: event.toolName, input: event.input };

    case "tool":
      if (event.kind !== "tool-finished") throw new Error("invalid transition");
      return { kind: "review", request: state.request, result: event.result };

    case "review":
      if (event.kind === "approved") {
        return { kind: "done", request: state.request, result: state.result };
      }
      if (event.kind === "rejected") {
        return { kind: "blocked", request: state.request, reason: event.reason };
      }
      throw new Error("invalid transition");

    case "done":
    case "blocked":
      throw new Error("terminal state cannot advance");
  }
}

The code separates the model response from the transition. An agentic layer can produce plan-ready, but the runtime must validate the tool name and arguments before execution. TypeScript tool-call validation is the next level of this boundary: state controls the sequence, while the tool contract controls the data shape.

Where does the model belong in a hybrid architecture?

The model belongs where useful uncertainty exists, not at every state change. It can extract intent, choose between allowed paths, or interpret a tool result. Code can run a known sequence, count attempts, block an effect, and route an approval request.

The LangGraph Graph API expresses this split with State, Nodes, and Edges: nodes receive state and produce updates, while fixed or conditional edges choose the next node. The idea does not depend on that library. You can implement it with a TypeScript function, a queue, Step Functions, or an agent runtime.

A common design looks like this:

  1. Classify: the model turns the request into a structured intent.
  2. Plan: the runtime checks whether that intent has a supported path.
  3. Execute: deterministic code calls the allowed tool.
  4. Review: a rule or person approves the effect.
  5. Finish: the system records the result and ends.

If classification is invalid, do not create an approximate transition. Send it to blocked or ask for clarification. If a tool fails, record the failure in state and choose between a bounded retry, compensation, or human review. The model should not decide alone whether an external write already happened.

How do persistence, retries, and approvals change the design?

States only help after they survive the process that executes them. The LangGraph overview positions the runtime for long-running stateful workflows and agents, with durable execution, human-in-the-loop control, and memory. Those features do not remove the need to choose what belongs in a checkpoint.

Persist at least the execution ID, schema version, current state, attempt count, relevant events, and references to external effects. Do not save only the last model message. To resume an approval, the system must know which effect was waiting and which event ends the wait.

Retry also belongs in the transition design. An idempotent call can be repeated safely. A charge, publication, or message send needs an operation key, an observable confirmation, or a compensation state. The article on durable execution, queues, and workflows goes deeper into recovery infrastructure. Here, the point is not to hide this contract inside a generic loop.

How do you verify an agent state machine?

Test the graph as code before testing the quality of the model response. A small suite should prove that each transition accepts the right event, rejects events from the wrong phase, and reaches a terminal state. Then test tool and model behavior in separate layers.

Include at least these cases:

  • planning accepts a valid plan and rejects an approval.
  • tool rejects another plan before the call finishes.
  • review reaches done only with a valid approval.
  • done and blocked cannot advance.
  • A retry does not duplicate the external effect.
  • A restored checkpoint keeps a compatible state-schema version.
  • An error path produces an event that can be investigated.

Record execution_id, previous state, event, next state, tool, attempt, and rejection reason. Observability for coding agents in CI shows why structured events make a loop investigable. The same principle applies here: a model narrative does not replace a transition history.

When is a state machine the wrong choice?

Do not use a state machine to give the appearance of control to a process that still has no stopping rule. If states change after every prompt, the graph is not governing the agent. It is only recording improvisation. Define the result contract, allowed effects, and success condition first.

A free-form loop can be right for exploration, brainstorming, open research, or tasks where the next tool depends on a discovery that does not exist yet. Even then, impose a step budget, timeout, tool allowlist, and failure state. Autonomous does not mean unlimited.

For larger systems, a hybrid is often easier to read: a state machine at the important boundaries and a local agent inside an exploratory step. Multi-agent orchestration can then be a state or subworkflow, instead of turning every agent-to-agent message into a new architecture. See multi-agent orchestration for coding agents with TypeScript when the problem is coordination between agents, not the lifecycle of one agent.

Frequently asked questions

Does a state machine make an agent deterministic?

No. It makes states and accepted transitions explicit. Model output remains variable, so it still needs schemas, validation, and tests. The benefit is that a plausible output cannot silently skip approval or call a tool outside its allowed phase.

Do I need LangGraph or Step Functions?

No. Both document architectures that model state, work, and transitions. You can start with a discriminated union and a transition function in TypeScript. Adopt a runtime when persistence, resumption, queues, waiting, or observability stop being a small responsibility in your service.

Does a queue replace a state machine?

No. A queue delivers work and can help distribute attempts. The state machine defines the execution state, valid events, and permitted effects. Many systems use both: the queue transports the event, and the workflow validates the transition.

How do I start without drawing a huge graph?

Choose a flow that already has a pause, an external effect, or a failure that is hard to investigate. Model four states, write the invalid transitions, and add one test per rule. Extract shared states later. If the diagram does not change an execution decision, it is documentation, not control.

Conclusion

A state machine for an AI agent is worth the cost when the system must remember where it is, wait for something, protect an effect, or explain why it did not advance. The model can keep doing uncertain work. The runtime should store state, validate events, limit tools, and end the flow.

Start with the smallest path that contains a real risk. Model the state, make illegal transitions testable, and persist the checkpoint before adding more autonomy. In long Claude Code and Codex sessions, I use RemoteCode as the author's tool for continuing agentic flows. It does not replace state contracts, tool limits, or human review.

Sources consulted