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 modify agent state — state changes happen in the returned agent struct.

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)
StopChildGracefully stop a tracked child agentUses children map
ScheduleSchedule a delayed message
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}})

# 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)

# Errors
Directive.error(Jido.Error.validation_error("Invalid input"))

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})

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