AshJido: Ash Resources as Jido Tools
Mix.install([
{:ash, "~> 3.12"},
{:ash_jido, path: "."},
{:jido, path: "../jido"}
])
Overview
This livebook demonstrates how AshJido bridges Ash Framework resources with Jido agents. The key insight: every Ash action becomes a Jido tool automatically.
You’ll learn:
-
How to define an Ash resource with the
AshJidoextension -
How AshJido generates
Jido.Actionmodules from Ash actions -
How to use those generated actions in a Jido agent via
cmd/2
Step 1: Define the Domain and Resource
We’ll create a simple Task resource with CRUD actions, using ETS for in-memory storage.
defmodule Demo.Domain do
use Ash.Domain, validate_config_inclusion?: false
resources do
resource(Demo.Task)
end
end
defmodule Demo.Task do
use Ash.Resource,
domain: Demo.Domain,
extensions: [AshJido],
data_layer: Ash.DataLayer.Ets
ets do
private?(true)
end
attributes do
uuid_primary_key(:id)
attribute(:title, :string, allow_nil?: false)
attribute(:status, :atom, default: :pending)
timestamps()
end
actions do
defaults([:read, :destroy])
create :create do
description("Create a new task")
argument(:title, :string, allow_nil?: false)
change(set_attribute(:title, arg(:title)))
end
update :complete do
description("Mark a task as complete")
change(set_attribute(:status, :complete))
end
end
jido do
action(:create, name: "create_task", output_map?: true)
action(:read, name: "list_tasks")
action(:complete, name: "complete_task", output_map?: true)
action(:destroy, name: "delete_task")
end
end
Step 2: Explore the Generated Jido Actions
AshJido automatically generates Jido.Action modules at compile time. Let’s inspect them:
IO.puts("Generated modules:")
IO.puts(" - Demo.Task.Jido.Create")
IO.puts(" - Demo.Task.Jido.Read")
IO.puts(" - Demo.Task.Jido.Complete")
IO.puts(" - Demo.Task.Jido.Destroy")
IO.puts("\nCreate action:")
IO.puts(" name: #{Demo.Task.Jido.Create.name()}")
IO.puts(" schema: #{inspect(Demo.Task.Jido.Create.schema())}")
Step 3: Use Generated Actions Directly
The generated actions work like any Jido.Action - call run/2 with params and context:
context = %{domain: Demo.Domain}
{:ok, task1} = Demo.Task.Jido.Create.run(%{title: "Write documentation"}, context)
IO.inspect(task1, label: "Created task")
{:ok, task2} = Demo.Task.Jido.Create.run(%{title: "Review PR"}, context)
IO.inspect(task2, label: "Created another task")
{:ok, tasks} = Demo.Task.Jido.Read.run(%{}, context)
IO.inspect(tasks, label: "All tasks")
Step 4: Update and Delete
task_id = task1[:id]
{:ok, completed} = Demo.Task.Jido.Complete.run(%{id: task_id}, context)
IO.inspect(completed[:status], label: "Status after complete")
{:ok, _} = Demo.Task.Jido.Destroy.run(%{id: task_id}, context)
{:ok, remaining} = Demo.Task.Jido.Read.run(%{}, context)
IO.inspect(length(remaining), label: "Remaining tasks")
Step 5: Using Actions in a Jido Agent
Now let’s build a Jido agent that uses these Ash-backed tools:
defmodule Demo.TaskAgent do
use Jido.Agent,
name: "task_agent",
description: "Manages tasks using Ash-backed storage",
schema: [
last_task: [type: :map, default: nil],
task_count: [type: :integer, default: 0]
]
end
defmodule Demo.TaskAgent.CreateTask do
use Jido.Action,
name: "agent_create_task",
description: "Create a task via AshJido",
schema: [title: [type: :string, required: true]]
def run(params, context) do
case Demo.Task.Jido.Create.run(params, %{domain: Demo.Domain}) do
{:ok, task} ->
current_count = context.state[:task_count] || 0
{:ok, %{last_task: task, task_count: current_count + 1}}
{:error, error} ->
{:error, error}
end
end
end
defmodule Demo.TaskAgent.GetTaskCount do
use Jido.Action,
name: "agent_get_count",
description: "Get task count from Ash",
schema: []
def run(_params, _context) do
case Demo.Task.Jido.Read.run(%{}, %{domain: Demo.Domain}) do
{:ok, tasks} -> {:ok, %{task_count: length(tasks)}}
{:error, error} -> {:error, error}
end
end
end
Step 6: Drive the Agent with cmd/2
alias Demo.TaskAgent
alias Demo.TaskAgent.{CreateTask, GetTaskCount}
agent = TaskAgent.new()
IO.inspect(agent.state, label: "Initial state")
{agent, _} = TaskAgent.cmd(agent, {CreateTask, %{title: "First task from agent"}})
IO.inspect(agent.state, label: "After first create")
{agent, _} = TaskAgent.cmd(agent, {CreateTask, %{title: "Second task from agent"}})
IO.inspect(agent.state, label: "After second create")
{agent, _} = TaskAgent.cmd(agent, GetTaskCount)
IO.inspect(agent.state, label: "After count refresh")
How It Works
┌─────────────────────────────────────────────────────────────┐
│ Jido Agent │
│ TaskAgent.cmd(agent, {CreateTask, %{title: "..."}}) │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Agent Action │
│ CreateTask.run(params, context) │
│ - Calls AshJido-generated action │
│ - Transforms result to agent state │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ AshJido Generated │
│ Demo.Task.Jido.Create.run(params, %{domain: Domain}) │
│ - Type-safe schema from Ash action │
│ - Automatic error mapping │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Ash Framework │
│ Ash.Changeset.for_create → Ash.create! │
│ - Validation, policies, data layer │
└─────────────────────────────────────────────────────────────┘
The key value: you never write the bridge code. AshJido generates type-safe Jido.Action modules from your Ash resource definitions automatically.
Next Steps
- Add more Ash actions (filters, complex validations)
-
Use
all_actionsDSL to expose everything at once -
Run agents under
AgentServerfor signal-based workflows - Add actors/tenants for authorization