# How Signal Routing Works in Jido AgentServer: A Complete Technical Guide

> Understand Jido AgentServer signal routing. Explore the unified trie-based router that matches signals and dispatches them to action modules for efficient agent communication.

- Repository: [agentjido/jido](https://github.com/agentjido/jido)
- Tags: deep-dive
- Published: 2026-03-09

---

**Signal routing in Jido AgentServer uses a unified trie-based router built from strategy, agent, and plugin routes to match incoming signals and dispatch them to specific action modules.**

When a signal reaches a running agent in the [Jido](https://github.com/agentjido/jido) framework, the `Jido.AgentServer` must decide which actions to execute. This article explains how signal routing in Jido AgentServer works, from building the router at startup to processing individual signals through the trie-based matching system.

## Building the Signal Router

The router is constructed once during `AgentServer` initialization and stored in the server state. In [`lib/jido/agent_server.ex`](https://github.com/agentjido/jido/blob/main/lib/jido/agent_server.ex), the `handle_continue(:post_init, state)` callback invokes `SignalRouter.build/1` to create the unified routing table.

### Route Sources and Priorities

The `SignalRouter.build/1` function in [`lib/jido/agent_server/signal_router.ex`](https://github.com/agentjido/jido/blob/main/lib/jido/agent_server/signal_router.ex) aggregates routes from three distinct sources, each with a default priority:

| Source | Default Priority | Contribution |
|--------|------------------|--------------|
| **Strategy** (`strategy.signal_routes/1`) | 50 | Strategy-level commands like `{:strategy_cmd, :my_cmd}` |
| **Agent** (`agent_module.signal_routes/1`) | 0 | Agent-specific actions with module references |
| **Plugins** (`plugin_routes/0` or `plugin.signal_routes/1`) | -10 | Plugin-provided routes or pattern matches |

Lower priority values take precedence, meaning plugin routes (-10) are checked before agent routes (0), which are checked before strategy routes (50).

### The SignalRouter.build/1 Function

The builder normalizes each route into a canonical `{path, target, priority}` tuple using `normalize_routes/2`. If a route tuple lacks an explicit priority, the function injects the appropriate default based on the source.

```elixir

# From lib/jido/agent_server/signal_router.ex

signal_router = SignalRouter.build(state)
state = %{state | signal_router: signal_router}

```

After normalization, the route list passes to `Jido.Signal.Router.new/1`, which constructs the trie-based router used for fast pattern matching against signal types.

## Processing Incoming Signals

When a client sends a signal via `AgentServer.cast/2` or `AgentServer.call/3`, the server eventually reaches `process_signal/2`, which consults the stored router.

### The Routing Pipeline

The `process_signal/2` function in [`lib/jido/agent_server.ex`](https://github.com/agentjido/jido/blob/main/lib/jido/agent_server.ex) delegates to `route_to_actions/2`:

```elixir
defp process_signal(%Signal{} = signal, %State{signal_router: router} = state) do
  case route_to_actions(router, signal) do
    {:ok, actions} -> 
      # Execute actions via agent.cmd/2

    {:error, reason} -> 
      # Handle routing failure or default action

  end
end

```

The `route_to_actions/2` function calls `JidoRouter.route/2` to perform the trie lookup:

```elixir
defp route_to_actions(router, signal) do
  case JidoRouter.route(router, signal) do
    {:ok, targets} when targets != [] ->
      actions = Enum.map(targets, &target_to_action(&1, signal))
      {:ok, actions}
    {:error, %{details: %{reason: :no_handlers_found}}} ->
      default_system_action(signal)
    {:error, reason} -> 
      {:error, reason}
  end
end

```

### Converting Targets to Actions

Each matched target transforms into a concrete action tuple via `target_to_action/2` in [`lib/jido/agent_server.ex`](https://github.com/agentjido/jido/blob/main/lib/jido/agent_server.ex):

| Target Pattern | Resulting Action | Purpose |
|----------------|------------------|---------|
| `{:strategy_cmd, cmd}` | `{cmd, signal.data}` | Strategy-level command dispatch |
| `{:strategy_tick}` | `{:strategy_tick, %{}} | Periodic strategy execution |
| `{:custom, _}` | `{:custom, signal.data}` | Custom action handling |
| `mod` (atom) | `{mod, signal.data}` | Direct module invocation |
| `{mod, params}` | `{mod, params}` | Module with predefined parameters |

The resulting `{module, params}` tuples pass to `state.agent_module.cmd(state.agent, action_arg)`, executing the agent's pure logic and returning directives for the server to process.

## Plugin Signal Hooks and Pre-Routing

Before the router executes, plugins can intercept signals via `run_plugin_signal_hooks`. In `compute_signal_call_result/2` (around line 1034 in [`lib/jido/agent_server.ex`](https://github.com/agentjido/jido/blob/main/lib/jido/agent_server.ex)), the server evaluates plugin callbacks:

```elixir
case run_plugin_signal_hooks(signal, state) do
  {:error, error} -> 
    # Abort processing and return error

  {:override, action_spec, modified_signal} -> 
    # Skip routing entirely, use specified action

  {:continue, modified_signal} -> 
    # Proceed with normal routing using modified signal

end

```

Plugins match signals using patterns defined in `plugin.signal_patterns` (supporting wildcards `*` and `.*`). The `signal_type_matches?/2` helper implements this pattern logic, allowing plugins to selectively handle signal types without modifying the core router.

## Default System Actions

When `JidoRouter.route/2` returns `{:error, :no_handlers_found}`, the server falls back to `default_system_action/1` in [`lib/jido/agent_server.ex`](https://github.com/agentjido/jido/blob/main/lib/jido/agent_server.ex):

```elixir
defp default_system_action(%Signal{type: "jido.agent.stop", data: data}) do
  params = if is_map(data), do: data, else: %{}
  {:ok, [{Jido.Actions.Lifecycle.StopSelf, params}]}
end

defp default_system_action(_), do: {:error, :no_matching_route}

```

This built-in handler automatically processes lifecycle signals like `"jido.agent.stop"` by mapping them to `Jido.Actions.Lifecycle.StopSelf`, while unknown signal types result in a routing error.

## End-to-End Signal Flow

The complete signal routing pipeline in Jido AgentServer follows this execution path:

```text
Client → AgentServer.cast/2 or call/3
      → enqueue_signal_call
      → maybe_start_next_signal_call
      → process_signal
          ├─ run_plugin_signal_hooks
          │   ├─ :override   → compute_signal_call_dispatch (skip router)
          │   ├─ :continue   → route_to_actions (normal routing)
          │   └─ :error      → emit error directive
          └─ route_to_actions
                ├─ JidoRouter.route (trie lookup by signal type)
                ├─ target_to_action (convert to {module, params})
                └─ state.agent_module.cmd/2 (execute pure agent logic)
                     → directives queued → drain loop executes

```

The router remains static after initialization unless explicitly rebuilt via `AgentServer.update_routes/2`, ensuring fast O(log n) trie lookups for every signal while supporting dynamic route updates when agents modify their behavior at runtime.

## Summary

- **Signal routing in Jido AgentServer** relies on a unified trie-based router built during server initialization via `SignalRouter.build/1`.
- Routes originate from three sources: **Strategy** (priority 50), **Agent** (priority 0), and **Plugins** (priority -10), with lower values taking precedence.
- The router matches signal types against patterns using `Jido.Signal.Router`, then converts targets to action tuples via `target_to_action/2` in [`lib/jido/agent_server.ex`](https://github.com/agentjido/jido/blob/main/lib/jido/agent_server.ex).
- **Plugin hooks** can intercept signals before routing using `run_plugin_signal_hooks`, supporting `:override`, `:continue`, or `:error` responses.
- **Default system actions** handle unmatched signals, including automatic lifecycle management for `"jido.agent.stop"` via `Jido.Actions.Lifecycle.StopSelf`.

## Frequently Asked Questions

### How does Jido AgentServer prioritize conflicting signal routes?

Jido AgentServer assigns default priorities based on route source: **Plugins** receive priority -10, **Agent modules** receive priority 0, and **Strategies** receive priority 50. When multiple routes match the same signal type, the router selects the target with the lowest priority value, ensuring plugin routes override agent routes, and agent routes override strategy routes unless explicitly configured otherwise.

### What happens when no route matches an incoming signal?

When the trie-based router returns `{:error, :no_handlers_found}`, the server invokes `default_system_action/1` in [`lib/jido/agent_server.ex`](https://github.com/agentjido/jido/blob/main/lib/jido/agent_server.ex). This function checks for built-in system signals like `"jido.agent.stop"`, which automatically maps to `Jido.Actions.Lifecycle.StopSelf`. For any other unmatched signal type, the function returns `{:error, :no_matching_route}`, which the server handles according to its error policy.

### Can plugins override the default signal routing behavior?

Yes, plugins can intercept signals before they reach the router through the `run_plugin_signal_hooks` mechanism implemented in `compute_signal_call_result/2`. When a plugin's `signal_patterns` match the incoming signal type (supporting wildcards like `*` and `.*`), the plugin can return `{:override, action_spec, modified_signal}` to bypass the router entirely, `{:continue, modified_signal}` to proceed with normal routing using a transformed signal, or `{:error, reason}` to abort processing.

### How do I define custom signal routes in my agent module?

Define the `signal_routes/1` callback in your agent module to return a list of route tuples. Each route specifies a signal pattern and a target module or command. For example, `{"user.request.*", MyHandler}` routes any signal matching "user.request.*" to the `MyHandler` module. You can also use tuple targets like `{:strategy_cmd, :my_command}` or include explicit priorities like `{"alert.*", AlertHandler, 5}`. The `SignalRouter.build/1` function automatically calls your agent's `signal_routes/1` callback during server initialization with default priority 0.