How to Implement Lifecycle Hooks (on_before_cmd and on_after_cmd) in Jido Agents
Override the optional on_before_cmd/2 and on_after_cmd/3 callbacks in your Jido.Agent module to execute pure transformations before action normalization and after directive generation, ensuring all logic remains side-effect free.
Jido agents function as immutable data structures, meaning the only way to intercept and modify command processing is through the optional lifecycle callbacks defined in the Jido.Agent behaviour. By implementing on_before_cmd/2 and on_after_cmd/3 in your agent module, you can inject deterministic transformations at critical execution points without violating the framework's purity constraints. These hooks integrate directly with the cmd/2 function in lib/jido/agent.ex (lines 815 and 1059), allowing you to enrich actions and directives while maintaining the agent's functional core.
Understanding the Lifecycle Hook Architecture
The lifecycle hooks are declared as optional callbacks in lib/jido/agent.ex (lines 283-298) with default no-op implementations that simply pass data through. When you create an agent with use Jido.Agent, the macro injects defoverridable for these functions, allowing your module to replace the defaults.
def on_before_cmd(agent, action), do: {:ok, agent, action}
def on_after_cmd(agent, _action, directives), do: {:ok, agent, directives}
The critical constraint is that both callbacks must remain pure functions—they cannot send messages, start processes, or perform I/O. They must return a tuple wrapped in {:ok, ...}; any other return value triggers a runtime error.
Implementing on_before_cmd/2
When It Runs
The on_before_cmd/2 callback executes once at line 815 in lib/jido/agent.ex, immediately after cmd/2 is called but before the action is normalized. This is the ideal location to transform the incoming action tuple or modify the agent's state to inject default parameters or enforce invariants.
Code Example: Injecting Timestamps
defmodule MyApp.Agent do
use Jido.Agent,
name: "my_agent",
schema: [
last_query: [type: :integer, default: 0]
]
def on_before_cmd(agent, {action, params}) do
enriched = Map.put(params, :received_at, DateTime.utc_now())
{:ok, agent, {action, enriched}}
end
end
This hook pattern matches the action as a tuple {action_module, params_map}, performs a pure transformation to add metadata, and returns the enriched action wrapped in the required {:ok, agent, action} format.
Implementing on_after_cmd/3
When It Runs
The on_after_cmd/3 callback executes once after the strategy has produced the final list of directives. According to the source in lib/jido/agent.ex at line 1059, the private helper __do_after_cmd__/3 invokes this hook, passing the current agent, the original action, and the generated directives.
Code Example: Validation and Directive Enrichment
defmodule MyApp.Agent do
use Jido.Agent,
name: "my_agent",
schema: [
counter: [type: :integer, default: 0]
]
def on_after_cmd(agent, _action, directives) do
log_directive = %Jido.Agent.Directive.Emit{
signal: {:log, "command processed"}
}
case Jido.Agent.validate(agent) do
{:ok, valid_agent} ->
{:ok, valid_agent, [log_directive | directives]}
{:error, _reason} ->
{:ok, agent, directives}
end
end
end
This example demonstrates adding a derived directive to the list while optionally running pure validation logic. The function must return {:ok, updated_agent, final_directives} to satisfy the contract expected by the runtime.
Complete Implementation with Both Hooks
For agents requiring both pre-processing and post-processing, implement both callbacks in the same module:
defmodule MyApp.Agent do
use Jido.Agent,
name: "my_agent",
schema: [
last_user: [type: :string, default: ""],
request_count: [type: :integer, default: 0]
]
def on_before_cmd(agent, {action, %{user: user} = params}) do
new_agent = %{
agent |
state: Map.put(agent.state, :last_user, user)
}
{:ok, new_agent, {action, params}}
end
def on_after_cmd(agent, _action, directives) do
new_state = Map.update!(agent.state, :request_count, &(&1 + 1))
metric = %Jido.Agent.Directive.Emit{
signal: {:metric, :requests, new_state.request_count}
}
{:ok, %{agent | state: new_state}, [metric | directives]}
end
end
This full implementation captures the issuing user before command processing and emits a metric directive after the strategy completes, all while maintaining pure functional semantics.
Critical Constraints and Runtime Behavior
Purity Requirements
Both hooks must be deterministic and side-effect free. The Jido runtime expects these functions to behave like mathematical transformations—given the same inputs, they must always produce the same outputs without altering external state.
Hard Stops and AgentServer
As documented in lib/jido/agent_server.ex, lifecycle hooks are not executed when a directive returns {:stop, ...}. This hard stop mechanism bypasses on_after_cmd/3 to ensure immediate termination, preventing the after-hook from running during shutdown sequences.
Summary
- Override
on_before_cmd/2inlib/jido/agent.exto transform actions before normalization at line 815, returning{:ok, agent, action}. - Override
on_after_cmd/3to modify directives after strategy completion via__do_after_cmd__/3at line 1059, returning{:ok, agent, directives}. - Maintain purity—hooks must not perform side effects, send messages, or execute I/O operations.
- Handle the action tuple
{action_module, params}in before-hooks and the directives list in after-hooks. - Respect hard stops—when
AgentServerprocesses a{:stop, ...}directive, the after-hook is skipped.
Frequently Asked Questions
Can lifecycle hooks perform side effects like database calls?
No, both on_before_cmd/2 and on_after_cmd/3 must remain pure functions. They cannot perform side effects such as sending messages, starting processes, or doing I/O. Any impure operations will violate the Jido agent contract and may cause unpredictable behavior in the AgentServer runtime.
What return format is required for lifecycle hooks?
Both callbacks must return a tuple wrapped in {:ok, ...}. Specifically, on_before_cmd/2 must return {:ok, agent, action} and on_after_cmd/3 must return {:ok, agent, directives}. Any other return value will cause a runtime error when invoked by the cmd/2 function at lines 815 or 1059 in lib/jido/agent.ex.
Do hooks execute when an agent server receives a stop directive?
According to lib/jido/agent_server.ex, lifecycle hooks are not run when a directive returns {:stop, ...}. The on_after_cmd/3 hook specifically runs after the strategy produces the final list of directives, but hard stops bypass this mechanism to ensure immediate termination.
How do I access the action parameters in on_before_cmd?
The action parameter arrives as a tuple {action, params} where action is the action module and params is a map. You can pattern match on this structure to inject or modify values, as shown in the timestamp injection example, before returning the transformed tuple in the {:ok, agent, {action, modified_params}} format.
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 →