Custom Strategies
View SourceAfter: You can implement a strategy for specialized execution patterns.
defmodule RoundRobinStrategy do
use Jido.Agent.Strategy
alias Jido.Agent.Strategy.State, as: StratState
@impl true
def init(agent, _ctx) do
agent = StratState.put(agent, %{
module: __MODULE__,
status: :idle,
current_index: 0,
total_executed: 0
})
{agent, []}
end
@impl true
def cmd(agent, instructions, _ctx) do
state = StratState.get(agent, %{})
index = Map.get(state, :current_index, 0)
# Execute only the instruction at current index
case Enum.at(instructions, rem(index, length(instructions))) do
nil ->
{agent, []}
instruction ->
instruction = %{instruction | context: Map.put(instruction.context, :state, agent.state)}
case Jido.Exec.run(instruction) do
{:ok, result} ->
agent = Jido.Agent.StateOps.apply_result(agent, result)
agent = StratState.put(agent, %{state |
current_index: index + 1,
total_executed: state.total_executed + 1,
status: :success
})
{agent, []}
{:error, reason} ->
error = Jido.Error.execution_error("Instruction failed", %{reason: reason})
agent = StratState.put(agent, %{state | status: :failure})
{agent, [%Jido.Agent.Directive.Error{error: error, context: :instruction}]}
end
end
end
endUse it in your agent:
defmodule MyAgent do
use Jido.Agent,
name: "round_robin_agent",
strategy: RoundRobinStrategy
endStrategy Responsibilities
Strategies control three things:
- Execution — How
cmd/2processes instructions - Routing — Which signals map to which actions (via
signal_routes/1) - State — Tracking execution progress in
agent.state.__strategy__
Required Callback
cmd/3
@callback cmd(agent :: Agent.t(), instructions :: [Instruction.t()], ctx :: context()) ::
{Agent.t(), [directive()]}This is the only required callback. It receives normalized instructions and must return the updated agent plus any directives.
Optional Callbacks
init/2
Initialize strategy state. Called by AgentServer after new/1.
@impl true
def init(agent, ctx) do
agent = StratState.put(agent, %{
module: __MODULE__,
status: :idle,
my_data: []
})
{agent, []}
endtick/2
Tick-based continuation for multi-step strategies. Called when you schedule a :strategy_tick.
@impl true
def tick(agent, ctx) do
# Continue long-running work
{agent, []}
endsnapshot/2
Return a stable view of strategy state for external inspection.
@impl true
def snapshot(agent, _ctx) do
state = StratState.get(agent, %{})
%Jido.Agent.Strategy.Snapshot{
status: Map.get(state, :status, :idle),
done?: Map.get(state, :status) in [:success, :failure],
result: Map.get(state, :result),
details: %{custom_field: Map.get(state, :custom_field)}
}
endaction_spec/1
Schema for strategy-specific actions. Enables parameter normalization.
@impl true
def action_spec(:my_internal_action) do
%{
schema: [query: [type: :string, required: true]],
doc: "Internal action for this strategy"
}
end
def action_spec(_), do: nilsignal_routes/1
Declare signal-to-action routing handled by the strategy.
@impl true
def signal_routes(_ctx) do
[
{"my_strategy.start", {:strategy_cmd, :start_action}},
{"my_strategy.continue", {:strategy_cmd, :continue_action}}
]
endStrategy.Snapshot
The snapshot struct provides a stable interface for inspecting strategy state:
%Strategy.Snapshot{
status: :idle | :running | :waiting | :success | :failure,
done?: boolean(),
result: term() | nil,
details: map()
}Use Snapshot.terminal?/1 to check if in a terminal state, or Snapshot.running?/1 for active execution.
Strategy State Helpers
The Jido.Agent.Strategy.State module provides helpers for managing agent.state.__strategy__:
alias Jido.Agent.Strategy.State, as: StratState
# Get strategy state (with default)
state = StratState.get(agent, %{})
# Put new strategy state
agent = StratState.put(agent, %{status: :running, data: []})
# Update with function
agent = StratState.update(agent, fn state ->
%{state | counter: state.counter + 1}
end)
# Status helpers
StratState.status(agent) # :idle, :running, :waiting, :success, :failure
StratState.terminal?(agent) # true if :success or :failure
StratState.active?(agent) # true if :running or :waiting
StratState.set_status(agent, :running)
# Clear strategy state
agent = StratState.clear(agent)Minimal Custom Strategy Skeleton
defmodule MyStrategy do
use Jido.Agent.Strategy
alias Jido.Agent.Strategy.State, as: StratState
alias Jido.Agent.StateOps
@impl true
def init(agent, _ctx) do
agent = StratState.put(agent, %{module: __MODULE__, status: :idle})
{agent, []}
end
@impl true
def cmd(agent, instructions, _ctx) do
Enum.reduce(instructions, {agent, []}, fn instruction, {acc, directives} ->
instruction = %{instruction | context: Map.put(instruction.context, :state, acc.state)}
case Jido.Exec.run(instruction) do
{:ok, result} ->
{StateOps.apply_result(acc, result), directives}
{:ok, result, effects} ->
acc = StateOps.apply_result(acc, result)
StateOps.apply_state_ops(acc, List.wrap(effects))
{:error, reason} ->
error = Jido.Error.execution_error("Failed", %{reason: reason})
{acc, directives ++ [%Jido.Agent.Directive.Error{error: error, context: :instruction}]}
end
end)
end
endWhen NOT to Write a Custom Strategy
Don't write a custom strategy if:
- You just need sequential action execution → Use
Direct - You need state machine transitions → Use
FSM - You want to modify action behavior → Write a different Action, not a Strategy
- You want pre/post processing → Use agent hooks (
on_before_cmd/2,on_after_cmd/3) - You want to route signals differently → Use skill
router/1callbacks
Write a custom strategy when:
- You need non-sequential execution (parallel, round-robin, priority-based)
- You're implementing complex control flow (behavior trees, planners)
- You need multi-step execution with ticks (LLM chains, async workflows)
- The execution model itself is the distinguishing feature
Most agents work fine with Direct. The FSM strategy handles 90% of cases that need more. Custom strategies are for the remaining 10%.
See Strategies for an overview of Direct vs FSM.
For FSM-specific patterns, see the FSM Strategy Guide.