# Actions vs Directives vs State Operations in Jido: A Complete Guide

> Understand Jido actions directives and state operations. Learn how Jido agents separate logic from execution for predictable and testable behavior.

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

---

**Actions are pure functions that return immutable descriptions of internal state changes (state operations) and external side effects (directives), enabling Jido agents to separate decision logic from execution while maintaining predictable, testable behavior.**

Jido is an Elixir framework for building autonomous agents that enforces strict separation between pure decision logic and impure side effects. Understanding the distinction between actions, directives, and state operations is fundamental to architecting reliable agent systems in the `agentjido/jido` repository.

## What Are Actions in Jido?

Actions in Jido are pure, synchronous functions that implement a single unit of business logic. Defined in modules like [`lib/jido/actions/status.ex`](https://github.com/agentjido/jido/blob/main/lib/jido/actions/status.ex) and [`lib/jido/actions/control.ex`](https://github.com/agentjido/jido/blob/main/lib/jido/actions/control.ex), actions receive parameters and context, perform computations, and return descriptions of work to be performed without executing side effects themselves.

To create an action, you `use Jido.Action` and implement the `run/2` callback:

```elixir
defmodule MyApp.Actions.Example do
  use Jido.Action,
    name: "example",
    description: "Demonstrates action structure",
    schema: [value: [type: :integer]]

  def run(%{value: val}, _ctx) do
    # Return {:ok, result_map, operations_list}

    {:ok, %{computed: val * 2}, []}
  end
end

```

Actions never perform side effects directly. Instead, they return a tuple of `{:ok, result_map, operations_list}`, where `operations_list` may contain **state operations** (for internal state changes) and **directives** (for external side effects).

## Understanding State Operations in Jido

State operations are immutable data structures that describe how an agent's internal state should change. Defined in [`lib/jido/agent/state_op.ex`](https://github.com/agentjido/jido/blob/main/lib/jido/agent/state_op.ex), these structs represent pure descriptions of state transitions without performing the mutation themselves. They ensure that state changes remain explicit, auditable, and atomic.

The framework provides several state operation types:

- `%SetState{}` – Merges attributes into the existing state map
- `%ReplaceState{}` – Completely replaces the state with a new map
- `%DeleteKeys{}` – Removes specific keys from the state
- `%SetPath{}` – Updates a nested value using path notation
- `%DeletePath{}` – Removes a value at a specific path

State operations are applied by `Jido.Agent.StateOps.apply_state_ops/2` in [`lib/jido/agent/state_ops.ex`](https://github.com/agentjido/jido/blob/main/lib/jido/agent/state_ops.ex). This function walks the list of operations returned by an action, applies any `%Jido.Agent.StateOp{}` structs to the current agent state, and collects the remaining structs (directives) for the runtime to execute.

```elixir
alias Jido.Agent.StateOp

# Create a state operation that sets the :status key

op = StateOp.set_state(%{status: :working, retries: 0})

# In an action, you would return:

{:ok, %{}, [op]}

```

## What Are Directives in Jido?

Directives are pure descriptions of external side effects that the runtime (`Jido.AgentServer`) must perform. Defined in [`lib/jido/agent/directive.ex`](https://github.com/agentjido/jido/blob/main/lib/jido/agent/directive.ex), directives represent intentions to interact with the outside world—such as sending signals, spawning processes, or scheduling messages—without executing those effects within the action itself.

Common directive types include:

- `%Emit{}` – Dispatches a signal to external systems via pub/sub, HTTP, or other adapters
- `%Spawn{}` – Creates a child process or sub-agent
- `%Schedule{}` – Schedules a future message or action invocation

Directives are never used to modify the agent's internal state; they are exclusively for external communication. When an action returns directives in the operations list, `StateOps.apply_state_ops/2` passes them through unchanged (since they are not state operations), and the runtime executes them after state changes are applied.

```elixir
alias Jido.{Agent.Directive, Signal}

# Create a signal and wrap it in an emit directive

signal = Signal.new!("user.created", %{id: 123})
directive = Directive.emit(signal, {:pubsub, topic: "users"})

# Return from an action:

{:ok, %{}, [directive]}

```

## How Actions, State Operations, and Directives Work Together

The execution flow in Jido follows a strict pipeline that maintains purity while enabling side effects. This architecture ensures that decision logic remains testable without external dependencies, while the runtime handles all mutations and external interactions.

The flow proceeds as follows:

1. **Command Invocation**: An agent's `cmd/2` function invokes an action with parameters and context.
2. **Action Execution**: The action runs synchronously, performing pure computations and returning `{:ok, result_map, operations_list}`.
3. **State Application**: `Jido.Agent.StateOps.apply_state_ops/2` processes the operations list, applying any `%StateOp{}` structs to the agent's internal state and filtering out directives.
4. **Directive Execution**: The runtime (`Jido.AgentServer`) receives the updated agent state and the list of directives, executing each external side effect—such as calling `Directive.emit/2` to dispatch signals or processing `%Spawn{}` directives to create child processes.

This three-tier architecture ensures that **state mutation stays internal** while **external effects are clearly isolated**. Because actions remain pure functions that merely return descriptions of work, they can be unit-tested without mocking external dependencies or running a full agent server.

## Practical Code Examples

### Combined State Changes and Side Effects

The following action demonstrates returning both state operations and directives, similar to patterns found in [`lib/jido/actions/status.ex`](https://github.com/agentjido/jido/blob/main/lib/jido/actions/status.ex):

```elixir
defmodule MyApp.Actions.Increment do
  use Jido.Action,
    name: "increment",
    description: "Increment counter and broadcast change",
    schema: [by: [type: :integer, default: 1]]

  alias Jido.{Agent, Agent.Directive, Agent.StateOp, Signal}

  def run(%{by: by}, _ctx) do
    # State operation: update the counter

    current = Agent.State.get(:counter, 0)
    state_op = StateOp.set_path([:counter], current + by)

    # Directive: emit signal to external system

    signal = Signal.new!("counter.updated", %{value: current + by})
    directive = Directive.emit(signal, {:pubsub, topic: "counters"})

    {:ok, %{}, [state_op, directive]}
  end
end

```

### Pure External Effects

Actions can emit directives without touching state, as seen in [`lib/jido/actions/control.ex`](https://github.com/agentjido/jido/blob/main/lib/jido/actions/control.ex):

```elixir
defmodule MyApp.Actions.Ping do
  use Jido.Action,
    name: "ping",
    description: "Send health check to external service",
    schema: []

  alias Jido.{Agent.Directive, Signal}

  def run(_params, _ctx) do
    signal = Signal.new!("health.ping", %{timestamp: System.utc_now()})
    
    {:ok, %{}, [
      Directive.emit(signal, {:http, url: "https://api.example.com/health"})
    ]}
  end
end

```

### Pure State Changes

For internal state mutations without side effects:

```elixir
defmodule MyApp.Actions.Reset do
  use Jido.Action,
    name: "reset",
    description: "Reset all counters to zero",
    schema: []

  alias Jido.Agent.StateOp

  def run(_params, _ctx) do
    op = StateOp.replace_state(%{counter: 0, status: :idle, retries: 0})
    
    {:ok, %{}, [op]}
  end
end

```

## Summary

- **Actions** are pure functions defined in modules like `lib/jido/actions/*.ex` that implement business logic and return descriptions of work to be performed via the `run/2` callback.
- **State Operations** are immutable structs defined in [`lib/jido/agent/state_op.ex`](https://github.com/agentjido/jido/blob/main/lib/jido/agent/state_op.ex) (such as `%SetState{}`, `%SetPath{}`, and `%ReplaceState{}`) that describe internal state mutations applied atomically by `Jido.Agent.StateOps.apply_state_ops/2`.
- **Directives** are pure descriptions of external side effects defined in [`lib/jido/agent/directive.ex`](https://github.com/agentjido/jido/blob/main/lib/jido/agent/directive.ex) (such as `%Emit{}` and `%Spawn{}`) that the runtime executes only after state changes are successfully applied.
- This three-tier architecture ensures that decision logic remains pure and testable, state mutations are explicit and atomic, and external effects are clearly isolated from business logic.

## Frequently Asked Questions

### Can an action return both state operations and directives simultaneously?

Yes, actions frequently return both types in the same list. When you return `{:ok, %{}, [state_op, directive]}`, the `Jido.Agent.StateOps.apply_state_ops/2` function applies the state operation to the agent's internal state and passes the directive through to the runtime for execution. This allows a single action to update internal counters while simultaneously emitting signals to external systems.

### What happens if a state operation fails during application?

State operations are applied synchronously by `apply_state_ops/2` in [`lib/jido/agent/state_ops.ex`](https://github.com/agentjido/jido/blob/main/lib/jido/agent/state_ops.ex). If an operation attempts an invalid state transition (such as setting a path that doesn't exist when strict validation is enabled), the function returns an error tuple that halts the action pipeline. This prevents partial state mutations and ensures atomicity—either all valid state operations apply successfully, or the agent state remains unchanged and directives are not executed.

### Are directives executed immediately when returned from an action?

No, directives are not executed immediately. They are pure data structures returned by the action's `run/2` function, collected in a list, and filtered from state operations by `StateOps.apply_state_ops/2`. Only after the agent's internal state has been updated does the runtime (typically `Jido.AgentServer`) iterate through the directive list and execute each side effect—such as calling `Directive.emit/2` to dispatch signals or processing `%Spawn{}` directives to create child processes.

### How do I create custom directives for external integrations?

Custom directives are created by defining structs that conform to the `Jido.Agent.Directive.t()` type shape defined in [`lib/jido/agent/directive.ex`](https://github.com/agentjido/jido/blob/main/lib/jido/agent/directive.ex). You define a struct with the necessary fields for your external integration (such as HTTP endpoints, database connections, or message queue topics), then implement the execution logic in the runtime or via a behavior callback. The runtime pattern-matches on the directive struct type to determine which execution path to take, allowing you to extend Jido's side-effect capabilities without modifying the core action logic or polluting pure decision functions with impure code.