Skip to content

danrday/jido_poc

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Jido POC

An introductory, hello-world style project for learning the Jido autonomous agent framework for Elixir.

What is Jido?

Jido (自動 — Japanese for "automatic") is an Elixir framework for building autonomous, stateful agents. The core idea is that your program is organized as a collection of agents — each one owning a piece of state — and actions — pure, validated functions that transform that state.

This might sound abstract, but the key insight is the separation of concerns:

  • Actions hold your business logic. They're pure functions that say what should change.
  • Agents hold your state. They say what data we're tracking.
  • Directives describe side effects. They say what should happen in the outside world as a result of an action — spawning a process, emitting a signal, scheduling future work, etc.

This separation makes logic easy to test in isolation (actions are just functions), keeps state changes predictable and auditable, and sets you up for building more complex multi-agent systems later.

Prerequisites

  • Elixir 1.17+ and Erlang/OTP 26+
  • This project was built with Elixir 1.19.5 / OTP 28

Getting started

git clone <this repo>
cd jido_poc
mix deps.get
iex -S mix

Then try the demos:

iex> JidoPoc.hello_greeter()
iex> JidoPoc.hello_counter()
iex> JidoPoc.hello_pipeline()

Project structure

lib/
  jido_poc.ex                    ← runnable demos
  jido_poc/
    actions/
      say_hello.ex               ← Action: generates a greeting
      increment.ex               ← Action: adds to a counter
    agents/
      greeter.ex                 ← Agent: tracks greeting history
      counter.ex                 ← Agent: tracks a numeric count

Tutorial

Part 1 — Actions

An Action is the fundamental unit of work in Jido. Think of it as a validated, self-describing function. You define it with use Jido.Action, give it a parameter schema, and implement a run/2 callback.

# lib/jido_poc/actions/say_hello.ex

defmodule JidoPoc.Actions.SayHello do
  use Jido.Action,
    name: "say_hello",
    description: "Generates a greeting for the given name",
    schema: [
      name: [type: :string, required: true],
      style: [type: {:in, [:formal, :casual]}, default: :casual]
    ]

  @impl true
  def run(%{name: name, style: :casual}, _context) do
    {:ok, %{last_greeting: "Hey, #{name}!"}}
  end

  def run(%{name: name, style: :formal}, _context) do
    {:ok, %{last_greeting: "Good day, #{name}."}}
  end
end

A few things to notice:

The schema is validated before run/2 is called. If you pass the wrong type, a missing required field, or a value outside the allowed set, Jido rejects it before your function ever runs. There's no defensive validation code needed inside run/2.

run/2 receives (params, context).

  • params is the validated, atom-keyed map of inputs. You can pattern match on it directly (as shown above).
  • context carries the agent's current state and other metadata. If your action needs to read existing state to compute the next state (like Increment does), you access it via context.state.

The return value is a map of state updates. Returning {:ok, %{last_greeting: "Hey, Alice!"}} tells Jido to merge that map into the agent's state. You only return the keys you want to change; everything else stays the same.

Here's Increment, which does need to read existing state:

# lib/jido_poc/actions/increment.ex

defmodule JidoPoc.Actions.Increment do
  use Jido.Action,
    name: "increment",
    description: "Adds `amount` to the agent's counter",
    schema: [
      amount: [type: :integer, default: 1]
    ]

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

context.state[:count] reads the current value out of the agent's state map, adds amount to it, and returns the new value. Simple and composable.


Part 2 — Agents

An Agent owns a piece of state. You define it with use Jido.Agent, declare the shape of its state via a schema, and register which actions it can run.

# lib/jido_poc/agents/counter.ex

defmodule JidoPoc.Agents.Counter do
  use Jido.Agent,
    name: "counter",
    description: "Counts things",
    schema: [
      count: [type: :integer, default: 0]
    ],
    actions: [JidoPoc.Actions.Increment]
end

The schema here uses the same NimbleOptions syntax as Actions. It defines the fields that live in agent.state, their types, and their defaults. Jido validates the state against this schema whenever it changes, so you can't accidentally put a string where an integer belongs.

Agents are immutable structs. Counter above is a plain Elixir struct, not a process. You create one with new/1 and update it by calling cmd/2, which returns a new struct with updated state — it doesn't mutate anything in place. This makes agents easy to reason about and test.


Part 3 — Running Actions with cmd/2

Once you have an agent and an action, you run them together with cmd/2:

agent = Counter.new()
# agent.state.count => 0

{agent, _directives} = Counter.cmd(agent, {Increment, %{amount: 10}})
# agent.state.count => 10

{agent, _directives} = Counter.cmd(agent, {Increment, %{amount: 5}})
# agent.state.count => 15

cmd/2 returns a two-element tuple: the updated agent struct, and a list of directives.

The updated agent is what you care about most when starting out. You thread it from call to call — each cmd receives the agent returned by the previous one.

Directives are instructions for the Jido runtime describing side effects: spawn a child process, emit a signal to another agent, schedule something for later, and so on. For simple synchronous use like this, you can ignore them with _directives. They become important once you move to supervised agent processes.


Part 4 — Action pipelines

You can pass a list of {Action, params} tuples to cmd/2 and they run sequentially as a pipeline. Each action sees the state produced by the one before it:

{agent, _} =
  Counter.cmd(agent, [
    {Increment, %{amount: 1}},
    {Increment, %{amount: 2}},
    {Increment, %{amount: 3}}
  ])

agent.state.count  #=> 6

This is useful for expressing multi-step workflows atomically — either all steps succeed, or the whole pipeline fails.


Part 5 — Inspecting state

Because agents are plain structs, you can inspect their state directly at any point:

agent = Greeter.new()
IO.inspect(agent.state)
# => %{greeting_count: 0, last_greeting: nil}

{agent, _} = Greeter.cmd(agent, {SayHello, %{name: "Alice"}})
agent.state.last_greeting
# => "Hey, Alice!"

There's no hidden state, no message passing to a process, no unwrapping of GenServer replies. The agent is just a struct, and .state is just a map.


What's next

This POC covers the synchronous, pure-functional layer of Jido. Once you're comfortable with it, the natural next steps are:

Topic What it unlocks
Jido.AgentServer Run an agent as a supervised OTP process — persistent, fault-tolerant, callable over message passing
Signals Agent-to-agent messaging built on the CloudEvents spec. Agents communicate by emitting and routing signals rather than calling each other directly
Directives Returning Jido.Directive.Emit or Jido.Directive.Spawn from an action to trigger side effects
jido_ai Drop-in actions for calling LLMs, with tool-calling support that maps Jido actions directly to LLM tool schemas

Resources

About

Project to learn the Jido framework

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages