Phoenix Integration
View SourceAfter: You can run Jido agents inside a Phoenix application with LiveView updates.
This guide shows how to integrate Jido agents with Phoenix controllers, LiveView, and PubSub for real-time UI updates.
Adding Jido to Your Supervision Tree
Create a Jido instance module in your Phoenix app:
# lib/my_app/jido.ex
defmodule MyApp.Jido do
use Jido, otp_app: :my_app
endAdd it to your application supervision tree before the Endpoint:
# lib/my_app/application.ex
defmodule MyApp.Application do
use Application
@impl true
def start(_type, _args) do
children = [
MyApp.Repo,
MyApp.Jido,
{Phoenix.PubSub, name: MyApp.PubSub},
MyAppWeb.Endpoint
]
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
Supervisor.start_link(children, opts)
end
endOptional config in config/config.exs:
config :my_app, MyApp.Jido,
max_tasks: 1000,
agent_pools: []Sending Signals from Controllers
Start an agent and send signals from a Phoenix controller:
# lib/my_app_web/controllers/counter_controller.ex
defmodule MyAppWeb.CounterController do
use MyAppWeb, :controller
alias Jido.Signal
def show(conn, %{"id" => id}) do
case MyApp.Jido.whereis(id) do
nil ->
conn |> put_status(:not_found) |> json(%{error: "Agent not found"})
pid ->
{:ok, state} = Jido.AgentServer.state(pid)
json(conn, %{id: id, count: state.agent.state.count})
end
end
def create(conn, %{"id" => id}) do
case MyApp.Jido.start_agent(MyApp.CounterAgent, id: id) do
{:ok, _pid} ->
conn |> put_status(:created) |> json(%{id: id, count: 0})
{:error, {:already_started, _pid}} ->
conn |> put_status(:conflict) |> json(%{error: "Agent already exists"})
end
end
def increment(conn, %{"id" => id, "amount" => amount}) do
signal = Signal.new!("counter.increment", %{amount: amount}, source: "/api")
case MyApp.Jido.whereis(id) do
nil ->
conn |> put_status(:not_found) |> json(%{error: "Agent not found"})
pid ->
{:ok, agent} = Jido.AgentServer.call(pid, signal)
json(conn, %{id: id, count: agent.state.count})
end
end
endBroadcasting State Changes
Use PubSub to broadcast agent state changes. Define an action that emits to PubSub:
# lib/my_app/actions/increment.ex
defmodule MyApp.Actions.Increment do
use Jido.Action,
name: "increment",
schema: [amount: [type: :integer, default: 1]]
alias Jido.Agent.Directive
def run(%{amount: amount}, context) do
current = context.state[:count] || 0
new_count = current + amount
broadcast_signal =
Jido.Signal.new!("counter.updated", %{count: new_count}, source: "/agent")
{:ok, %{count: new_count}, [
Directive.emit(broadcast_signal, {:pubsub, pubsub: MyApp.PubSub, topic: "counter:updates"})
]}
end
endOr broadcast from the controller after the call returns:
def increment(conn, %{"id" => id, "amount" => amount}) do
signal = Signal.new!("counter.increment", %{amount: amount}, source: "/api")
with pid when is_pid(pid) <- MyApp.Jido.whereis(id),
{:ok, agent} <- Jido.AgentServer.call(pid, signal) do
Phoenix.PubSub.broadcast(MyApp.PubSub, "counter:#{id}", {:counter_updated, agent.state})
json(conn, %{id: id, count: agent.state.count})
else
nil -> conn |> put_status(:not_found) |> json(%{error: "Agent not found"})
{:error, reason} -> conn |> put_status(:unprocessable_entity) |> json(%{error: inspect(reason)})
end
endLiveView Integration
Subscribe to agent updates and render state in real-time:
# lib/my_app_web/live/counter_live.ex
defmodule MyAppWeb.CounterLive do
use MyAppWeb, :live_view
alias Jido.Signal
@impl true
def mount(%{"id" => id}, _session, socket) do
if connected?(socket) do
Phoenix.PubSub.subscribe(MyApp.PubSub, "counter:#{id}")
end
socket =
socket
|> assign(:id, id)
|> assign_agent_state(id)
{:ok, socket}
end
defp assign_agent_state(socket, id) do
case MyApp.Jido.whereis(id) do
nil ->
assign(socket, count: nil, error: "Agent not found")
pid ->
{:ok, state} = Jido.AgentServer.state(pid)
assign(socket, count: state.agent.state.count, error: nil)
end
end
@impl true
def handle_event("increment", %{"amount" => amount}, socket) do
amount = String.to_integer(amount)
send_signal(socket.assigns.id, "counter.increment", %{amount: amount})
{:noreply, socket}
end
def handle_event("decrement", _params, socket) do
send_signal(socket.assigns.id, "counter.decrement", %{amount: 1})
{:noreply, socket}
end
def handle_event("reset", _params, socket) do
send_signal(socket.assigns.id, "counter.reset", %{})
{:noreply, socket}
end
defp send_signal(id, type, data) do
signal = Signal.new!(type, data, source: "/liveview")
case MyApp.Jido.whereis(id) do
nil -> :ok
pid -> Jido.AgentServer.cast(pid, signal)
end
end
@impl true
def handle_info({:counter_updated, state}, socket) do
{:noreply, assign(socket, count: state.count)}
end
@impl true
def render(assigns) do
~H"""
<div class="counter">
<h1>Counter: <%= @id %></h1>
<%= if @error do %>
<p class="error"><%= @error %></p>
<% else %>
<p class="count"><%= @count %></p>
<div class="controls">
<button phx-click="decrement">-</button>
<button phx-click="increment" phx-value-amount="1">+1</button>
<button phx-click="increment" phx-value-amount="10">+10</button>
<button phx-click="reset">Reset</button>
</div>
<% end %>
</div>
"""
end
endAdd the route:
# lib/my_app_web/router.ex
live "/counter/:id", CounterLiveJSON API Responses
Extract agent state for API responses:
defmodule MyAppWeb.AgentJSON do
def show(%{agent: agent}) do
%{
id: agent.id,
state: sanitize_state(agent.state),
dirty_state: agent.dirty_state
}
end
defp sanitize_state(state) when is_map(state) do
state
|> Map.drop([:__internal__, :children])
|> Map.new(fn {k, v} -> {k, serialize_value(v)} end)
end
defp serialize_value(%DateTime{} = dt), do: DateTime.to_iso8601(dt)
defp serialize_value(value), do: value
endUse in controllers:
def show(conn, %{"id" => id}) do
with pid when is_pid(pid) <- MyApp.Jido.whereis(id),
{:ok, state} <- Jido.AgentServer.state(pid) do
render(conn, :show, agent: state.agent)
else
nil -> conn |> put_status(:not_found) |> json(%{error: "not_found"})
end
endComplete Example: Counter with LiveView
Here's a complete working example you can copy into a Phoenix app.
The Agent
# lib/my_app/agents/counter_agent.ex
defmodule MyApp.CounterAgent do
use Jido.Agent,
name: "counter",
description: "A counter with PubSub broadcasting",
schema: [
count: [type: :integer, default: 0]
]
def signal_routes do
[
{"counter.increment", MyApp.Actions.Increment},
{"counter.decrement", MyApp.Actions.Decrement},
{"counter.reset", MyApp.Actions.Reset}
]
end
endThe Actions
# lib/my_app/actions/counter_actions.ex
defmodule MyApp.Actions.Increment do
use Jido.Action,
name: "increment",
schema: [amount: [type: :integer, default: 1]]
def run(%{amount: amount}, context) do
{:ok, %{count: (context.state[:count] || 0) + amount}}
end
end
defmodule MyApp.Actions.Decrement do
use Jido.Action,
name: "decrement",
schema: [amount: [type: :integer, default: 1]]
def run(%{amount: amount}, context) do
{:ok, %{count: (context.state[:count] || 0) - amount}}
end
end
defmodule MyApp.Actions.Reset do
use Jido.Action,
name: "reset",
schema: []
def run(_params, _context) do
{:ok, %{count: 0}}
end
endThe LiveView with Inline Broadcast
# lib/my_app_web/live/counter_live.ex
defmodule MyAppWeb.CounterLive do
use MyAppWeb, :live_view
alias Jido.Signal
@impl true
def mount(%{"id" => id}, _session, socket) do
if connected?(socket) do
Phoenix.PubSub.subscribe(MyApp.PubSub, "counter:#{id}")
ensure_agent_started(id)
end
{:ok, socket |> assign(:id, id) |> load_count(id)}
end
defp ensure_agent_started(id) do
case MyApp.Jido.whereis(id) do
nil -> MyApp.Jido.start_agent(MyApp.CounterAgent, id: id)
_pid -> :ok
end
end
defp load_count(socket, id) do
case MyApp.Jido.whereis(id) do
nil -> assign(socket, count: 0)
pid ->
{:ok, state} = Jido.AgentServer.state(pid)
assign(socket, count: state.agent.state.count)
end
end
@impl true
def handle_event("increment", %{"amount" => amount}, socket) do
{:noreply, send_and_broadcast(socket, "counter.increment", %{amount: String.to_integer(amount)})}
end
def handle_event("decrement", _params, socket) do
{:noreply, send_and_broadcast(socket, "counter.decrement", %{amount: 1})}
end
def handle_event("reset", _params, socket) do
{:noreply, send_and_broadcast(socket, "counter.reset", %{})}
end
defp send_and_broadcast(socket, type, data) do
id = socket.assigns.id
signal = Signal.new!(type, data, source: "/liveview")
case MyApp.Jido.whereis(id) do
nil ->
socket
pid ->
{:ok, agent} = Jido.AgentServer.call(pid, signal)
Phoenix.PubSub.broadcast(MyApp.PubSub, "counter:#{id}", {:counter_updated, agent.state})
assign(socket, count: agent.state.count)
end
end
@impl true
def handle_info({:counter_updated, state}, socket) do
{:noreply, assign(socket, count: state.count)}
end
@impl true
def render(assigns) do
~H"""
<div class="p-8">
<h1 class="text-2xl font-bold mb-4">Counter: <%= @id %></h1>
<p class="text-6xl font-mono mb-8"><%= @count %></p>
<div class="flex gap-2">
<button phx-click="decrement" class="px-4 py-2 bg-red-500 text-white rounded">-1</button>
<button phx-click="increment" phx-value-amount="1" class="px-4 py-2 bg-green-500 text-white rounded">+1</button>
<button phx-click="increment" phx-value-amount="10" class="px-4 py-2 bg-green-700 text-white rounded">+10</button>
<button phx-click="reset" class="px-4 py-2 bg-gray-500 text-white rounded">Reset</button>
</div>
<p class="mt-4 text-gray-500">Open this page in multiple tabs to see real-time sync.</p>
</div>
"""
end
endRouter
# lib/my_app_web/router.ex
scope "/", MyAppWeb do
pipe_through :browser
live "/counter/:id", CounterLive
endVisit /counter/my-counter in multiple browser tabs. Changes sync in real-time.
Next Steps
- Signals — Signal routing and creation
- Runtime — AgentServer lifecycle and parent-child hierarchies
- Await & Coordination — Wait for agent completion