# FSM Strategy Deep Dive

<!-- covers: jido.strategies.fsm_support -->

[![Run in Livebook](https://livebook.dev/badge/v1/blue.svg)](https://livebook.dev/run?url=https://github.com/agentjido/jido/blob/main/guides/fsm-strategy.livemd)

```elixir
Mix.install([
  {:jido, path: "."}
])
```

## Overview

The FSM (Finite State Machine) Strategy gives an agent an explicit execution
state machine around `cmd/2`.

It keeps `cmd/2` pure by emitting `%Directive.RunInstruction{}` directives for
runtime execution, then handling execution results when the runtime routes them
back through `cmd/2`.

This strategy is useful when you want to:

- track whether an agent is idle or actively processing work
- gate execution so commands only run from valid execution states
- inspect strategy state via `strategy_snapshot/1`

The FSM state lives in `agent.state.__strategy__`.

## Key Contract

The built-in FSM strategy always enters `"processing"` while instructions are
running. That means custom transition maps must include `"processing"` as the
runtime execution state.

If `:auto_transition` is enabled, `"processing"` must also be able to return to
the configured initial state.

Use your regular agent state for domain workflow data such as
`agent.state.order_status`.

## Configuration

```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],
      order_status: [type: :atom, default: :pending],
      shipped_via: [type: :string, default: nil],
      shipped_at: [type: :any, default: nil],
      delivered_at: [type: :any, default: nil]
    ],
    signal_routes: [
      {"order.confirm", ConfirmOrder},
      {"order.ship", ShipOrder},
      {"order.deliver", DeliverOrder}
    ],
    strategy: {Jido.Agent.Strategy.FSM,
      initial_state: "ready",
      transitions: %{
        "ready" => ["processing"],
        "processing" => ["ready"]
      },
      auto_transition: true
    }
end
```

### FSM Strategy Options

| Option | Description | Default |
|--------|-------------|---------|
| `:initial_state` | The starting execution state | `"idle"` |
| `:transitions` | Map of valid execution transitions `%{from => [to_states]}` | Default workflow (see below) |
| `:auto_transition` | Auto-return to the initial state after processing | `true` |

### Default Transitions

If no transitions are provided, the strategy uses:

```elixir
%{
  "idle" => ["processing"],
  "processing" => ["idle", "completed", "failed"],
  "completed" => ["idle"],
  "failed" => ["idle"]
}
```

## Runtime Execution

`MyAgent.cmd/2` stays pure under this strategy. It prepares work and returns
directives; it does not execute instructions inline.

Use `Jido.AgentServer.call/3` to run a signal through the runtime:

```elixir
{:ok, pid} = Jido.AgentServer.start_link(agent: OrderAgent)
{:ok, agent} = Jido.AgentServer.call(pid, signal)
```

The runtime handles `%Directive.RunInstruction{}` for you and feeds the result
back into the strategy.

If you need the settled agent after the runtime finishes draining directives,
fetch it from `Jido.AgentServer.state/1` after the strategy returns to `:idle`.

## Inspecting Strategy State

Use `strategy_snapshot/1` to inspect the FSM execution state:

```elixir
snap = OrderAgent.strategy_snapshot(agent)

snap.status              # :idle or :running
snap.done?               # false for the default runtime flow
snap.result              # last instruction result
snap.details[:fsm_state] # "ready" or "processing"
```

Keep domain state in the agent itself:

```elixir
agent.state[:order_status] # :pending, :confirmed, :shipped, :delivered
```

## Complete Example: Order Fulfillment at Runtime

This example uses the FSM strategy for execution-state tracking while the order
lifecycle itself lives in `agent.state.order_status`.

### Step 1: Define the Actions

```elixir
defmodule ConfirmOrder do
  use Jido.Action,
    name: "confirm_order",
    schema: []

  @impl true
  def run(_params, context) do
    case context.state[:order_status] do
      :pending -> {:ok, %{order_status: :confirmed}}
      other -> {:error, "cannot confirm order from #{inspect(other)}"}
    end
  end
end

defmodule ShipOrder do
  use Jido.Action,
    name: "ship_order",
    schema: [
      carrier: [type: :string, default: "Standard Shipping"]
    ]

  @impl true
  def run(%{carrier: carrier}, context) do
    case context.state[:order_status] do
      :confirmed ->
        {:ok, %{order_status: :shipped, shipped_via: carrier, shipped_at: DateTime.utc_now()}}

      other ->
        {:error, "cannot ship order from #{inspect(other)}"}
    end
  end
end

defmodule DeliverOrder do
  use Jido.Action,
    name: "deliver_order",
    schema: []

  @impl true
  def run(_params, context) do
    case context.state[:order_status] do
      :shipped -> {:ok, %{order_status: :delivered, delivered_at: DateTime.utc_now()}}
      other -> {:error, "cannot deliver order from #{inspect(other)}"}
    end
  end
end
```

### Step 2: Define the Agent

```elixir
defmodule OrderAgent do
  use Jido.Agent,
    name: "order_agent",
    description: "Handles order lifecycle while FSM tracks execution state",
    schema: [
      order_id: [type: :string],
      customer: [type: :string],
      items: [type: {:list, :map}, default: []],
      total: [type: :float, default: 0.0],
      order_status: [type: :atom, default: :pending],
      shipped_via: [type: :string, default: nil],
      shipped_at: [type: :any, default: nil],
      delivered_at: [type: :any, default: nil]
    ],
    signal_routes: [
      {"order.confirm", ConfirmOrder},
      {"order.ship", ShipOrder},
      {"order.deliver", DeliverOrder}
    ],
    strategy: {Jido.Agent.Strategy.FSM,
      initial_state: "ready",
      transitions: %{
        "ready" => ["processing"],
        "processing" => ["ready"]
      },
      auto_transition: true
    }
end
```

### Step 3: Start the Runtime

```elixir
alias Jido.Signal

def wait_for_idle!(pid, timeout_ms \\ 1_000) do
  deadline = System.monotonic_time(:millisecond) + timeout_ms
  do_wait_for_idle(pid, deadline)
end

defp do_wait_for_idle(pid, deadline) do
  {:ok, status} = Jido.AgentServer.status(pid)

  cond do
    status.snapshot.status == :idle ->
      {:ok, state} = Jido.AgentServer.state(pid)
      state.agent

    System.monotonic_time(:millisecond) >= deadline ->
      raise "timed out waiting for FSM runtime to become idle"

    true ->
      Process.sleep(10)
      do_wait_for_idle(pid, deadline)
  end
end

{:ok, pid} =
  Jido.AgentServer.start_link(
    agent: OrderAgent,
    id: "order-001",
    initial_state: %{
      order_id: "ORD-12345",
      customer: "Alice",
      items: [%{sku: "WIDGET-A", qty: 2}],
      total: 49.99,
      order_status: :pending
    }
  )
```

### Step 4: Process the Order

```elixir
{:ok, _agent} =
  Jido.AgentServer.call(pid, Signal.new!("order.confirm", %{}, source: "/guide"))

agent = wait_for_idle!(pid)
snap = OrderAgent.strategy_snapshot(agent)
IO.inspect(agent.state[:order_status], label: "Order status")
IO.inspect(snap.details[:fsm_state], label: "FSM state")
IO.inspect(snap.details[:processed_count], label: "Actions processed")
```

```elixir
{:ok, _agent} =
  Jido.AgentServer.call(
    pid,
    Signal.new!("order.ship", %{carrier: "FedEx Express"}, source: "/guide")
  )

agent = wait_for_idle!(pid)
snap = OrderAgent.strategy_snapshot(agent)
IO.inspect(agent.state[:order_status], label: "Order status after ship")
IO.inspect(agent.state[:shipped_via], label: "Carrier")
IO.inspect(snap.details[:fsm_state], label: "FSM state")
```

```elixir
{:ok, _agent} =
  Jido.AgentServer.call(pid, Signal.new!("order.deliver", %{}, source: "/guide"))

agent = wait_for_idle!(pid)
snap = OrderAgent.strategy_snapshot(agent)
IO.inspect(agent.state[:order_status], label: "Order status after deliver")
IO.inspect(agent.state[:delivered_at], label: "Delivered at")
IO.inspect(snap.details[:fsm_state], label: "FSM state")
```

After each successful runtime call:

- `agent.state.order_status` advances through the business workflow
- `snap.details[:fsm_state]` returns to `"ready"`
- `snap.details[:processed_count]` increments

## Common Pitfall: Missing `"processing"`

The following configuration looks like a business-state FSM, but it is invalid
for the built-in strategy because `"processing"` is missing:

```elixir
defmodule BrokenOrderAgent do
  use Jido.Agent,
    name: "broken_order_agent",
    strategy: {Jido.Agent.Strategy.FSM,
      initial_state: "pending",
      transitions: %{
        "pending" => ["confirmed", "cancelled"],
        "confirmed" => ["shipped", "cancelled"],
        "shipped" => ["delivered"],
        "delivered" => [],
        "cancelled" => []
      },
      auto_transition: false
    }
end
```

Calling `cmd/2` on that agent returns an error directive immediately because the
strategy cannot transition from `"pending"` to `"processing"`:

```elixir
agent = BrokenOrderAgent.new()
{agent, directives} = BrokenOrderAgent.cmd(agent, ConfirmOrder)

IO.inspect(directives, label: "Directive(s)")
IO.inspect(BrokenOrderAgent.strategy_snapshot(agent).details[:fsm_state], label: "FSM state")
```

If you want business-state transitions like `"pending" -> "confirmed"`, model
those in `agent.state` or build a custom strategy on top of this runtime
pattern.

## Summary

| Concept | Purpose |
|---------|---------|
| `strategy: {FSM, opts}` | Configure execution-state tracking around `cmd/2` |
| `"processing"` | Runtime state entered while instructions execute |
| `AgentServer.call/3` | Executes `%Directive.RunInstruction{}` and returns the updated agent |
| `strategy_snapshot/1` | Inspect execution state without reading internals |
| `agent.state` | Store domain workflow data such as `order_status` |

## Next Steps

- **[Agents Guide](agents.md)** - Core agent concepts
- **[Signals Guide](signals.md)** - Route signals into actions
- **[Runtime Guide](runtime.md)** - Run agents in production
