Search
Search docs, blog posts, and ecosystem packages with citations.
Enter a query to see grounded citations.
We can't find the internet
Attempting to reconnect
Search docs, blog posts, and ecosystem packages with citations.
GenServer-backed modules that bridge external events into Jido's signal layer.
Sensors connect the external world to your agents. They observe sources like PubSub topics, HTTP webhooks, message queues, database changes, and timers, then transform those raw events into typed signals.
Under the hood, a sensor is a GenServer. The Jido.Sensor.Runtime process wraps your sensor module, manages its lifecycle, and implements the standard GenServer callbacks - init/1, handle_info/2, handle_cast/2, and terminate/2. Your sensor module defines the behavior; the runtime provides the process.
Agents need to react to the outside world, but coupling them directly to external sources creates fragile systems. Polling a metrics endpoint inside an agent mixes concerns. Parsing webhook payloads in a signal router scatters transformation logic.
Sensors give you a single, focused boundary for this work. Each sensor owns the translation from one external source into the signal vocabulary your agents understand. When a webhook format changes or a queue protocol evolves, you update one sensor module instead of touching agent logic.
Define a sensor with use Jido.Sensor and provide metadata. This example listens to a Phoenix.PubSub topic for new orders.
defmodule MyApp.OrderSensor do
use Jido.Sensor,
name: "order_sensor",
description: "Listens for new orders from PubSub",
schema: Zoi.object(%{
pubsub: Zoi.atom(),
topic: Zoi.string()
})
@impl true
def init(config, _context) do
Phoenix.PubSub.subscribe(config.pubsub, config.topic)
{:ok, %{pubsub: config.pubsub, topic: config.topic}}
end
@impl true
def handle_event({:order_placed, order}, state) do
signal = Jido.Signal.new!(
"order.placed",
%{order_id: order.id, total: order.total, customer_id: order.customer_id},
source: "/sensor/orders"
)
{:ok, state, [{:emit, signal}]}
end
def handle_event(_unknown, state) do
{:ok, state}
end
end
Three options go into the use macro:
name - A unique identifier for the sensor. Must contain only letters, numbers, and underscores. description - A human-readable summary of what the sensor monitors. schema - A Zoi schema that validates configuration passed to init/2 at startup. init/2Called when the sensor starts. Receives the validated configuration map and a runtime context map. Returns initial state and optional startup directives.
In the order sensor, init/2 subscribes to the PubSub topic. Because Sensor.Runtime is a GenServer, PubSub broadcast messages arrive as handle_info messages, which the runtime forwards to your handle_event/2 callback.
The return values are {:ok, state}, {:ok, state, directives}, or {:error, reason}.
handle_event/2Called when the sensor receives an event from its connected source. You inspect the event, build signals, update state, and return directives that tell the runtime what to do next.
The order sensor pattern-matches on {:order_placed, order} and builds a typed signal from the raw PubSub payload. Unrecognized events return {:ok, state} with no directives.
terminate/2
Called on shutdown. The default implementation returns :ok. Override it only when you need to clean up resources.
Callbacks return a list of directives that instruct the runtime to perform actions on behalf of the sensor. These are distinct from agent directives.
| Directive | Purpose |
|---|---|
{:emit, signal} | Emit a signal to the connected agent |
{:schedule, ms} |
Schedule a :tick event after the given interval |
{:schedule, ms, payload} | Schedule a custom event after the given interval |
You can combine directives freely. A polling sensor returns {:emit, signal} and {:schedule, interval} together so it emits data and re-arms itself in one step.
Start the agent and sensor in your application supervision tree. The sensor’s context includes the agent_ref - the pid or name of the AgentServer that should receive the emitted signals.
defmodule MyApp.OrderAgent do
use Jido.Agent,
name: "order_agent",
schema: Zoi.object(%{
orders_processed: Zoi.integer() |> Zoi.default(0),
last_order_id: Zoi.string() |> Zoi.optional()
})
end
defmodule MyApp.ProcessOrderAction do
use Jido.Action,
name: "process_order",
schema: Zoi.object(%{
order_id: Zoi.string(),
total: Zoi.float(),
customer_id: Zoi.string()
})
@impl true
def run(params, _context) do
{:ok, %{
orders_processed: params.orders_processed + 1,
last_order_id: params.order_id
}}
end
end
Wire them together in your supervision tree:
children = [
{Jido.AgentServer, agent: MyApp.OrderAgent, id: :order_agent},
{Jido.Sensor.Runtime,
sensor: MyApp.OrderSensor,
config: %{pubsub: MyApp.PubSub, topic: "orders"},
context: %{agent_ref: :order_agent}}
]
Supervisor.start_link(children, strategy: :one_for_one)
When something in your application broadcasts an order:
Phoenix.PubSub.broadcast(MyApp.PubSub, "orders", {:order_placed, order})
The sensor receives the broadcast, transforms it into a typed "order.placed" signal, and emits it to the agent. The AgentServer routes the signal through its signal router and executes the matching action.
You can push events into a sensor programmatically using Jido.Sensor.Runtime.event/2. This is useful for testing or for sources that don’t use PubSub.
Jido.Sensor.Runtime.event(sensor_pid, {:order_placed, order})
Because sensors define pure callbacks, you can test them in isolation without starting any processes.
test "order sensor emits signal for placed orders" do
{:ok, state} = MyApp.OrderSensor.init(
%{pubsub: MyApp.PubSub, topic: "orders"}, %{}
)
order = %{id: "ord_123", total: 99.99, customer_id: "cus_456"}
{:ok, _state, directives} = MyApp.OrderSensor.handle_event(
{:order_placed, order}, state
)
assert [{:emit, signal}] = directives
assert signal.type == "order.placed"
assert signal.data.order_id == "ord_123"
end