BB and Jido both have an event mechanism. BB has BB.PubSub — a hierarchical-path message broker. Jido has signals — CloudEvents-flavoured structs routed by an agent's signal router. bb_jido translates the former into the latter. This page explains why, and what the design choices buy you.

Two systems, two vocabularies

BB.PubSub delivers Erlang messages with a uniform shape:

{:bb, source_path, %BB.Message{payload: payload, robot: robot, ...}}

Jido signals are CloudEvents: a :type string plus a payload in :data, plus standard envelope fields (:source, :id, :specversion, :time). The agent's router dispatches by type-string pattern.

These two vocabularies don't translate one-to-one. PubSub is a transport — it doesn't care what the payload means. Signals are a protocol — the type carries semantic meaning.

Why a bridge instead of a Jido Sensor?

Jido provides a Jido.Sensor abstraction for ingesting external events. On paper that's the obvious place to hook PubSub in. In practice it adds a layer of indirection that buys nothing:

  • The sensor runtime expects events to be injected via Jido.Sensor.Runtime.event/2. But BB already delivers events as Erlang messages.
  • We'd be writing a sensor that does nothing but receive a message and forward it. The bridge code is the same; the sensor wrapper is dead weight.

So BB.Jido.PubSubBridge is a plain GenServer that subscribes to BB.PubSub and casts forward via Jido.AgentServer.cast/2. A future sensor wrapper remains possible if a future Jido feature genuinely needs one — it's not a one-way door.

Canonical signal types

The bridge produces three families of signal type:

WhenTypeWhy
%BB.StateMachine.Transition{} payloadbb.state.transitionSpecialised — agents almost always want to react to transitions specifically.
%BB.Safety.HardwareError{} payloadbb.safety.errorSpecialised — safety errors deserve their own type.
Anything elsebb.pubsub.<dotted source path>Generic — preserves the path information but no semantic claim.

The specialised types are stable. Even if a future BB version changes where state transitions are published, the signal type stays bb.state.transition. The generic bb.pubsub.* type is necessarily coupled to the path layout — it's a fallback, not a contract.

The signal :source is /bb/<robot module>, following CloudEvents' URI-like source convention. Traceability stays sane when multiple robots' events end up on the same downstream bus.

Filtering happens before the agent

Three places to filter, from cheapest to most expensive:

  1. Topic allowlist — only subscribe to paths the agent needs. Filters inside BB's registry; non-matching topics never trigger any work.
  2. Message-type filter — pass :message_types so the registry only delivers matching payloads.
  3. Bridge-side throttle:throttle_ms drops repeat signals of the same type within the window. Signals are still constructed; they're just not cast forward.

Filtering at the agent's mailbox is the wrong place. By the time the agent receives the cast, it's already paid the signal-construction cost, and the mailbox queue may already be backed up. The bridge owns the discipline.

Filtering happens outside PubSub for content

The bridge can't peek inside a payload — that would couple it to every payload type in BB. If you need content-based filtering ("only IMU readings on link 3 with temperature > 60"), do it in your action:

def run(%{message: %BB.Message{payload: payload}} = params, _ctx) do
  case payload do
    %BB.Sensor.IMU{frame_id: :link3, temperature: t} when t > 60 ->
      handle_overheat(params.robot)

    _ ->
      {:ok, %{ignored: true}}
  end
end

This keeps the bridge a thin pipe and pushes domain knowledge into the agent's action code, where it belongs.

See also