After: You can emit directives from actions when an effect should be owned by the runtime.
Directives are pure descriptions of runtime-owned 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 are not the only place side effects can happen. Jido keeps agent decision logic pure; actions may be pure or effectful. Use a directive when the workflow has already decided on an outbound effect and wants the runtime or integration layer to own delivery.
Directives vs State Operations
Jido separates two distinct concerns:
| Concept | Module | Purpose | Handled By |
|---|---|---|---|
| Directives | Jido.Agent.Directive | Runtime-owned 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 |
StartSensor | Start or replace a tagged sensor runtime | Tracked under {:sensor, tag} |
StopSensor | Stop a tagged sensor runtime | Uses sensor tag |
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)
# Sensor lifecycles
Directive.start_sensor(:market_data, MyApp.MarketDataSensor,
config: %{symbol: "AAPL", interval: 1000},
link?: false
)
Directive.stop_sensor(:market_data)
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 want runtime-owned instruction
execution. 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.
Sensor Lifecycle
StartSensor starts or replaces a tagged Jido.Sensor runtime and tracks it
under {:sensor, tag} in the owning AgentServer. Sensor signals are delivered
back to that owning agent by default.
Managed sensors default to link?: false with explicit owner monitoring:
- if the owning
AgentServerexits, the sensor stops itself - if the sensor exits unexpectedly, the owner receives
jido.agent.sensor.exit - controlled
StopSensorshutdowns do not emitjido.agent.sensor.exit
Use link?: true only for fail-fast input paths where an abnormal sensor exit
should also take down the owning AgentServer. Even then, controlled
replacement and StopSensor unlink before shutdown so intentional lifecycle
changes stay local.
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
Jido.AgentServer.DirectiveExec for your directive type to handle custom
effects.
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