Powered by AppSignal & Oban Pro

Setup

first-agent.livemd

%{ title: “Your first agent”, description: “Define typed state, implement a validated action, and run your first command.”, category: :docs, order: 30, tags: [:docs, :getting_started, :tutorial, :agents, :livebook], draft: false, legacy_paths: [“/docs/learn/first-agent”], learning_outcomes: [

"Define an agent module with typed state",
"Implement an action and execute it via cmd/2",
"Interpret updated state and returned directives"

], prerequisites: [“/docs/getting-started/installation”], livebook: %{

runnable: true

} }


Setup

This notebook is self-contained. Install the dependency below and run the cells top to bottom. If you want the Mix-project setup afterward, the Installation and setup guide covers that path.

Mix.install([
  {{mix_dep:jido}}
])

# Livebook imports can execute generated docs as doctests.
# Disable compiler docs until the current Jido Hex release drops the invalid signal_types/0 example.
Code.put_compiler_option(:docs, false)

Define the agent

An agent is an immutable struct backed by a typed schema. The schema enforces types and defaults so state transitions stay consistent across runs.

defmodule CounterAgent do
  use Jido.Agent,
    name: "counter_agent",
    description: "Tracks a simple counter",
    schema: Zoi.object(%{
      count: Zoi.integer() |> Zoi.default(0)
    })
end

This module provides new/1, set/2, validate/2, and cmd/2. The agent is data, not a process. You do not need to start a runtime for this first example because cmd/2 runs directly against the agent struct.

Define an action

Actions are the only way to change agent state. Each action defines a schema for its inputs and implements run/2. The first argument is validated params. The second is the execution context, which includes context.state, the current agent state.

defmodule IncrementAction do
  use Jido.Action,
    name: "increment",
    description: "Increments the counter by a specified amount",
    schema: Zoi.object(%{
      by: Zoi.integer() |> Zoi.default(1)
    })

  @impl true
  def run(params, context) do
    current = Map.get(context.state, :count, 0)
    {:ok, %{count: current + params.by}}
  end
end

Params are schema-validated before run/2 is called, so params.by is always an integer. The return value is a partial state map that cmd/2 merges into the agent.

Create an agent and run a command

cmd/2 takes the current agent and an action instruction, runs the action, and returns a two-element tuple: the updated agent and a list of directives. The original agent is unchanged.

agent = CounterAgent.new()
{updated_agent, directives} =
  CounterAgent.cmd(agent, {IncrementAction, %{by: 3}})

Inspect the results

Show the success path first, then inspect the returned data.

updated_agent.state
directives

The directives list is empty because this action is pure. If an action needed to emit an event or schedule work, it would return Jido.Agent.Directive structs alongside the state changes.

Handle validation errors

If you pass params that fail schema validation, cmd/2 returns the original agent unchanged and a list containing a Directive.Error struct.

{error_agent, error_directives} =
  CounterAgent.cmd(agent, {IncrementAction, %{by: "not_a_number"}})
error_agent.state
error_directives

The agent state remains %{count: 0}, the same as before the failed command. The error directive wraps a Jido.Error with context about what went wrong.

Using this in a Mix project

When you move from Livebook to a Mix project, namespace your modules and place them under lib/my_agent_app/.

lib/my_agent_app/counter_agent.ex

defmodule MyAgentApp.CounterAgent do
  use Jido.Agent,
    name: "counter_agent",
    description: "Tracks a simple counter",
    schema: Zoi.object(%{
      count: Zoi.integer() |> Zoi.default(0)
    })
end

lib/my_agent_app/increment_action.ex

defmodule MyAgentApp.IncrementAction do
  use Jido.Action,
    name: "increment",
    description: "Increments the counter by a specified amount",
    schema: Zoi.object(%{
      by: Zoi.integer() |> Zoi.default(1)
    })

  @impl true
  def run(params, context) do
    current = Map.get(context.state, :count, 0)
    {:ok, %{count: current + params.by}}
  end
end

Then in iex -S mix:

alias MyAgentApp.{CounterAgent, IncrementAction}

agent = CounterAgent.new()
{updated_agent, _directives} = CounterAgent.cmd(agent, {IncrementAction, %{by: 3}})
updated_agent.state

Next steps

  • Continue to Your first LLM agent to add a runtime and model-backed reasoning.
  • Review Agents for the full conceptual model.
  • Review Actions for deeper action design patterns.