Skip to content

agentjido/ash_jido

Repository files navigation

AshJido

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

Ash Framework integration with Jido Actions

Resources

License

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages