# AshJido
[](https://hex.pm/packages/ash_jido)
[](https://hexdocs.pm/ash_jido/)
[](https://github.com/agentjido/ash_jido/actions/workflows/ci.yml)
[](https://github.com/agentjido/ash_jido/blob/main/LICENSE)
[](https://jido.run)
[](https://jido.run/ecosystem)
[](https://jido.run/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
```bash
mix igniter.install ash_jido
```
Or add manually to `mix.exs`:
```elixir
def deps do
[
{:ash_jido, "~> 0.2.0"}
]
end
```
## Quick Start
```elixir
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:
```elixir
{: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:
```elixir
{: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:**
```elixir
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
```elixir
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
```elixir
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`.
```elixir
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:
```elixir
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`):
```elixir
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:
```elixir
# 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:
```elixir
# 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](guides/walkthrough-failure-semantics.md).
## 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**
- [Getting Started](guides/getting-started.md) — comprehensive usage
- [Interactive Demo](guides/ash-jido-demo.livemd) — try in Livebook
**Walkthroughs: Core**
- [Resource to Action](guides/walkthrough-resource-to-action.md) — define resources and run generated actions
- [Policy, Scope, and Authorization](guides/walkthrough-policy-scope-auth.md) — policy-aware actor, scope, tenant patterns
- [AshPostgres Consumer Harness](guides/walkthrough-ash-postgres-consumer.md) — real DB-backed integration scenarios
**Walkthroughs: Operations**
- [Signals, Telemetry, and Sensors](guides/walkthrough-signals-telemetry-sensors.md) — notification signals and observability
- [Failure Semantics](guides/walkthrough-failure-semantics.md) — deterministic errors and telemetry outcomes
**Walkthroughs: Agent Integration**
- [Tools and AI Integration](guides/walkthrough-tools-and-ai.md) — action metadata and tool export
- [Agent Tool Wiring](guides/walkthrough-agent-tool-wiring.md) — domain tool catalogs and safe execution wrappers
**Reference**
- [Usage Rules](usage-rules.md) — AI/LLM patterns
## 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