AshJido bridges Ash Framework resources with Jido agents by automatically generating Jido.Action modules from your Ash actions. Every Ash action becomes a tool in an agent's toolbox while maintaining type safety and respecting Ash authorization policies.
Installation
Add ash_jido to your dependencies in mix.exs:
def deps do
[
{:ash_jido, "~> 0.2"}
]
endThen fetch dependencies:
mix deps.get
Walkthrough Guides
For focused end-to-end examples, use these guides alongside this reference:
Core
Operations
Agent Integration
Basic Usage
Add the AshJido extension to your Ash resource and define which actions to expose in the jido section:
defmodule MyApp.Accounts.User do
use Ash.Resource,
domain: MyApp.Accounts,
extensions: [AshJido]
attributes do
uuid_primary_key :id
attribute :name, :string, allow_nil?: false
attribute :email, :string, allow_nil?: false
attribute :role, :atom, default: :user
end
actions do
defaults [:read, :destroy]
create :register do
accept [:name, :email]
end
update :update_profile do
accept [:name]
end
update :promote do
accept []
change set_attribute(:role, :admin)
end
end
jido do
action :register, name: "create_user", description: "Creates a new user account"
action :read, name: "list_users"
action :update_profile
action :destroy
end
endThis generates Jido.Action modules for each exposed action:
MyApp.Accounts.User.Jido.RegisterMyApp.Accounts.User.Jido.ReadMyApp.Accounts.User.Jido.UpdateProfileMyApp.Accounts.User.Jido.Destroy
Exposing All Actions
Use all_actions to quickly expose all public Ash actions on a resource with smart defaults:
jido do
all_actions
endall_actions uses Ash's public API boundary and skips actions with
public?: false by default. Use explicit action :name entries for deliberate
per-action exposure, or opt into a trusted/internal catalog:
jido do
all_actions include_private?: true
endGenerated schemas also use Ash's public input boundary by default. Accepted
attributes and action arguments marked public?: false are omitted from the
Jido schema unless include_private?: true is set for a trusted/internal tool.
This controls tool exposure; Ash authorization, policies, and runtime validation
remain authoritative when the action executes.
You can filter which actions to expose:
jido do
# Exclude specific actions
all_actions except: [:destroy, :internal_update]
endjido do
# Only expose specific actions
all_actions only: [:register, :read, :update_profile]
endYou can also add tags to all generated actions:
jido do
all_actions tags: ["user-management", "public-api"]
endAnd apply static relationship loads to all generated read actions:
jido do
all_actions only: [:read], read_load: [:profile, :roles]
endUsing Generated Actions
Call the generated modules using run/2 with params and a context map. context[:domain]
overrides the Ash resource's static domain: configuration; if neither is available,
AshJido raises an ArgumentError.
# Create a user
{:ok, user} = MyApp.Accounts.User.Jido.Register.run(
%{name: "John Doe", email: "john@example.com"},
%{domain: MyApp.Accounts}
)
# List users (returns list of maps when output_map?: true)
{:ok, users} = MyApp.Accounts.User.Jido.Read.run(
%{},
%{domain: MyApp.Accounts}
)
# Update a user (requires the resource primary key; id for the default primary key)
{:ok, updated_user} = MyApp.Accounts.User.Jido.UpdateProfile.run(
%{id: user[:id], name: "Jane Doe"},
%{domain: MyApp.Accounts}
)
# Delete a user (requires the resource primary key; id for the default primary key)
{:ok, _} = MyApp.Accounts.User.Jido.Destroy.run(
%{id: user[:id]},
%{domain: MyApp.Accounts}
)Context Options
The context map supports additional options for authorization and multi-tenancy:
context = %{
domain: MyApp.Accounts, # Required only when no static resource domain is configured or you need an override
actor: current_user, # Optional: for authorization policies
tenant: "org_123", # Optional: for multi-tenant apps
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.Accounts.User.Jido.Register.run(params, context)Configuration Options
Each action in the jido section supports these options:
| Option | Type | Default | Description |
|---|---|---|---|
name | string | "resource_action" | Custom name for the Jido action |
module_name | atom | Resource.Jido.ActionName | Custom module name for the generated action |
description | string | Ash action description | Description for AI discovery and documentation |
category | string | nil | Category for discovery/tool organization |
tags | list(string) | [] | Tags for categorization and AI discovery |
vsn | string | nil | Optional semantic version metadata |
output_map? | boolean | true | Convert output 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 statement 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 (overridable 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 additionally supports:
read_loadfor static read relationship loadingread_query_params?to enable/disable query parameters for read actionsread_max_page_sizeto set maximum page size for read actionsinclude_private?to include Ash actions and inputs withpublic?: falsein trusted/internal catalogscategory(defaultash.<action_type>)tagsvsnemit_signals?,signal_dispatch,signal_type,signal_source, andtelemetry?
Signals
Use AshJido.Notifier for Ash-native lifecycle publications to a Jido signal bus:
defmodule MyApp.Blog.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]
publish_all :update, include: :changes_only
end
endGenerated actions can also emit signals when a tool run needs runtime dispatch overrides or telemetry signal counters:
jido do
action :create,
emit_signals?: true,
signal_dispatch: {:pid, target: self()},
telemetry?: true
endBoth paths use AshJido.SignalFactory. Generated-action signals include
primary key data in signal.data by default; use signal_include to widen the
payload intentionally. Notifier publications use the configured include mode.
Telemetry
Telemetry is opt-in:
jido do
action :create, telemetry?: true
endWhen enabled, generated actions emit:
[:jido, :action, :ash_jido, :start][:jido, :action, :ash_jido, :stop][:jido, :action, :ash_jido, :exception]
Examples
jido do
# Simple exposure with defaults
action :create
# Custom name for better AI discoverability
action :read,
name: "search_users",
description: "Search for users by criteria",
load: [:profile]
# Add tags for categorization
action :update,
category: "ash.update",
tags: ["user-management", "data-modification"],
vsn: "1.0.0"
# Custom module name
action :promote, module_name: MyApp.Actions.PromoteUser
# Disable output map conversion (keep Ash structs)
action :special, output_map?: false
endTool Export Helpers
Use AshJido.Tools when integrating generated actions with tool-oriented agent systems:
AshJido.Tools.actions(MyApp.Accounts.User)
AshJido.Tools.actions(MyApp.Accounts)
AshJido.Tools.tools(MyApp.Accounts.User)Sensor Bridge Helpers
If you use signal dispatch targets that should also feed sensor runtimes, use AshJido.SensorDispatchBridge:
AshJido.SensorDispatchBridge.forward(signal_or_message, sensor_runtime)
AshJido.SensorDispatchBridge.forward_many(messages, sensor_runtime)
AshJido.SensorDispatchBridge.forward_or_ignore(message, sensor_runtime)Output Formats
By default (output_map?: true), Ash structs are converted to plain maps for easier consumption by agents and JSON serialization.
Set output_map?: false to preserve the original Ash resource structs in the output.
Policy Enforcement
AshJido respects Ash authorization policies. When you define policies on your resources, they are automatically enforced when actions are executed through the generated Jido modules.
defmodule MyApp.Accounts.SecureDocument do
use Ash.Resource,
domain: MyApp.Accounts,
extensions: [AshJido],
authorizers: [Ash.Policy.Authorizer]
policies do
policy action_type(:create) do
authorize_if actor_present()
end
policy action_type(:read) do
authorize_if always()
end
end
jido do
action :create
action :read
end
endWhen calling actions, pass the actor in the context:
# This will fail with :forbidden - no actor provided
{:error, error} = SecureDocument.Jido.Create.run(
%{title: "Secret"},
%{domain: MyApp.Accounts, actor: nil}
)
error.details.reason # => :forbidden
# This succeeds - actor is present
{:ok, doc} = SecureDocument.Jido.Create.run(
%{title: "Secret"},
%{domain: MyApp.Accounts, actor: current_user}
)Error Handling
Ash errors are automatically converted to Jido's Splode-based error system:
| Ash Error Type | Jido Error Type |
|---|---|
Ash.Error.Invalid | Jido.Action.Error.InvalidInputError (validation error) |
Ash.Error.Forbidden | Jido.Action.Error.ExecutionFailureError (with reason :forbidden) |
Ash.Error.Framework | Jido.Action.Error.InternalError |
Ash.Error.Unknown | Jido.Action.Error.InternalError |
Field-level validation errors are preserved and accessible:
case MyApp.Accounts.User.Jido.Register.run(%{name: ""}, %{domain: MyApp.Accounts}) do
{:ok, user} ->
# Success
user
{:error, %Jido.Action.Error.InvalidInputError{} = error} ->
# Access field-specific errors
error.details.fields
# => %{name: ["is required"]}
{:error, %Jido.Action.Error.ExecutionFailureError{details: %{reason: :forbidden}}} ->
# Authorization failed
:unauthorized
endNaming Conventions
Default Action Names
Auto-generated names follow verb-first patterns:
:create→"create_<resource>"(e.g."create_user"):readwith name:read→"list_<resources>"(e.g."list_users"):readwith name:by_id→"get_<resource>_by_id"(e.g."get_user_by_id"):update→"update_<resource>"(e.g."update_user"):destroy→"delete_<resource>"(e.g."delete_user")- custom
:action→"<resource>_<action_name>"or"<verb>_<resource>"for common verbs
Default Module Names
Modules are generated under the resource namespace:
MyApp.Accounts.Userwith:registeraction →MyApp.Accounts.User.Jido.RegisterMyApp.Blog.Postwith:publishaction →MyApp.Blog.Post.Jido.Publish
Action Types
Each Ash action type maps to corresponding behavior:
| Ash Action Type | Behavior |
|---|---|
:create | Creates a new record via Ash.create! |
:read | Queries records via Ash.read! |
:update | Updates a record using the resource primary key fields via Ash.update! |
:destroy | Deletes a record using the resource primary key fields and declared destroy arguments via Ash.destroy! |
:action | Runs custom logic via Ash.run_action! |
Complete Example
Here's a complete example with a domain, resource, and usage:
# lib/my_app/blog/domain.ex
defmodule MyApp.Blog do
use Ash.Domain
resources do
resource MyApp.Blog.Post
end
end
# lib/my_app/blog/post.ex
defmodule MyApp.Blog.Post do
use Ash.Resource,
domain: MyApp.Blog,
data_layer: AshPostgres.DataLayer,
extensions: [AshJido]
postgres do
table "posts"
repo MyApp.Repo
end
attributes do
uuid_primary_key :id
attribute :title, :string, allow_nil?: false
attribute :body, :string
attribute :status, :atom, default: :draft
timestamps()
end
actions do
defaults [:read, :destroy]
create :create do
accept [:title, :body]
end
update :update do
accept [:title, :body]
end
update :publish do
accept []
change set_attribute(:status, :published)
end
end
jido do
action :create,
name: "create_post",
description: "Create a new blog post draft",
tags: ["content-management", "authoring"]
action :read,
name: "list_posts",
description: "List and search blog posts"
action :update,
name: "edit_post",
tags: ["content-management"]
action :publish,
name: "publish_post",
description: "Publish a draft post",
tags: ["content-management", "publishing"]
action :destroy,
name: "delete_post",
tags: ["content-management", "destructive"]
end
endUsing the generated actions:
alias MyApp.Blog.Post
# Create a post
{:ok, post} = Post.Jido.Create.run(
%{title: "Hello World", body: "My first post"},
%{domain: MyApp.Blog}
)
# List all posts
{:ok, posts} = Post.Jido.Read.run(%{}, %{domain: MyApp.Blog})
# Publish the post
{:ok, published} = Post.Jido.Publish.run(
%{id: post.id},
%{domain: MyApp.Blog}
)Querying and Filtering
Generated Jido read actions support query parameters for filtering, sorting, pagination, and allowlisted relationship loading. These parameters are optional and provide powerful querying capabilities while respecting Ash's authorization policies.
Filter Syntax
Use the filter parameter to query records using Ash's filter input syntax:
# Simple equality filter
{:ok, users} = MyApp.Accounts.User.Jido.Read.run(
%{filter: %{name: "John Doe"}},
%{domain: MyApp.Accounts}
)
# Filter with operators
{:ok, adults} = MyApp.Accounts.User.Jido.Read.run(
%{filter: %{age: %{greater_than: 18}}},
%{domain: MyApp.Accounts}
)
# Multiple conditions (all must match)
{:ok, active_admins} = MyApp.Accounts.User.Jido.Read.run(
%{filter: %{status: "active", role: "admin"}},
%{domain: MyApp.Accounts}
)
# IN operator for multiple values
{:ok, users} = MyApp.Accounts.User.Jido.Read.run(
%{filter: %{status: %{in: ["active", "pending"]}}},
%{domain: MyApp.Accounts}
)Common Filter Operators:
%{field: value}— Equality%{field: %{greater_than: value}}— Greater than%{field: %{less_than: value}}— Less than%{field: %{greater_than_or_equal: value}}— Greater than or equal%{field: %{less_than_or_equal: value}}— Less than or equal%{field: %{in: [value1, value2]}}— Match any value in list%{field: %{contains: "substring"}}— String contains (case-sensitive)
Sorting
Use the sort parameter to order results. You can specify sorting as JSON-style entries, a keyword list, or a string:
# JSON-style entries (tool-call friendly)
{:ok, users} = MyApp.Blog.Post.Jido.Read.run(
%{sort: [%{"field" => "created_at", "direction" => "desc"}]},
%{domain: MyApp.Blog}
)
# Keyword list syntax
{:ok, users} = MyApp.Blog.Post.Jido.Read.run(
%{sort: [created_at: :desc, title: :asc]},
%{domain: MyApp.Blog}
)
# String syntax (- prefix for descending)
{:ok, users} = MyApp.Blog.Post.Jido.Read.run(
%{sort: "-created_at,title"},
%{domain: MyApp.Blog}
)Pagination
Use limit and offset for pagination:
# First page (20 items)
{:ok, page1} = MyApp.Accounts.User.Jido.Read.run(
%{limit: 20, offset: 0},
%{domain: MyApp.Accounts}
)
# Second page
{:ok, page2} = MyApp.Accounts.User.Jido.Read.run(
%{limit: 20, offset: 20},
%{domain: MyApp.Accounts}
)
# Combine with filtering and sorting
{:ok, active_users_page} = MyApp.Accounts.User.Jido.Read.run(
%{
filter: %{status: "active"},
sort: [name: :asc],
limit: 50,
offset: 100
},
%{domain: MyApp.Accounts}
)Dynamic Relationship Loading
Use the load parameter to dynamically load relationships at query time:
# Load a single relationship
{:ok, posts} = MyApp.Blog.Post.Jido.Read.run(
%{load: :author},
%{domain: MyApp.Blog}
)
# Load multiple relationships
{:ok, posts} = MyApp.Blog.Post.Jido.Read.run(
%{load: [:author, :comments, :tags]},
%{domain: MyApp.Blog}
)
# Load nested relationships
{:ok, posts} = MyApp.Blog.Post.Jido.Read.run(
%{load: [author: [:profile, :roles]]},
%{domain: MyApp.Blog}
)
# Combine with other query parameters
{:ok, posts} = MyApp.Blog.Post.Jido.Read.run(
%{
filter: %{status: "published"},
sort: [published_at: :desc],
limit: 10,
load: [author: :profile, comments: :author]
},
%{domain: MyApp.Blog}
)Configuration
Query parameters are enabled by default for read actions. You can configure this behavior:
jido do
# Query params enabled by default
action :read
# Disable query params for a specific action
action :read, query_params?: false
# Set maximum page size (clamps limit parameter)
action :read, max_page_size: 100
# Combine with static load
action :read, load: :profile, max_page_size: 50
# Configure defaults for all read actions
all_actions only: [:read], read_query_params?: true
all_actions only: [:read], read_max_page_size: 100
endSecurity Note: Query parameters use Ash's safe filter_input and sort_input variants, which:
- Only allow filtering and sorting on public attributes
- Honor field policies and authorization rules
- Prevent access to private or sensitive fields
- Validate all input before executing queries
Next Steps
- See Usage Rules for comprehensive patterns and best practices
- Check the API Documentation for detailed module docs
- Explore the Ash Framework documentation for more on defining resources and actions