Powered by AppSignal & Oban Pro

Quick Start

guides/getting-started.livemd

Quick Start

Run in Livebook

Mix.install([
  {:jido, "~> 2.0"}
])

Overview

This guide gets you from zero to a working Jido agent in 5 minutes. You'll learn:

  1. How to start Jido's supervision tree
  2. How to define an agent with state
  3. How to use cmd/2 for pure state transformations
  4. How to run agents in AgentServer for real applications

Step 1: Start Jido

Jido uses instance-scoped supervisors. Define an instance module and add it to your supervision tree:

# In lib/my_app/jido.ex
defmodule MyApp.Jido do
  use Jido, otp_app: :my_app
end
# In config/config.exs
config :my_app, MyApp.Jido,
  max_tasks: 1000,
  agent_pools: []
# In your application.ex
children = [
  MyApp.Jido
]

Supervisor.start_link(children, strategy: :one_for_one)

For this Livebook, we'll start it directly:

defmodule Demo.Jido do
  use Jido, otp_app: :demo
end

{:ok, _pid} = Demo.Jido.start_link()

Step 2: Define an Agent

Agents are immutable structs with a schema defining their state:

defmodule CounterAgent do
  use Jido.Agent,
    name: "counter",
    description: "A simple counter agent",
    schema: [
      count: [type: :integer, default: 0],
      status: [type: :atom, default: :idle]
    ]
end

Step 3: Use cmd/2 for State Transformations

The core pattern in Jido is cmd/2: pass an agent and an action, then receive the updated agent and any directives for runtime-owned effects:

{agent, directives} = MyAgent.cmd(agent, action)

Let's define an action and use it:

defmodule IncrementAction do
  use Jido.Action,
    name: "increment",
    description: "Increments the counter by a given amount",
    schema: [
      amount: [type: :integer, default: 1]
    ]

  @impl true
  def run(params, context) do
    current = context.state[:count] || 0
    {:ok, %{count: current + params.amount}}
  end
end

Now use it:

agent = CounterAgent.new()
IO.inspect(agent.state, label: "Initial state")

{agent, _directives} = CounterAgent.cmd(agent, {IncrementAction, %{amount: 5}})
IO.inspect(agent.state, label: "After increment by 5")

{agent, _directives} = CounterAgent.cmd(agent, IncrementAction)
IO.inspect(agent.state, label: "After increment by 1")

Key points:

  • The returned agent is fully updated
  • Actions may be pure or effectful when they need a result back now
  • Directives describe runtime-owned effects and never modify state

Step 4: Route Signals with AgentServer

For real applications, agents run inside AgentServer - a GenServer that handles signals and executes directives.

AgentServer routes each signal type to an Action. We already have IncrementAction, so define the other counter actions and declare the complete signal routes on the agent:

defmodule DecrementAction do
  use Jido.Action,
    name: "decrement",
    description: "Decrements the counter by a given amount",
    schema: [
      amount: [type: :integer, default: 1]
    ]

  @impl true
  def run(params, context) do
    current = context.state[:count] || 0
    {:ok, %{count: current - params.amount}}
  end
end

defmodule ResetAction do
  use Jido.Action,
    name: "reset",
    description: "Resets the counter to zero",
    schema: []

  @impl true
  def run(_params, _context) do
    {:ok, %{count: 0}}
  end
end

defmodule CounterAgentWithSignals do
  use Jido.Agent,
    name: "counter_with_signals",
    description: "Counter that responds to signals",
    schema: [
      count: [type: :integer, default: 0],
      status: [type: :atom, default: :idle]
    ],
    signal_routes: [
      {"counter.increment", IncrementAction},
      {"counter.decrement", DecrementAction},
      {"counter.reset", ResetAction}
    ]
end

Now start the agent under Jido and send signals:

{:ok, pid} = Demo.Jido.start_agent(CounterAgentWithSignals, id: "my-counter")

Create and send signals:

alias Jido.Signal

increment_signal = Signal.new!("counter.increment", %{amount: 10}, source: "/user")
{:ok, agent} = Jido.AgentServer.call(pid, increment_signal)
IO.inspect(agent.state, label: "After increment by 10")

decrement_signal = Signal.new!("counter.decrement", %{amount: 3}, source: "/user")
{:ok, agent} = Jido.AgentServer.call(pid, decrement_signal)
IO.inspect(agent.state, label: "After decrement by 3")

For async processing, use cast/2:

reset_signal = Signal.new!("counter.reset", %{}, source: "/admin")
:ok = Jido.AgentServer.cast(pid, reset_signal)

Process.sleep(50)
{:ok, state} = Jido.AgentServer.state(pid)
IO.inspect(state.agent.state, label: "After reset")

Step 5: Query and Manage Agents

Your instance module provides utilities for managing agents:

Demo.Jido.list_agents()
|> IO.inspect(label: "All agents")

Demo.Jido.agent_count()
|> IO.inspect(label: "Agent count")

found_pid = Demo.Jido.whereis("my-counter")
IO.inspect(found_pid == pid, label: "Found by ID?")

Stop the agent when done:

:ok = Demo.Jido.stop_agent("my-counter")

Summary

You've learned the core Jido workflow:

Concept Purpose
use Jido, otp_app: :my_app Define an instance module for your agents
use Jido.Agent Define agents with schemas
cmd/2 Pure state transformations
MyApp.Jido.start_agent/2 Run agents in AgentServer
AgentServer.call/3 Synchronous signal processing
AgentServer.cast/2 Async signal processing
Signal.new!/3 Create signals to send to agents

Next Steps