core beginner

Counter Agent

A real Jido agent that counts up and down using Actions, Signals, and signal routing. Demonstrates the core agent pattern: immutable state, validated actions, and LiveView integration.

getting-started state actions signals

Related guides and notebooks

What you’ll learn

  • How to define a Jido Agent with a typed schema
  • How to write Actions with validated parameters
  • How signal routing connects events to actions
  • How to drive a LiveView from real agent state

How it works

This example shows the foundational Jido pattern. The CounterAgent is defined as an immutable data structure — not a process. Each operation creates a new agent struct with updated state.

The Agent

The counter agent declares its state schema and signal routes:

use Jido.Agent,
  name: "counter_agent",
  schema: [
    count: [type: :integer, default: 0]
  ]

def signal_routes do
  [
    {"counter.increment", IncrementAction},
    {"counter.decrement", DecrementAction},
    {"counter.reset", ResetAction}
  ]
end

Actions

Each action is a separate module with validated params and a run/2 callback. The increment action receives the current state via context:

use Jido.Action,
  name: "increment",
  schema: [
    by: [type: :integer, default: 1, doc: "Amount to increment by"]
  ]

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

LiveView Integration

The LiveView creates a new agent, then dispatches actions on each button click:

agent = CounterAgent.new()
{new_agent, _directives} = CounterAgent.cmd(agent, {IncrementAction, %{by: 1}})

The agent is always an immutable struct — there’s no GenServer, no PID. The LiveView holds the agent in socket assigns and re-renders when state changes.

Key concepts

Agents are data, not processes. CounterAgent.new() returns a struct. CounterAgent.cmd/2 returns a new struct. You choose when and how to manage the lifecycle.

Actions are validated. The schema option on each action defines parameter types and defaults. Invalid params are rejected before run/2 is called.

Signal routing is declarative. The signal_routes/0 callback maps signal types to action modules. This decouples “what happened” from “what to do about it.”