# How to Implement Custom Directives in Jido for External Side Effects

> Learn Jido custom directives to manage external side effects easily. Define structs and implement the DirectiveExec protocol for robust agent behavior.

- Repository: [agentjido/jido](https://github.com/agentjido/jido)
- Tags: how-to-guide
- Published: 2026-03-09

---

**You implement custom directives in Jido for external side effects by defining a struct, implementing the `Jido.AgentServer.DirectiveExec` protocol, and returning the directive from your agent's `cmd/2` function.**

Jido is an Elixir framework for building autonomous agents that enforces a strict separation between pure business logic and impure side effects. While the core library provides built-in directives like `Emit`, `Spawn`, and `Schedule`, you can extend the system with your own directives to handle external operations such as HTTP calls, database writes, or LLM invocations without modifying the core `agentjido/jido` codebase.

## Architecture of Custom Directives in Jido

Jido's directive system follows the Elm/Redux pattern where effects are described by data rather than executed directly. The runtime handles all side effects, keeping your agent code deterministic and testable.

| Component | Role | How It Relates to Custom Directives |
|-----------|------|--------------------------------------|
| **`Jido.Agent.Directive`** | Holds definitions of core directives and documents the extensibility contract. | Shows how a custom struct can be added in a separate namespace (e.g., `MyApp.Directive.CallLLM`). |
| **`Jido.AgentServer.DirectiveExec`** | Protocol that executes directives. Implementations are looked up by the directive's struct type. | You implement `exec/3` for your struct to translate the directive into real side effects. |
| **`Jido.AgentServer.DirectiveExecutors`** | Provides concrete implementations for built-in directives and a fallback for unknown types. | Serves as a reference implementation showing the pattern to follow for your own directive. |
| **`Jido.AgentServer.State`** | Manages how directives are enqueued and how the server maintains the queue. | Useful when you need to manipulate state inside your executor. |

## Step-by-Step: Implement Custom Directives in Jido for External Side Effects

### 1. Define the Directive Struct

Create a struct that describes the external effect you want to perform. While optional, using Zoi for schema validation follows the pattern used by core directives in [`lib/jido/agent/directive.ex`](https://github.com/agentjido/jido/blob/main/lib/jido/agent/directive.ex).

```elixir
defmodule MyApp.Directive.CallLLM do
  @moduledoc """
  Directive to call a Large Language Model (LLM) service.
  """

  @schema Zoi.struct(
    __MODULE__,
    %{
      model: Zoi.any(description: "LLM model name") |> Zoi.optional(),
      prompt: Zoi.string(description: "Prompt to send to the LLM"),
      tag: Zoi.any(description: "Optional correlation tag") |> Zoi.optional()
    },
    coerce: true
  )

  @type t :: unquote(Zoi.type_spec(@schema))
  @enforce_keys Zoi.Struct.enforce_keys(@schema)
  defstruct Zoi.Struct.struct_fields(@schema)

  @doc false
  def schema, do: @schema
end

```

### 2. Implement the DirectiveExec Protocol

Implement the `Jido.AgentServer.DirectiveExec` protocol for your struct in [`lib/jido/agent_server/directive_exec.ex`](https://github.com/agentjido/jido/blob/main/lib/jido/agent_server/directive_exec.ex). The `exec/3` function receives the directive, the triggering signal, and the current server state. It must return one of three allowed tuples:

- `{:ok, state}` for synchronous completion
- `{:async, ref | nil, state}` for asynchronous operations
- `{:stop, reason, state}` to halt the server

```elixir
defimpl Jido.AgentServer.DirectiveExec, for: MyApp.Directive.CallLLM do
  @moduledoc false

  require Logger

  def exec(%{model: model, prompt: prompt, tag: tag}, _input_signal, state) do
    # Example: fire-and-forget an async LLM call

    Task.Supervisor.start_child(
      Jido.TaskSupervisor,
      fn ->
        result = MyApp.LLMClient.call(model || "default-model", prompt)
        # When the LLM finishes, emit a signal back to the agent:

        signal = Jido.Signal.new!("myapp.llm.result", %{result: result, tag: tag})
        Jido.AgentServer.cast(self(), signal)
      end
    )

    # No immediate state change – the async work will later emit a signal.

    {:async, nil, state}
  end
end

```

Reference implementations for built-in directives are available in [`lib/jido/agent_server/directive_executors.ex`](https://github.com/agentjido/jido/blob/main/lib/jido/agent_server/directive_executors.ex), which demonstrates patterns for handling `Emit`, `Spawn`, and other core directives.

### 3. Emit the Directive from Your Agent

Return your custom directive from the agent's `cmd/2` function (or from a strategy) just as you would return a built-in directive. The runtime automatically dispatches it to your protocol implementation.

```elixir
defmodule MyApp.Agent.Example do
  use Jido.Agent, name: "example"

  @impl true
  def cmd(agent, {:ask_llm, prompt}) do
    # Return the directive together with the unchanged agent state

    directive = %MyApp.Directive.CallLLM{prompt: prompt, tag: :question_1}
    {agent, [directive]}
  end

  # Handle the LLM result signal (optional)

  @impl true
  def handle_signal(agent, %Jido.Signal{name: "myapp.llm.result", payload: %{result: r, tag: _}}) do
    # Update state with the answer, then continue

    new_state = Map.put(agent.state, :last_answer, r)
    {%{agent | state: new_state}, []}
  end
end

```

The agent's `cmd/2` remains a pure function—it merely describes the external effect. The `Jido.AgentServer` runtime handles the actual execution via your `DirectiveExec` implementation, maintaining the separation between pure logic and side effects.

## Key Source Files for Custom Directive Implementation

| File | Why It Matters |
|------|----------------|
| [[`lib/jido/agent/directive.ex`](https://github.com/agentjido/jido/blob/main/lib/jido/agent/directive.ex)](https://github.com/agentjido/jido/blob/main/lib/jido/agent/directive.ex) | Defines core directives and documents the extensibility contract showing how custom structs fit into the system. |
| [[`lib/jido/agent_server/directive_exec.ex`](https://github.com/agentjido/jido/blob/main/lib/jido/agent_server/directive_exec.ex)](https://github.com/agentjido/jido/blob/main/lib/jido/agent_server/directive_exec.ex) | Protocol definition (`Jido.AgentServer.DirectiveExec`) that you implement for custom directives; specifies the `exec/3` signature and return values. |
| [[`lib/jido/agent_server/directive_executors.ex`](https://github.com/agentjido/jido/blob/main/lib/jido/agent_server/directive_executors.ex)](https://github.com/agentjido/jido/blob/main/lib/jido/agent_server/directive_executors.ex) | Concrete implementations for built-in directives; serves as the reference pattern for your own `defimpl`. |
| [[`lib/jido/agent_server/state.ex`](https://github.com/agentjido/jido/blob/main/lib/jido/agent_server/state.ex)](https://github.com/agentjido/jido/blob/main/lib/jido/agent_server/state.ex) | Shows how directives are enqueued and how server state is maintained; useful when manipulating state within custom executors. |

These files provide the complete context required to extend Jido with your own side-effect directives without modifying the core library.

## Summary

- **Jido directives** are plain structs that describe external side effects, keeping agent logic pure and testable.
- To **implement custom directives in Jido for external side effects**, define a struct, implement the `Jido.AgentServer.DirectiveExec` protocol, and return the directive from `cmd/2`.
- The protocol's `exec/3` function must return `{:ok, state}`, `{:async, ref, state}`, or `{:stop, reason, state}` to control execution flow.
- Custom directives require no changes to the core `agentjido/jido` codebase—the protocol automatically dispatches to your implementation based on the struct type.

## Frequently Asked Questions

### What is the difference between a directive and a signal in Jido?

A **signal** represents an event or message flowing through the system, often triggering state transitions, while a **directive** is a declarative description of an external side effect that the runtime should execute. Signals are processed by the agent's `handle_signal/2` function, whereas directives are executed by the `Jido.AgentServer.DirectiveExec` protocol implementations.

### Can I implement async operations in custom directives?

Yes, the `Jido.AgentServer.DirectiveExec` protocol explicitly supports asynchronous operations. Your `exec/3` implementation can start background work using `Task.Supervisor.start_child/2` and return `{:async, ref, state}` where `ref` is an optional reference to the async task. When the async work completes, you can emit a signal back to the agent using `Jido.AgentServer.cast/2`.

### Do I need to modify Jido's core code to add custom directives?

No, you never need to modify the core `agentjido/jido` codebase. The `DirectiveExec` protocol is open-ended and uses Elixir's protocol dispatch to automatically route your custom struct to your implementation. Simply define your struct and `defimpl` in your own application or library, and the runtime will handle the rest.

### How do I test custom directives in Jido?

Test custom directives by verifying both the struct creation and the protocol implementation. Unit tests should assert that your `exec/3` function returns the correct tuples (`{:ok, state}`, `{:async, _, state}`, etc.) and that it correctly manipulates the server state. Integration tests can verify that when your directive is returned from `cmd/2`, the runtime correctly dispatches to your implementation, often by capturing the side effects or mocking the external calls.