How the FSM Strategy Powers State-Driven Workflows in Jido
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. 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 theinitial_stateafter completing a batch (defaults totrue).
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):
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:
# 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:
# 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.eximplements a pure finite state machine that validates transitions against user-defined rules before executing instructions. - The Machine struct tracks
status,processed_count, andtransitions, whilecmd/2orchestrates 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/1function exposes normalized status atoms and raw FSM details for external monitoring. - Thread integration via
maybe_append_checkpoint/3enables 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →