Directives
View SourceAfter: You can emit directives from actions to perform effects without polluting pure logic.
Directives are pure descriptions of external effects. Agents emit them from cmd/2 callbacks; the runtime (AgentServer) executes them.
Key principle: Directives never mutate state directly; state changes happen through cmd/2 return values (including result_action callbacks used by RunInstruction).
Directives vs State Operations
Jido separates two distinct concerns:
| Concept | Module | Purpose | Handled By |
|---|---|---|---|
| Directives | Jido.Agent.Directive | External effects (emit signals, spawn processes) | Runtime (AgentServer) |
| State Operations | Jido.Agent.StateOp | Internal state transitions (set, replace, delete) | Strategy layer |
State operations are applied during cmd/2 and never leave the strategy layer. Directives are passed through to the runtime for execution. See the State Operations guide for details on SetState, SetPath, and other state ops.
def cmd({:notify_user, message}, agent, _context) do
signal = Jido.Signal.new!("notification.sent", %{message: message}, source: "/agent")
{:ok, agent, [Directive.emit(signal)]}
endCore Directives
| Directive | Purpose | Tracking |
|---|---|---|
Emit | Dispatch a signal via configured adapters | — |
Error | Signal an error from cmd/2 | — |
Spawn | Spawn generic BEAM child process | None (fire-and-forget) |
SpawnAgent | Spawn child Jido agent with hierarchy | Full (monitoring, exit signals, restart: :transient default) |
AdoptChild | Attach an orphaned or unattached child to the current parent | Full (monitoring, parent ref refresh, children map update) |
StopChild | Gracefully stop and remove a tracked child agent | Uses children map |
Schedule | Schedule a delayed message | — |
RunInstruction | Execute %Instruction{} at runtime and route result back through cmd/2 | — |
Stop | Stop the agent process (self) | — |
Cron | Recurring scheduled execution | — |
CronCancel | Cancel a cron job | — |
Helper Constructors
alias Jido.Agent.Directive
# Emit signals
Directive.emit(signal)
Directive.emit(signal, {:pubsub, topic: "events"})
Directive.emit_to_pid(signal, pid)
Directive.emit_to_parent(agent, signal)
# Spawn processes
Directive.spawn(child_spec)
Directive.spawn_agent(MyWorkerAgent, :worker_1)
Directive.spawn_agent(MyWorkerAgent, :processor, opts: %{initial_state: %{batch_size: 100}})
Directive.spawn_agent(MyWorkerAgent, :durable, restart: :permanent)
Directive.adopt_child("worker-123", :recovered_worker)
Directive.adopt_child(child_pid, :recovered_worker, meta: %{restored: true})
# Stop processes
Directive.stop_child(:worker_1)
Directive.stop()
Directive.stop(:shutdown)
# Scheduling
Directive.schedule(5000, :timeout)
Directive.cron("*/5 * * * *", :tick, job_id: :heartbeat)
Directive.cron_cancel(:heartbeat)
# Runtime instruction execution
Directive.run_instruction(instruction, result_action: :fsm_instruction_result)
# Errors
Directive.error(Jido.Error.validation_error("Invalid input"))Cron and CronCancel Semantics
Cron and CronCancel are failure-isolated:
- Invalid cron expression or timezone is rejected at runtime without crashing the agent
- Scheduler registration failures return errors and leave agent state unchanged
CronCancelis safe when runtime pid is missing; durable spec removal still applies
For keyed InstanceManager lifecycles with storage enabled, dynamic cron mutations are
write-through durable via Jido.Persist/Jido.Storage before state commit.
Non-persistent lifecycles keep cron state runtime-only.
RunInstruction
RunInstruction is used by strategies that keep cmd/2 pure. Instead of calling
Jido.Exec.run/1 inline, the strategy emits %Directive.RunInstruction{} and the
runtime executes it, then routes the result back through cmd/2 using result_action.
Spawn vs SpawnAgent
Spawn | SpawnAgent |
|---|---|
| Generic Tasks/GenServers | Child Jido agents |
| Fire-and-forget | Full hierarchy tracking |
| No monitoring | Monitors child, receives exit signals |
| — | Enables emit_to_parent/3 |
# Fire-and-forget task
Directive.spawn({Task, :start_link, [fn -> send_webhook(url) end]})
# Tracked child agent
Directive.spawn_agent(WorkerAgent, :worker_1, opts: %{initial_state: state})SpawnAgent forwards standard child startup options such as :id,
:initial_state, and :on_parent_death. It does not install
InstanceManager lifecycle features, so lifecycle/persistence options like
:storage, :idle_timeout, :lifecycle_mod, :pool, :pool_key, and
:restored_from_storage are rejected.
SpawnAgent children default to restart: :transient, which means:
Directive.stop_child/2cleanly removes them- abnormal exits still restart the child
- callers can override to
:permanentor:temporarywhen needed
Children spawned this way can later become orphaned if on_parent_death is set
to :continue or :emit_orphan. In that case, Directive.adopt_child/3 is
the explicit way to reattach the live child to a new logical parent. Jido keeps
the active logical binding in Jido.RuntimeStore, so child restarts continue
to use the current parent relationship after adoption.
Parent-Aware Communication
Directive.emit_to_parent/3 is intentionally strict:
- it works only while
agent.state.__parent__is present - it returns
nilfor standalone agents - it returns
nilfor orphaned agents after the runtime clears__parent__
That prevents stale routing to a dead coordinator. If a child needs to remember
where it came from after orphaning, read agent.state.__orphaned_from__ or
handle jido.agent.orphaned instead of relying on emit_to_parent/3.
See Orphans & Adoption for the full orphan lifecycle.
Custom Directives
External packages can define their own directives:
defmodule MyApp.Directive.CallLLM do
defstruct [:model, :prompt, :tag]
endThe runtime dispatches on struct type — no core changes needed. Implement a custom AgentServer or middleware to handle your directive types.
Complete Example: Action → Directive Flow
Here's a full example showing an action that processes an order and emits a signal:
defmodule ProcessOrderAction do
use Jido.Action,
name: "process_order",
schema: [order_id: [type: :string, required: true]]
alias Jido.Agent.{Directive, StateOp}
def run(%{order_id: order_id}, context) do
signal = Jido.Signal.new!(
"order.processed",
%{order_id: order_id, processed_at: DateTime.utc_now()},
source: "/orders"
)
{:ok, %{order_id: order_id}, [
StateOp.set_state(%{last_order: order_id}), # Applied by strategy
Directive.emit(signal) # Passed to runtime
]}
end
endWhen the agent runs this action via cmd/2:
- The strategy applies
StateOp.set_state— agent state is updated - The
Emitdirective passes through to the runtime AgentServerdispatches the signal via configured adapters
See Jido.Agent.Directive moduledoc for the complete API reference.
Related guides: State Operations, Orphans & Adoption