Skip to content

agentjido/ash_jido

Repository files navigation

AshJido

Hex.pm Hex Docs CI License Website Ecosystem Discord

Bridge Ash Framework resources with Jido agents. Generates Jido.Action modules from Ash actions at compile time.

What This Library Does

  • Adds a jido DSL section to Ash resources
  • Generates Jido.Action modules at compile time for selected actions
  • Maps Ash argument types to NimbleOptions schemas
  • Runs actions via Ash with the provided or resource-configured domain, actor, and tenant
  • Converts Ash errors to Jido.Action.Error (Splode-based) errors
  • Publishes Jido.Signal events from Ash action notifications

What It Does Not Do

  • Auto-discover domains outside Ash resource configuration
  • Bypass Ash authorization, policies, or data layers

Installation

mix igniter.install ash_jido

Or add manually to mix.exs:

def deps do
  [
    {:ash_jido, "~> 0.2.0"}
  ]
end

Quick Start

defmodule MyApp.User do
  use Ash.Resource,
    domain: MyApp.Accounts,
    extensions: [AshJido]

  actions do
    create :register
    read :by_id
    update :profile
  end

  jido do
    action :register, name: "create_user"
    action :by_id, name: "get_user"
    action :profile
  end
end

Generated modules:

{:ok, user} = MyApp.User.Jido.Register.run(
  %{name: "John", email: "john@example.com"},
  %{domain: MyApp.Accounts}
)

Query Parameters

Generated Jido read actions include optional query parameters for filtering, sorting, and pagination:

{:ok, users} = MyApp.User.Jido.Read.run(
  %{
    filter: %{status: %{in: ["active", "pending"]}},
    sort: [name: :asc, created_at: :desc],
    limit: 20,
    offset: 40
  },
  %{domain: MyApp.Accounts}
)

Available Parameters:

  • filter (map) — Filter using Ash's filter input syntax: %{name: "fred"}, %{age: %{greater_than: 25}}
  • sort (any) — Sort via JSON-style entries [%{"field" => "name", "direction" => "asc"}], keyword list [name: :asc], or string "name,-age"
  • limit (pos_integer) — Maximum results to return
  • offset (non_neg_integer) — Results to skip (for pagination)
  • load (any) — Optional runtime relationship/calculation loads, available only when the action configures allowed_loads

Security: Query parameters use Ash's safe filter_input/sort_input variants, which only allow filtering and sorting on public attributes and honor field policies. Runtime load is disabled unless explicitly allowlisted.

Configuration:

jido do
  action :read                            # query params enabled by default
  action :read, query_params?: false      # opt out
  action :read, allowed_loads: [:profile] # opt into runtime load
  action :read, max_page_size: 100        # clamp limit to max
  all_actions read_query_params?: true    # default for all read actions
  all_actions read_allowed_loads: [:profile]
  all_actions read_max_page_size: 100     # max page size for all reads
end

Context Requirements

AshJido resolves the Ash domain in this order:

  1. context[:domain]
  2. the resource's static domain: configuration
  3. ArgumentError if neither is available
context = %{
  domain: MyApp.Accounts,       # required only when the resource has no static domain or you need an override
  actor: current_user,          # optional: for authorization
  tenant: "org_123",            # optional: for multi-tenancy
  authorize?: true,             # optional: explicit authorization mode
  tracer: [MyApp.Tracer],       # optional: Ash tracer modules
  scope: MyApp.Scope.for(user), # optional: Ash scope
  context: %{request_id: "1"},  # optional: Ash action context
  timeout: 15_000,              # optional: Ash operation timeout
  signal_dispatch: {:pid, target: self()} # optional: override signal dispatch
}

MyApp.User.Jido.Create.run(params, context)

DSL Options

Individual Actions

jido do
  action :create
  action :read, name: "list_users", description: "List all users", load: [:profile]
  action :update, category: "ash.update", tags: ["user-management"], vsn: "1.0.0"
  action :special, output_map?: false  # preserve Ash structs
end

Default generated module names are based on the Ash action name, e.g. action :create generates Resource.Jido.Create even when name: is set. Use module_name: to intentionally choose a different generated module, and provide explicit module_name: values when exposing the same Ash action more than once.

Bulk Exposure

all_actions follows Ash's public API boundary by default: it expands only actions marked public?: true. Explicit action :name entries remain the way to expose a specific private action deliberately, and include_private?: true is available for trusted/internal tool catalogs. Generated schemas also follow Ash's public input boundary by default and omit accepted attributes or action arguments marked public?: false.

jido do
  all_actions
  all_actions except: [:destroy, :internal]
  all_actions only: [:create, :read]
  all_actions include_private?: true
  all_actions category: "ash.resource"
  all_actions tags: ["public-api"]
  all_actions vsn: "1.0.0"
  all_actions only: [:read], read_load: [:profile]
end

Reactive Signals

The canonical Ash integration path is AshJido.Notifier: add it to the resource and configure publications in jido when you want resource lifecycle events published to a Jido signal bus:

defmodule MyApp.Post do
  use Ash.Resource,
    domain: MyApp.Blog,
    extensions: [AshJido],
    notifiers: [AshJido.Notifier]

  jido do
    signal_bus MyApp.SignalBus
    signal_prefix "blog"

    publish :create, "blog.post.created",
      include: [:id, :title],
      metadata: [:actor, :tenant]

    publish_all :update, include: :changes_only
  end
end

