Powered by AppSignal & Oban Pro

Quick Start

guides/getting-started.livemd

Quick Start

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 - a pure function that takes an agent and an action, returning the updated agent and any directives:

{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:

  • cmd/2 is pure - same inputs always produce same outputs
  • The returned agent is fully updated
  • Directives describe side effects but don’t modify state

Step 4: Handle Signals with AgentServer

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

First, let’s add signal handling to our agent:

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]
    ]

  alias Jido.Signal

  @impl true
  def handle_signal(agent, %Signal{type: "counter.increment"} = signal) do
    amount = signal.data[:amount] || 1
    current = agent.state[:count]
    {:ok, agent} = set(agent, count: current + amount)
    {agent, []}
  end

  def handle_signal(agent, %Signal{type: "counter.decrement"} = signal) do
    amount = signal.data[:amount] || 1
    current = agent.state[:count]
    {:ok, agent} = set(agent, count: current - amount)
    {agent, []}
  end

  def handle_signal(agent, %Signal{type: "counter.reset"}) do
    {:ok, agent} = set(agent, count: 0)
    {agent, []}
  end

  def handle_signal(agent, _signal), do: {agent, []}
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