Actions vs Directives vs State Operations in Jido: A Complete Guide
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 and 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:
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, 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. 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.
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, 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.
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:
- Command Invocation: An agent's
cmd/2function invokes an action with parameters and context. - Action Execution: The action runs synchronously, performing pure computations and returning
{:ok, result_map, operations_list}. - State Application:
Jido.Agent.StateOps.apply_state_ops/2processes the operations list, applying any%StateOp{}structs to the agent's internal state and filtering out directives. - Directive Execution: The runtime (
Jido.AgentServer) receives the updated agent state and the list of directives, executing each external side effect—such as callingDirective.emit/2to 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:
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:
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:
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/*.exthat implement business logic and return descriptions of work to be performed via therun/2callback. - State Operations are immutable structs defined in
lib/jido/agent/state_op.ex(such as%SetState{},%SetPath{}, and%ReplaceState{}) that describe internal state mutations applied atomically byJido.Agent.StateOps.apply_state_ops/2. - Directives are pure descriptions of external side effects defined in
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. 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. 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.
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 →