Powered by AppSignal & Oban Pro

AshJido: Ash Resources as Jido Tools

guides/ash-jido-demo.livemd

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:

  1. How to define an Ash resource with the AshJido extension
  2. How AshJido generates Jido.Action modules from Ash actions
  3. 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_actions DSL to expose everything at once
  • Run agents under AgentServer for signal-based workflows
  • Add actors/tenants for authorization