# Directives

<!-- covers: jido.signals_and_directives.directive_effect_boundary jido.signals_and_directives.scheduling_support -->

**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](state-ops.md) for details on `SetState`, `SetPath`, and other state ops.

```elixir
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

| 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

```elixir
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
- `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 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` |

```elixir
# 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.

## 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 `AgentServer` exits, the sensor stops itself
- if the sensor exits unexpectedly, the owner receives `jido.agent.sensor.exit`
- controlled `StopSensor` shutdowns do not emit `jido.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 `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](orphans.md) for the full orphan lifecycle.

## Custom Directives

External packages can define their own directives:

```elixir
defmodule MyApp.Directive.CallLLM do
  defstruct [:model, :prompt, :tag]
end
```

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

```elixir
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](state-ops.md), [Orphans & Adoption](orphans.md)
