# How the FSM Strategy Powers State-Driven Workflows in Jido

> Learn how Jido's FSM strategy creates pure declarative finite state machines to drive agent workflows with explicit states and transitions validated against user rules.

- Repository: [agentjido/jido](https://github.com/agentjido/jido)
- Tags: deep-dive
- Published: 2026-03-09

---

**The FSM strategy provides a pure, declarative finite state machine that drives agent workflows through explicit states and transitions, validating each change against user-defined rules while emitting execution directives.**

The Jido agent framework implements state-driven workflows through its declarative FSM strategy, defined in [`lib/jido/agent/strategy/fsm.ex`](https://github.com/agentjido/jido/blob/main/lib/jido/agent/strategy/fsm.ex). This strategy enables developers to model complex business processes—like order fulfillment or task pipelines—as explicit state machines that transition predictably between defined statuses while maintaining immutable agent state.

## Core Architecture and Data Structures

The FSM strategy centers on a **Machine struct** that maintains workflow state without side effects. According to the Jido source code, this struct tracks the current `status` as a string, a `processed_count` for batch operations, optional `last_result` and `error` fields, and a `transitions` map that defines valid state movements.

### Strategy Configuration Options

When configuring an agent with the FSM strategy, three primary options control behavior:

- **`:initial_state`** — Sets the starting state string (defaults to `"idle"`).
- **`:transitions`** — A map defining valid movements where each key is a state string and each value is a list of allowable next states.
- **`:auto_transition`** — Boolean controlling whether the machine returns to the `initial_state` after completing a batch (defaults to `true`).

These options are validated during agent initialization via the `init/2` function, which builds the Machine struct and stores it within the agent's strategy state (`StratState`).

## Execution Flow and State Transitions

The FSM strategy processes workflows through a five-stage execution pipeline that maintains pure functional semantics while integrating with Jido's directive-based runtime.

### 1. Initialization and Command Entry

When `cmd/2` receives a list of instructions, the FSM immediately attempts to transition from the current state to `"processing"` via `Machine.transition/2`. This validation step ensures the workflow can legally enter the execution phase before any side effects occur.

If the transition succeeds, the strategy emits a `%Directive.RunInstruction{}` targeting the first instruction in the batch, tagged with the result-action `:fsm_instruction_result` to enable callback routing.

### 2. Instruction Result Handling

After instruction execution, `handle_instruction_result/3` processes the payload—expecting either `%{status: :ok, result: ..., effects: ...}` for success or error details for failure. This function updates the Machine's `processed_count`, stores the `last_result` or `error`, and applies any state operations via `StateOps.apply_result/2` and `StateOps.apply_state_ops/2`.

During this phase, the strategy accumulates **deferred directives** (such as side-effects) that must execute after the entire batch completes.

### 3. Batch Finalization and Auto-Transition

The `finalize_batch/2` function concludes processing by optionally invoking `maybe_auto_transition/3`. When `:auto_transition` is enabled (the default), the machine shifts back to the `initial_state` after directives are emitted, making the agent ready for the next command batch.

The final Machine struct is persisted back to the strategy state, ensuring state continuity across command cycles.

## Configuring State-Driven Agents

To implement a state-driven workflow, define an agent module using the `Jido.Agent` macro with the FSM strategy tuple.

### Defining Valid State Transitions

The following example models an order processing workflow with explicit transition rules preventing invalid state jumps (such as shipping a cancelled order):

```elixir
defmodule OrderAgent do
  use Jido.Agent,
    name: "order_agent",
    schema: [
      order_id: [type: :string],
      customer: [type: :string],
      items: [type: {:list, :map}, default: []],
      total: [type: :float, default: 0.0]
    ],
    strategy: {Jido.Agent.Strategy.FSM,
      initial_state: "pending",
      transitions: %{
        "pending" => ["confirmed", "cancelled"],
        "confirmed" => ["shipped", "cancelled"],
        "shipped" => ["delivered"],
        "delivered" => [],
        "cancelled" => []
      },
      auto_transition: false
    }
end

```

This configuration lives in the agent module and is processed by the strategy's `init/2` function to construct the initial Machine struct.

### Executing Commands and Observing State

Drive the workflow forward using `cmd/2` and inspect the current state via `strategy_snapshot/1`, which returns a lightweight view containing `status`, `done?`, `result`, and detailed FSM internals:

```elixir

# Initialize agent

agent = OrderAgent.new(
  id: "order-001",
  state: %{order_id: "ORD-12345", customer: "Alice", total: 49.99}
)

# Confirm order: pending → confirmed

{agent, _} = OrderAgent.cmd(agent, ConfirmOrder)
snap = OrderAgent.strategy_snapshot(agent)
IO.inspect(snap.details[:fsm_state])  # "confirmed"

# Ship order: confirmed → shipped  

{agent, _} = OrderAgent.cmd(agent, {ShipOrder, %{carrier: "FedEx"}})
IO.inspect(OrderAgent.strategy_snapshot(agent).details[:fsm_state])  # "shipped"

```

The `snapshot/2` implementation maps raw string statuses to coarse atoms (`:idle`, `:running`, `:success`, `:failure`) for easier pattern matching while exposing the raw `fsm_state` string for domain-specific logic.

## Validation and Error Handling

All state transitions are validated against the user-supplied `:transitions` map at runtime. When `Machine.transition/2` encounters an invalid movement—such as attempting to ship a cancelled order—it returns an error tuple that `cmd/2` converts into a `%Directive.Error{}` via `Error.execution_error/2`.

The FSM state remains unchanged during validation failures, providing atomic safety:

```elixir

# Attempt invalid transition

{agent, directives} = OrderAgent.cmd(cancelled_agent, ShipOrder)

# Verify error directive returned

[%Directive.Error{error: %Error{}}] = directives

# State remains "cancelled"

"cancelled" = OrderAgent.strategy_snapshot(agent).details[:fsm_state]

```

## Observability and Thread Integration

For production workflows requiring audit trails, the FSM strategy integrates with Jido's `ThreadAgent` system. When configured with `thread?: true`, the strategy records each state change as a checkpoint event using `maybe_append_checkpoint/3` and `append_checkpoint/3`, capturing both the `:event` type and current `:fsm_state`.

This checkpointing occurs without violating the pure `cmd/2` contract, as thread effects are handled as deferred directives that execute after the batch finalizes. The implementation enables full workflow replay and debugging while maintaining the strategy's functional core.

## Summary

- The **FSM strategy** in [`lib/jido/agent/strategy/fsm.ex`](https://github.com/agentjido/jido/blob/main/lib/jido/agent/strategy/fsm.ex) implements a pure finite state machine that validates transitions against user-defined rules before executing instructions.
- The **Machine struct** tracks `status`, `processed_count`, and `transitions`, while `cmd/2` orchestrates the execution flow through initialization, processing, and finalization stages.
- **Invalid transitions** produce `%Directive.Error{}` results without mutating the FSM state, ensuring workflow integrity.
- The **`strategy_snapshot/1`** function exposes normalized status atoms and raw FSM details for external monitoring.
- **Thread integration** via `maybe_append_checkpoint/3` enables observable, replayable workflows without side-effecting the pure execution path.

## Frequently Asked Questions

### How do I configure the initial state and allowed transitions for an FSM agent?

Define the `strategy` option in your agent module using the tuple `{Jido.Agent.Strategy.FSM, opts}`, where `opts` includes `:initial_state` as a string and `:transitions` as a map of state strings to lists of valid next states. The strategy's `init/2` function validates this configuration during agent initialization and constructs the internal Machine struct.

### What happens when an agent attempts an invalid state transition?

The `Machine.transition/2` function validates the requested move against the configured `:transitions` map. If invalid, it returns an error that `cmd/2` wraps in a `%Directive.Error{}` directive, and the agent's FSM state remains unchanged. This atomic validation prevents illegal workflow states without partial execution.

### How does the auto_transition option affect batch processing?

When `:auto_transition` is `true` (the default), the `maybe_auto_transition/3` function in `finalize_batch/2` automatically moves the FSM back to the `:initial_state` after completing a command batch. Setting this to `false` keeps the agent in the terminal state of the transition chain, useful for workflows requiring explicit reset actions.

### How can I observe the current FSM state during workflow execution?

Call `strategy_snapshot/1` on your agent module to receive a struct containing `status` (mapped to atoms like `:running` or `:success`), `done?` boolean, and a `details` keyword list with the raw `fsm_state` string and `processed_count`. This function reads the Machine struct from the agent's strategy state without exposing internal implementation details.