BB.Jido.Action.Command runs a single robot command. For more involved
sequences — pick-and-place, calibration, an assembly step — you almost
always want a bb_reactor workflow. This tutorial shows how to invoke a
reactor from an agent: the agent decides "this is what I want to do
next", the reactor handles the structured execution and compensation.
Prerequisites
Tutorials 1 and 2.
Familiarity with
bb_reactor.Install it with Igniter (
bb_jidodoesn't depend on it):mix igniter.install bb_reactor
Step 1: Define a reactor
Workflows are reactor modules. A minimal one that runs a single command:
defmodule MyRobot.Workflow.PickAndPlace do
use Reactor
middlewares do
middleware BB.Reactor.Middleware.Context
end
input :pick_pose
input :place_pose
step :move_to_pick do
impl {BB.Reactor.Step.Command, command: :move_to}
argument :target, input(:pick_pose)
end
step :grasp do
impl {BB.Reactor.Step.Command, command: :close_gripper}
wait_for :move_to_pick
end
step :move_to_place do
impl {BB.Reactor.Step.Command, command: :move_to}
argument :target, input(:place_pose)
wait_for :grasp
end
step :release do
impl {BB.Reactor.Step.Command, command: :open_gripper}
wait_for :move_to_place
end
return :release
endThe BB.Reactor.Middleware.Context middleware reads
context.private.bb_robot and exposes it to every step.
Step 2: Invoke the reactor from the agent
BB.Jido.Action.Command's reactor cousin is BB.Jido.Action.Reactor. It's
already on the agent — the robot plugin attaches it. Send it via the
default route, bb.reactor.run:
:ok =
Jido.AgentServer.cast(
pid,
Jido.Signal.new!(
"bb.reactor.run",
%{
robot: MyRobot,
reactor: MyRobot.Workflow.PickAndPlace,
inputs: %{
pick_pose: %{x: 0.2, y: 0.0, z: 0.1},
place_pose: %{x: 0.0, y: 0.2, z: 0.1}
}
}
)
)That's it. The action injects context.private.bb_robot = MyRobot and
calls Reactor.run/3. On success it returns
{:ok, %{robot: ..., reactor: ..., result: result}} to the agent.
Why didn't I have to thread the robot module through every step? Because the reactor middleware reads it from context. Each step picks the robot out of
context.private.bb(a%BB.Reactor.Context{}).
Step 3: Compose decisions before invoking
The whole point of agents is to decide before running a structured workflow. Scaffold a higher-level action that wraps the reactor call:
mix bb_jido.add_action MyRobot.Actions.PickRedBlock
Then replace the stub run/2 so it selects inputs and invokes the
reactor:
defmodule MyRobot.Actions.PickRedBlock do
use Jido.Action,
name: "pick_red_block",
schema: [robot: [type: :atom, required: true]]
alias Jido.Agent.Directive.Emit
@impl Jido.Action
def run(%{robot: robot}, _context) do
pick_pose = locate_red_block(robot)
place_pose = %{x: 0.0, y: 0.2, z: 0.1}
BB.Jido.Action.Reactor.run(
%{
robot: robot,
reactor: MyRobot.Workflow.PickAndPlace,
inputs: %{pick_pose: pick_pose, place_pose: place_pose}
},
%{}
)
end
defp locate_red_block(_robot), do: %{x: 0.2, y: 0.0, z: 0.1}
endYou can call BB.Jido.Action.Reactor.run/2 directly — actions are plain
Elixir modules.
Step 4: Handle reactor outcomes
BB.Jido.Action.Reactor maps reactor's three return shapes onto
bb_jido's error taxonomy:
| Reactor returns | Action returns |
|---|---|
{:ok, result} | {:ok, %{result: result, ...}} |
{:ok, result, _struct} | {:ok, %{result: result, ...}} |
{:halted, halted} | {:error, {:reactor_halted, halted}} |
{:error, errors} | {:error, {:reactor_failed, errors}} |
If your workflow has compensation steps, they run before
{:error, ...} is returned — that's the reactor's saga behaviour, not
the action's job.
Should the agent retry or compensate? That's an application decision. The reactor unwinds its own steps; the agent decides what happens next (re-plan, escalate, ask a human). See Layered architecture.
Step 5: Chain the result into another signal
Actions can emit signals via directives. To follow a successful pick with
a celebratory state-machine transition, return an Emit directive:
alias Jido.Agent.Directive.Emit
def run(params, _ctx) do
case BB.Jido.Action.Reactor.run(params, %{}) do
{:ok, result} ->
followup =
Jido.Signal.new!("my_robot.pick.completed", %{
target: params[:target]
})
{:ok, result, %Emit{signal: followup}}
{:error, reason} ->
{:error, reason}
end
endThe runtime dispatches the follow-up signal back through the router. Any
plugin that listens for my_robot.pick.completed will fire.
What you've built
bb.reactor.run signal
│
▼
BB.Jido.Action.Reactor
│ Reactor.run/3 with private.bb_robot = MyRobot
▼
PickAndPlace ── steps run BB commands via BB.Reactor.Step.Command
│
▼
{:ok, result} → agent
│
▼ (optional directive)
my_robot.pick.completed signal → back into the routerWhere next
- Layered architecture — why the agent dispatches reactors rather than running step logic directly.
- Emit directives from actions — more on signal chaining.
- Wait for robot state — block inside an action until the robot reaches a given state.