View Source Agents
In our previous guide, we explored how Actions serve as composable building blocks for workflows. Now, let's discover how Agents bring these Actions to life by providing a stateful, intelligent wrapper around them.
What are Agents?
At their core, Jido Agents are simply data structures that understand how to maintain and transform their own state. While you might be familiar with Elixir's built-in Agent processes for state management, Jido Agents take a layered approach. The Jido.Agent
module provides the foundational data structure, and the Jido.Agent.Server
provides the GenServer implementation that manages the Agent's runtime state within OTP.
Think of a Jido Agent as a specialized map that knows two important things:
- What shape its data should have (through its schema)
- What transformations it can apply to that data (through its registered Actions)
When you create an Agent, you're not starting a process or service - you're creating a data structure that can validate its own contents and understands how to transform itself through Actions. This pure data approach means Agents are predictable, testable, and can be easily serialized or persisted.
Agents as Data Transformation Engines
The fundamental job of an Agent is to transform data in controlled, predictable ways. It does this through a simple pattern:
- It holds some initial state (input data)
- It applies one or more Actions to transform that data
- It either returns the transformed data or updates its own state
For example, imagine an Agent processing user registration data. It might:
- Start with raw input:
%{name: "JOHN DOE ", email: "JOHN@EXAMPLE.COM"}
- Apply a formatting Action that normalizes the data
- Store or return the result:
%{name: "John Doe", email: "john@example.com"}
The Role of State
An Agent's state is just a schema-validated map. The schema serves several purposes:
- It documents what fields the Agent can contain
- It ensures data consistency through type validation
- It provides default values for fields
- It makes the Agent's purpose clear to other developers
The schema isn't just for validation - it's the Agent's contract with the rest of your system about what data it manages and how that data should look.
State Validation Modes
By default, Agents use a permissive validation approach - they validate fields defined in the schema while allowing additional unknown fields. This flexibility supports development workflows and experimental features. For example, you might want to temporarily store debugging information or track experimental metrics without modifying your schema.
However, when you need stricter guarantees about your data, you can enable strict validation mode when setting state. In strict mode, the Agent will reject any fields not defined in its schema. This is valuable when:
- You need to guarantee complete state consistency
- You want to catch typos or mistakes in field names early
- You're working with sensitive data where extra fields could be problematic
- You want to maintain a strict contract about what data an Agent manages
You control this behavior through the strict_validation
option when setting state, which we'll see in practice shortly.
Actions and Transformation
While the Agent holds state, Actions do the actual work of transformation. The Agent's job is to:
- Validate that an Action is registered and can be used
- Provide its current state as input to the Action
- Validate and store (or return) the Action's output
Think of it like a pipeline where data flows through one or more transformations, with the Agent ensuring each step maintains data integrity.
Planning and Execution
One key feature of Agents is their ability to plan sequences of transformations before executing them. When you plan Actions on an Agent, you're creating a queue of transformations to apply. This separation between planning and execution lets you:
- Validate the entire transformation sequence before running it
- Ensure Actions will receive the right input at each step
- Choose whether to apply results to Agent state or just return them
- Control how results flow between Actions in the sequence
The actual execution happens through a Runner, which orchestrates how the transformations occur and how results flow between steps.
Creating Your First Agent
Let's create a simple User Registration Agent that will help us understand these concepts. We'll start with the same user registration workflow from our Actions guide, but now we'll wrap it in an Agent:
defmodule MyApp.UserAgent do
use Jido.Agent,
name: "user_agent",
description: "Manages user registration",
# Actions this agent can use
actions: [FormatUser, EnrichUserData, NotifyUser],
# State schema
schema: [
# Input fields
name: [
type: {:or, [:string, nil]},
default: nil,
doc: "User's raw input name"
],
email: [
type: {:or, [:string, nil]},
default: nil,
doc: "User's raw input email"
],
age: [
type: {:or, [:integer, nil]},
default: nil,
doc: "User's age in years"
],
# Fields that will store action results
formatted_name: [
type: {:or, [:string, nil]},
default: nil,
doc: "Name after formatting"
],
username: [
type: {:or, [:string, nil]},
default: nil,
doc: "Generated username"
],
notification_sent: [
type: :boolean,
default: false,
doc: "Whether welcome notification was sent"
]
]
end
Let's break down this definition:
- The
name
anddescription
help other parts of the system understand what this Agent does - The
actions
list declares which Actions this Agent can use - The
schema
defines what information this Agent can remember, including both input fields and expected action results
Working with Agents
Now that we have our Agent defined, let's see how to use it. Working with Agents follows a natural progression of create → set → plan → run.
Creating an Agent
First, we create a new instance of our Agent:
# Create a new agent
agent = MyApp.UserAgent.new()
# It starts with default values
agent.state.name #=> nil
agent.state.email #=> nil
agent.state.username #=> nil
# Each agent has a unique ID
agent.id #=> "ag_123..."
Setting State
Before we can process anything, we need to give our Agent some data to work with. We can use set/2
in either permissive or strict mode:
# Default permissive mode allows unknown fields
{:ok, agent} = MyApp.UserAgent.set(agent, %{
name: "John Doe",
email: "john@example.com",
debug_info: %{source: "test"} # Not in schema, but allowed
})
# Strict mode rejects unknown fields
{:error, error} = MyApp.UserAgent.set(agent,
%{
name: "John Doe",
unknown_field: true
},
strict_validation: true
)
# The error clearly identifies rejected fields
assert error.message =~ "Unknown fields: [:unknown_field]"
State updates follow these rules:
- Each update returns a new immutable copy of the agent
- Schema fields are always validated
- Unknown fields are allowed by default but can be rejected with strict validation
- Multiple fields can be updated at once
- Existing fields are preserved unless explicitly changed
Planning Actions
Once our Agent has some state, we can plan what Actions it should take:
# Plan a single action using current state
{:ok, agent} = MyApp.UserAgent.plan(agent, FormatUser)
# Plan multiple actions with explicit parameters
{:ok, agent} = MyApp.UserAgent.plan(agent, [
{FormatUser, agent.state},
EnrichUserData,
NotifyUser
])
# Planning doesn't execute - state remains unchanged
assert agent.state.formatted_name == nil
The planning phase lets us:
- Validate that all actions are registered
- Set up the transformation sequence
- Prepare parameters for each step
- Check for obvious problems before execution
Why doesn't the agent pass it's state to the first action?
Great question - and there are two reasons for this:
- The Runner is the component responsible for passing state to the Action - so in our contrived example, it's important to demonstrate that the state is being explicitly passed
- The Agent has sensible defaults that try to remove as many assumptions and require explicit programming. Passing the state by default is a good example of this. If you want to pass the state to the first action, you can do so explicitly.
Running Actions
Once we've planned our Actions, we can execute them:
# Run and apply results to state
{:ok, agent} = MyApp.UserAgent.run(agent,
apply_state: true,
runner: Jido.Runner.Chain
)
# Verify transformations happened
assert agent.state.formatted_name == "John Doe"
assert agent.state.username == "john.doe"
Or we can run without updating state to inspect the results first:
# Run without applying state changes
{:ok, agent} = MyApp.UserAgent.run(agent, apply_state: false)
# State unchanged but results available
assert agent.state.formatted_name == nil
assert agent.result.result_state.formatted_name == "John Doe"
This separation between execution and state updates gives us control over when and how our Agent's state changes.
Putting It All Together: Commands
While we can use set
, plan
, and run
separately, Jido provides a convenient cmd/4
function that combines them:
{:ok, agent} = MyApp.UserAgent.cmd(
agent,
[{FormatUser, agent.state}, EnrichUserData], # Actions to run
%{age: 30}, # State to set
apply_state: true, # Update state with results
runner: Jido.Runner.Chain # Use chain runner
)
# Everything happened in one step
assert agent.state.formatted_name == "John Doe"
assert agent.state.username == "john.doe"
assert agent.state.age == 30
Best Practices
As you build with Agents, keep these principles in mind:
Use strict validation when data consistency is critical and permissive validation during development or for temporary data.
Keep your schema focused on the data your Agent truly needs to manage. Just because you can store additional fields doesn't mean you should.
Let your Actions handle complex transformations and use
set/2
primarily for direct state updates.Take advantage of the separation between planning and execution to validate operations before running them.
Consider whether to apply results to state based on your use case - sometimes you want to inspect results before committing them.
Next Steps
Now that you understand the basics of Agents, you can explore:
- Complex action chains
- Conditional execution paths
- Error handling and recovery
- State persistence
- Agent supervision and lifecycle
- Inter-agent communication
The test suite provides many examples of these advanced patterns. Remember: Agents are most powerful when they maintain focused state and use well-defined Actions to transform that state. Keep your Agents focused, their state clean, and their Actions clear.