After: You can define agents with schemas, hooks, and the cmd/2/cmd/3 contract.
Agents are immutable data structures that hold state and respond to actions. The
core operation is cmd/2 (or cmd/3 with options), which processes actions and
returns an updated agent plus any runtime-owned directives.
Jido keeps agent decision logic pure. Actions may be pure or effectful. Directives are for effects you want the runtime to own.
defmodule MyAgent do
use Jido.Agent,
name: "my_agent", # Required - alphanumeric + underscores
description: "My custom agent", # Optional
category: "example", # Optional
tags: ["demo"], # Default: []
vsn: "1.0.0", # Optional
schema: [ # State schema (see below)
status: [type: :atom, default: :idle],
counter: [type: :integer, default: 0]
],
strategy: Jido.Agent.Strategy.Direct, # Default
plugins: [MyPlugin], # Default: []
default_plugins: true, # Load built-in plugins (Default: true)
schedules: [ # Declarative cron schedules (Default: [])
{"*/5 * * * *", "heartbeat.tick", job_id: :heartbeat}
]
endThe fundamental operation:
{agent, directives} = MyAgent.cmd(agent, action)
{agent, directives} = MyAgent.cmd(agent, action, opts)Key invariants:
- The returned
agentis always complete—no "apply directives" step needed directivesdescribe runtime-owned external effects only—they never modify agent state- Agent decision logic stays explicit and testable
Use an effectful action when the current step needs a result back now to continue reasoning or update state. Use a directive when the workflow has already decided on an outbound effect and wants the runtime or integration layer to own delivery.
Action formats:
# Action module with no params
{agent, directives} = MyAgent.cmd(agent, MyAction)
# Action with params
{agent, directives} = MyAgent.cmd(agent, {MyAction, %{value: 42}})
# Action with params and context
{agent, directives} = MyAgent.cmd(agent, {MyAction, %{value: 42}, %{user_id: 123}})
# Action with params, context, and per-instruction opts
{agent, directives} = MyAgent.cmd(agent, {MyAction, %{value: 42}, %{}, [timeout: 5000]})
# Full instruction struct
{agent, directives} = MyAgent.cmd(agent, %Instruction{action: MyAction, params: %{}})
# List of actions (processed in sequence)
{agent, directives} = MyAgent.cmd(agent, [Action1, {Action2, %{x: 1}}])Execution options via cmd/3:
Pass options that apply to all actions in the command:
# With timeout (5 second limit per action)
{agent, directives} = MyAgent.cmd(agent, MyAction, timeout: 5000)
# With timeout and no retries
{agent, directives} = MyAgent.cmd(agent, MyAction, timeout: 1000, max_retries: 0)
# Options applied to all actions in a list
{agent, directives} = MyAgent.cmd(agent, [Action1, Action2], timeout: 5000)Supported options:
:timeout— Maximum time (in ms) for each action to complete:max_retries— Maximum retry attempts on failure:backoff— Initial backoff time in ms (doubles with each retry)
Deep-merges attributes into agent state:
{:ok, agent} = MyAgent.set(agent, %{status: :running})
{:ok, agent} = MyAgent.set(agent, counter: 5)# Validate state, keeping extra fields
{:ok, agent} = MyAgent.validate(agent)
# Strict mode: only schema-defined fields are kept
{:ok, agent} = MyAgent.validate(agent, strict: true)Set max_state_size to a byte limit when you define an agent:
use Jido.Agent,
name: "bounded_agent",
max_state_size: 10 * 1024 * 1024The default is nil (no limit). Jido measures the complete agent.state with
:erlang.external_size/1, including plugin, thread, and strategy data. This is
an external term size, not a process heap or total memory limit. The calculation
visits the state, so its cost grows with the state. No size calculation runs when
the limit is nil.
set/2, validate/2, and restore/2 return
{:error, %Jido.Error.ValidationError{kind: :state_size}} when the candidate state
exceeds the limit. Error details contain max_state_size and actual_state_size.
MyAgent.new/1 keeps its existing struct return contract and raises this error
for oversized initial state. The base Jido.Agent.new/1 returns a tagged error.
A failed cmd/2 or cmd/3 returns the original agent and one error directive
with context: :state_size. It discards the command's other directives. This
also applies to before/after hooks and custom strategies. An action that has
already performed an external effect cannot be rolled back.
Core state helpers that return a struct, such as StateOps.apply_result/2,
Strategy.State.put/2, and the thread, memory, and plugin state helpers, raise
the same validation error. The command boundary converts it to an error
directive. Jido.Agent.StateBudget.replace/2 is a tagged-result boundary for
custom code that replaces the complete state.
AgentServer checks initial state, command results, and directive results before it accepts them. Runtime references count towards the limit. Persistence checks the complete state after thread and plugin restoration. Leave room for runtime metadata when you choose a budget. If mandatory orphan metadata cannot fit, the runtime stops with a structured shutdown reason.
Jido does not choose which data to delete. The application can compact its state
and retry. Direct Elixir struct or map edits cannot be intercepted; call
validate/2 or a checked state helper before you use such a value. An arbitrary
custom callback that replaces the public API must also use these checks.
Optional callbacks for pure transformations before/after command processing.
Called before action processing. Transform agent or action:
def on_before_cmd(agent, action) do
# Example: log the action being processed
{:ok, agent} = set(agent, %{last_action: inspect(action)})
{:ok, agent, action}
endUse cases:
- Mirror action params into agent state
- Add default params based on current state
- Enforce invariants before execution
Called after action processing. Transform agent or directives:
def on_after_cmd(agent, action, directives) do
# Example: auto-validate after every command
{:ok, agent} = validate(agent)
{:ok, agent, directives}
endUse cases:
- Auto-validate state after changes
- Derive computed fields
- Add invariant checks
Agent state is validated against a schema. Two formats are supported:
use Jido.Agent,
name: "my_agent",
schema: [
status: [type: :atom, default: :idle],
counter: [type: :integer, default: 0],
config: [type: {:map, :atom, :string}, default: %{}]
]use Jido.Agent,
name: "my_agent",
schema: Zoi.object(%{
status: Zoi.atom() |> Zoi.default(:idle),
counter: Zoi.integer() |> Zoi.default(0),
config: Zoi.map() |> Zoi.default(%{})
})Both are handled transparently by the Agent module.
# Create with defaults
agent = MyAgent.new()
# Create with custom ID
agent = MyAgent.new(id: "custom-id")
# Create with initial state
agent = MyAgent.new(state: %{counter: 10})If the module is primarily a durable coordinator for named collaborators, use
Jido.Pod instead of Jido.Agent. Jido.Pod wraps the same agent model and
adds a canonical topology plus a reserved singleton pod plugin.
- Actions — Defining actions that transform agent state
- State Operations — Internal state transitions during
cmd/2 - Directives — External effects emitted by agents
- Strategies — Execution strategies for
cmd/2 - Plugins — Default Plugins — Built-in plugins (identity, thread) and how to override them
- Pods — Manager-led durable topologies built on top of agents
Jido.Agent— Full module documentation