Directives

View Source

After: 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:

ConceptModulePurposeHandled By
DirectivesJido.Agent.DirectiveExternal effects (emit signals, spawn processes)Runtime (AgentServer)
State OperationsJido.Agent.StateOpInternal 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)]}
end

Core Directives

DirectivePurposeTracking
EmitDispatch a signal via configured adapters
ErrorSignal an error from cmd/2
SpawnSpawn generic BEAM child processNone (fire-and-forget)
SpawnAgentSpawn child Jido agent with hierarchyFull (monitoring, exit signals, restart: :transient default)
AdoptChildAttach an orphaned or unattached child to the current parentFull (monitoring, parent ref refresh, children map update)
StopChildGracefully stop and remove a tracked child agentUses children map
ScheduleSchedule a delayed message
RunInstructionExecute %Instruction{} at runtime and route result back through cmd/2
StopStop the agent process (self)
CronRecurring scheduled execution
CronCancelCancel 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
  • CronCancel is 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

SpawnSpawnAgent
Generic Tasks/GenServersChild Jido agents
Fire-and-forgetFull hierarchy tracking
No monitoringMonitors 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/2 cleanly removes them
  • abnormal exits still restart the child
  • callers can override to :permanent or :temporary when 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 nil for standalone agents
  • it returns nil for 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]
end

The 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
end

When the agent runs this action via cmd/2:

  1. The strategy applies StateOp.set_state — agent state is updated
  2. The Emit directive passes through to the runtime
  3. AgentServer dispatches the signal via configured adapters

See Jido.Agent.Directive moduledoc for the complete API reference.

Related guides: State Operations, Orphans & Adoption