AshJido

Hex.pmHex DocsCILicenseWebsiteEcosystemDiscord

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

What This Library Does

What It Does Not Do

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:

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:

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
:createcreate_<resource>create_user
:read (:read) list_<resources>list_users
:read (:by_id) get_<resource>_by_idget_user_by_id
:updateupdate_<resource>update_user
:destroydelete_<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

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

Action X not found in resource

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

Compatibility

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:

License

Apache-2.0