This guide explains how a Jidoka DSL agent becomes a supervised Jido.AgentServer
process. It covers the default Jidoka.Jido runtime instance, the
start_agent / stop_agent / whereis helpers, the generated child_spec/1,
the "jidoka.turn.run" signal flow, and how the typed Jidoka state is read back
out of Jido.Agent.state[:jidoka]. By the end you will be able to host an agent
under a supervisor, run a turn against the registered id, and inspect its
status.
When To Use This
- Use this guide when you want a long-lived, addressable agent process: shared across requests, restartable, supervised, callable by id.
- Use this guide when you need
await_completion, hibernation, or to wire an agent into a Phoenixapplication.ex. - Do not use this guide for single-shot deterministic runs. For unit tests
and one-off invocations,
MyAgent.run_turn/2andJidoka.turn/3against the spec are simpler and faster. See Runtime And Harness.
Prerequisites
- A working Jidoka DSL agent module. See Getting Started.
- Elixir
~> 1.18and:jidokaresolved throughmix deps.get. - The default
Jidoka.Jidoinstance only needs to be in your supervision tree if you want supervisor-restartable agents. DirectJidoka.start_agent/2calls in IEx will start it on demand under the application supervisor.
Setup
Jidoka ships a default Jido runtime instance,
Jidoka.Jido, which is just use Jido, otp_app: :jidoka.
That single supervisor owns the registry, dynamic supervisor, task supervisor,
and runtime store that hosted agents need.
Application config:
# config/config.exs
import Config
config :jidoka,
default_model: "openai:gpt-4o-mini"Supervision tree:
# lib/my_app/application.ex
defmodule MyApp.Application do
use Application
@impl true
def start(_type, _args) do
children = [
Jidoka.Jido,
{MyApp.TimeAgent, jido: Jidoka.Jido}
]
Supervisor.start_link(children, strategy: :rest_for_one, name: MyApp.Supervisor)
end
endCredentials for live turns (OPENAI_API_KEY, ANTHROPIC_API_KEY, etc.) belong
in the host process environment. Jidoka itself does not read .env files.
Security / Trust Boundaries
- The
Jidoka.Jidoregistry is process-local; agent ids are not a global namespace. Two applications can each host an agent with id"time-agent-1"under their own instance without collision. Jidoka.Jido.start_agent/2accepts a module, which the caller controls. Never pass untrusted module names from external input; resolve through your own allowlist first.- Provider credentials are taken from the process environment by ReqLLM. They
are never written into
Agent.Spec, snapshots, or journals. - Inspect normalized errors with
Jidoka.error_to_map/1; credential-shaped values are sanitized before being returned.
Quick Example
Start a DSL agent under the default Jidoka.Jido supervisor and run a turn
against its registered id.
defmodule MyApp.LocalTime do
use Jidoka.Action,
name: "local_time",
description: "Returns the local time for a city.",
schema: Zoi.object(%{city: Zoi.string() |> Zoi.default("Chicago")})
@impl true
def run(params, _context) do
city = Map.get(params, :city) || Map.get(params, "city") || "Chicago"
{:ok, %{city: city, time: "09:30"}}
end
end
defmodule MyApp.TimeAgent do
use Jidoka.Agent
agent :time_agent do
instructions "Use local_time when asked for the time."
end
tools do
action MyApp.LocalTime
end
end
{:ok, _pid} = MyApp.TimeAgent.start(id: "time-agent-1")
{:ok, "Chicago time is 09:30."} =
Jidoka.chat("time-agent-1", "What time is it in Chicago?", llm: fake_llm())The DSL module, the spec, and the plan are the same as the in-process flow.
Only the execution boundary differs: Jidoka.chat/3 resolves the binary id
through Jidoka.whereis/2 and sends a signal to the
Jido.AgentServer.
Concepts
╭───────────────╮ start_agent ╭────────────────────╮
│ MyApp.Agent │───────────────────────▶│ Jidoka.Jido │
│ (DSL module) │ │ (registry + │
╰───────┬───────╯ │ dyn supervisor) │
│ child_spec/1 ╰─────────┬──────────╯
▼ │
╭───────────────────╮ "jidoka.turn.run" ▼
│ Jido.AgentServer │◀────────── signal ─── Jidoka.turn(id, ...)
│ state[:jidoka] = │
│ AgentServerState │──── routes to ────▶ Jidoka.Runtime.Actions.RunTurn
╰─────────┬─────────╯ │
│ ▼
│ ╭──────────────────────╮
│ │ Jidoka.Harness │
│ │ (Runic + Effects) │
│ ╰──────────┬───────────╯
▼ ▼
to_jido_state/1 Turn.Result / SnapshotThree pieces define this boundary:
Jidoka.Jidois ause Jido, otp_app: :jidokasupervisor. It owns the registry, dynamic supervisor, task supervisor, and runtime store. Applications may host their own instance instead.- The DSL module's
child_spec/1wrapsJido.AgentServer.child_spec/1withjido: Jidoka.Jidoand a default id derived from the agent module. The compiled signal route{"jidoka.turn.run", Jidoka.Runtime.Actions.RunTurn}is attached at compile time. Jidoka.Runtime.AgentServerStateis the typed Jidoka state stored underagent.state[:jidoka]. Conventional top-level Jido fields (:status,:last_answer,:error) are kept forJido.AgentServercompatibility.
How To
Step 1: Start An Agent Under The Default Runtime
The DSL module exposes start/1, which calls Jidoka.start_agent/2, which
delegates to Jidoka.Jido.start_agent/2:
{:ok, pid} = MyApp.TimeAgent.start(id: "time-agent-1")
^pid = Jidoka.whereis("time-agent-1")If id: is omitted, the agent module supplies one derived from its DSL agent
id (:time_agent becomes "time_agent").
Step 2: Supervise An Agent In Your Application
For production callers, prefer child_spec/1 over start_agent/2 so the agent
restarts with the rest of your tree:
children = [
Jidoka.Jido,
{MyApp.TimeAgent, jido: Jidoka.Jido, id: "time-agent-1"}
]
Supervisor.start_link(children, strategy: :rest_for_one, name: MyApp.Supervisor)MyApp.TimeAgent.child_spec/1 calls Jido.AgentServer.child_spec/1 with the
right defaults. The :rest_for_one strategy ensures that a restart of
Jidoka.Jido also restarts the agents that depend on its registry.
Step 3: Run A Turn Against A Registered Id
The facade accepts a process ref (pid, registered binary id, or :via tuple):
{:ok, %Jidoka.Turn.Result{} = result} =
Jidoka.turn("time-agent-1", "What time is it in Chicago?",
timeout: 30_000,
llm: fake_llm()
)
result.content
#=> "Chicago time is 09:30."Under the hood Jidoka:
- Builds a signal with
Jidoka.Runtime.Signals.turn_run/2(type"jidoka.turn.run"). - Resolves the binary id through
Jidoka.whereis/2. - Calls
Jido.AgentServer.call(pid, signal, timeout)which routes toJidoka.Runtime.Actions.RunTurn. - Reads the typed result back out of
agent.state[:jidoka]and returns{:ok, Turn.Result.t()},{:hibernate, snapshot}, or{:error, reason}.
Step 4: Read State Out Of A Hosted Agent
The current Jidoka state can be inspected directly:
agent = :sys.get_state(Jidoka.whereis("time-agent-1")).agent
{:ok, jidoka_state} =
Jidoka.Runtime.AgentServerState.from_jido_state(agent.state)
jidoka_state.status #=> :completed
jidoka_state.result.contentfrom_jido_state/1 reads state[:jidoka] and returns the typed
AgentServerState. Use to_run_result/1 to convert it back into the
{:ok, ...} | {:hibernate, ...} | {:error, ...} envelope.
Step 5: Await Terminal Status
Most callers will just block on Jidoka.turn/3, but for fire-and-forget signal
dispatch you can wait for a terminal Jido status:
{:ok, status_map} =
Jidoka.await_agent("time-agent-1", timeout: 30_000)
status_map.status
#=> :completedawait_agent/2 is only meaningful for process-hosted agents. It is a thin
wrapper around Jido.AgentServer.await_completion/2 with Jidoka error
normalization.
Step 6: Stop An Agent
:ok = Jidoka.stop_agent("time-agent-1")stop_agent/2 accepts a pid or the registered binary id. It returns
{:error, :not_found} if the id has no running process.
Common Patterns
- Treat the registered id as your routing key. Phoenix controllers and
LiveViews should call
Jidoka.turn(id, ...)instead of looking up a pid and threading it through assigns. - Use a custom Jido instance per app boundary. If a host app already
defines
MyApp.Jido, passjido: MyApp.Jidoto the child spec so the agent lives under that supervisor instead ofJidoka.Jido. - Prefer
:rest_for_onewhen supervising agents alongsideJidoka.Jidoso the registry and the agents that depend on it restart together. - Inspect with
Jidoka.inspect/1. Run it on the pid or registered id when you want a stable, human-readable view of agent status without poking into the rawstate[:jidoka]struct.
Testing
Process-hosted tests use the same deterministic capabilities as direct turns.
The test owns the supervised process; the runtime opts are forwarded as the
signal's runtime_opts and threaded into RunTurn.
defmodule MyApp.TimeAgentTest do
use ExUnit.Case, async: true
setup do
start_supervised!(Jidoka.Jido)
id = "time-agent-#{System.unique_integer([:positive])}"
start_supervised!({MyApp.TimeAgent, jido: Jidoka.Jido, id: id})
%{id: id}
end
test "answers the time against a hosted agent", %{id: id} do
llm = fn _intent, journal ->
llm_calls =
Enum.count(journal.results, fn {_id, r} -> r.kind == :llm end)
case llm_calls do
0 ->
{:ok,
%{type: :operation, name: "local_time", arguments: %{"city" => "Chicago"}}}
1 ->
{:ok, %{type: :final, content: "Chicago time is 09:30."}}
end
end
assert {:ok, "Chicago time is 09:30."} =
Jidoka.chat(id, "What time is it in Chicago?", llm: llm)
end
endThe fake llm is the same shape used in Getting Started.
No provider key is required.
Troubleshooting
| Symptom | Likely Cause | Fix |
|---|---|---|
{:error, :not_found} from turn/3 or stop_agent/2 | The binary id is not registered in Jidoka.Jido. | Confirm with Jidoka.whereis(id). Start with MyAgent.start(id: ...) or supervise via child_spec/1. |
{:error, %Jidoka.Error{} = e} with phase: :agent_server | RunTurn failed to build a valid request (missing :input or agent module context). | Send a non-empty string and verify the agent module compiled cleanly with Jidoka.inspect(MyAgent). |
Process exits when Jidoka.Jido restarts | Agents were supervised with :one_for_one. | Use :rest_for_one so hosted agents restart with the registry. |
await_agent/2 times out with :idle hint | No turn was ever sent; the agent has no work to wait on. | Send a turn/3 or chat/3 before awaiting, or skip await_agent and use the synchronous facade. |
| Different apps clash on the same id | They share the same Jidoka.Jido instance. | Each app should use Jido, otp_app: :my_app and host its own runtime instance. |
Reference
Key modules touched in this guide:
Jidoka.Jido- default Jido runtime instance for Jidoka agents.Jidoka-start_agent/2,stop_agent/2,whereis/2,await_agent/2,turn/3,chat/3.Jidoka.Agent- DSL module that injectsstart/1andchild_spec/1for hosted agents.Jidoka.Runtime.Signals- constructor for the"jidoka.turn.run"signal.Jidoka.Runtime.Actions.RunTurn- Jido action that runs the harness inside the agent server.Jidoka.Runtime.AgentServerState- typed Jidoka state stored underagent.state[:jidoka].
Related Guides
- Getting Started - the smallest DSL agent end to end.
- Runtime And Harness - sessions, snapshots, effects, and memory.
- Live LLM Tool Loop - running a hosted agent against a real provider.
- AshJido Resources - exposing Ash actions as agent tools.
- Browser Tools - hosted agents that read the web.
- MCP Tools - hosted agents that call external MCP servers.