Generated actions can also emit signals with emit_signals?: true; this is best when a tool run needs runtime dispatch overrides or telemetry signal counters. Both paths build payloads through AshJido.SignalFactory, so signal type/source/subject and signal.extensions["jido_metadata"] are consistent. Generated-action signals include primary key data by default; use signal_include to explicitly widen signal.data. Notifier publications use the configured include mode.

Action Options

Option Type Default Description
name string auto-generated Custom Jido action name
module_name atom Resource.Jido.Action Custom module name
description string from Ash action Action description
category string nil Category for discovery/tool organization
tags list(string) [] Tags for categorization
vsn string nil Optional semantic version metadata
output_map? boolean true Convert structs to public-field maps
include_private? boolean false Include inputs with public?: false in generated schemas for trusted/internal tools
load term nil Static Ash.Query.load/2 for read actions
allowed_loads term nil Allowlisted runtime load entries for read actions
query_params? boolean true Enable query parameters (filter, sort, limit, offset, and allowlisted load) for read actions
max_page_size pos_integer nil Maximum limit value for read actions (clamps the limit parameter)
emit_signals? boolean false Emit Jido signals from Ash notifications (create/update/destroy)
signal_dispatch term nil Default signal dispatch config (can be overridden via context)
signal_type string derived Override emitted signal type
signal_source string derived Override emitted signal source
signal_include atom/list(atom) :pkey_only Data inclusion mode for generated-action signals
telemetry? boolean false Emit Jido-namespaced telemetry for generated action execution

all_actions Options

Option Type Default Description
only list(atom) all public actions Limit generated actions
except list(atom) [] Exclude actions
include_private? boolean false Include Ash actions and inputs with public?: false for trusted/internal tool catalogs
category string ash.<action_type> Category added to generated actions
tags list(string) [] Tags added to all generated actions
vsn string nil Optional semantic version metadata for generated actions
read_load term nil Static Ash.Query.load/2 for generated read actions
read_query_params? boolean true Enable query parameters for generated read actions
read_max_page_size pos_integer nil Maximum limit value for generated read actions
emit_signals? boolean false Emit Jido signals from generated create/update/destroy actions
signal_dispatch term nil Default signal dispatch config for generated actions
signal_type string derived Override emitted signal type
signal_source string derived Override emitted signal source
telemetry? boolean false Emit Jido-namespaced telemetry for generated action execution

Telemetry

Telemetry is opt-in per action (or via all_actions):

jido do
  action :create, telemetry?: true
end

When enabled, generated actions emit:

  • [:jido, :action, :ash_jido, :start]
  • [:jido, :action, :ash_jido, :stop]
  • [:jido, :action, :ash_jido, :exception]

Metadata includes resource/action/module identity, domain/tenant, actor presence, signaling/read-load flags, and signal delivery counters.

Tool Export Helpers

Use AshJido.Tools to list generated actions and export LLM-friendly tool maps:

# Generated action modules for a resource
AshJido.Tools.actions(MyApp.Accounts.User)

# Generated action modules for all resources in a domain
AshJido.Tools.actions(MyApp.Accounts)

# Tool payloads (name/description/schema/function) for agent/LLM integrations
AshJido.Tools.tools(MyApp.Accounts.User)

Sensor Bridge

AshJido.SensorDispatchBridge keeps the dispatch-first signal model while adding optional sensor runtime forwarding:

# Accepts %Jido.Signal{}, {:signal, %Jido.Signal{}}, and {:signal, {:ok, %Jido.Signal{}}}
:ok = AshJido.SensorDispatchBridge.forward(signal_message, sensor_runtime)

# Batch forwarding with per-message errors
%{forwarded: count, errors: errors} =
  AshJido.SensorDispatchBridge.forward_many(messages, sensor_runtime)

# Ignore non-signal mailbox noise safely
:ok | :ignored | {:error, :runtime_unavailable} =
  AshJido.SensorDispatchBridge.forward_or_ignore(message, sensor_runtime)

Default Naming

Action Type Pattern Example
:create create_<resource> create_user
:read (:read) list_<resources> list_users
:read (:by_id) get_<resource>_by_id get_user_by_id
:update update_<resource> update_user
:destroy delete_<resource> delete_user

Generated schemas are the public tool surface for discovery and validation. Ash authorization, policies, and runtime validation remain the source of truth when an action executes.

Troubleshooting

AshJido: :domain must be provided in context

  • Pass %{domain: MyApp.Domain} as the second argument to run/2, or configure domain: MyApp.Domain on the Ash resource

Update actions require primary key parameter(s): ...

  • Include the resource's primary key field or fields in params for :update and :destroy actions
  • Resources with the default [:id] primary key continue to use id
  • Destroy actions also include and pass through any declared Ash destroy action arguments

Action X not found in resource

  • Check jido action :... entries match defined Ash actions

For a full error contract and telemetry interpretation, see Walkthrough: Failure Semantics.

Compatibility

  • Elixir: ~> 1.18
  • OTP: 27 or 28
  • Ash: ~> 3.12
  • Jido: ~> 2.2
  • Jido Action: ~> 2.2
  • Jido Signal: ~> 2.1

Documentation

Start Here

Walkthroughs: Core

Walkthroughs: Operations

Walkthroughs: Agent Integration

Reference

Real Consumer Integration App

A full AshPostgres-backed consumer harness lives at ash_jido_consumer/.

It exercises real integration scenarios end-to-end:

  • context passthrough + policy behavior
  • relationship-aware reads (load)
  • notifications to signals (emit_signals?)
  • Jido telemetry emission (telemetry?)

License

Apache-2.0

About

Compile-time bridge from Ash Framework resources to Jido Action modules

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages