ash-framework

forest's avatarfrom forest

Comprehensive Ash framework guidelines for Elixir applications. Use when working with Ash resources, domains, actions, queries, changesets, policies, calculations, or aggregates. Covers code interfaces, error handling, validations, changes, relationships, and authorization. Read documentation before using Ash features - do not assume prior knowledge.

⭐4stars🔀0forks📁View on GitHub🕐Updated Dec 24, 2025

When & Why to Use This Skill

This Claude skill provides comprehensive, expert-level guidelines for the Ash Framework within the Elixir ecosystem. It enables the agent to effectively model complex domains using declarative resources, manage sophisticated business logic through custom actions, and implement robust security policies. By following these best practices for code interfaces, data validation, and resource relationships, developers can significantly accelerate the construction of scalable, maintainable, and secure Elixir applications.

Use Cases

  • Architecting domain-driven Elixir applications by defining declarative Ash resources and domain interfaces.
  • Implementing complex business workflows using custom Ash actions, changesets, and transaction hooks.
  • Designing granular authorization systems with Ash policies to ensure secure data access across the application.
  • Optimizing data retrieval with advanced Ash queries, including filters, aggregates, calculations, and complex relationship loading.
  • Streamlining database management and migrations specifically tailored for Ash-based data layers like AshPostgres.
nameash-framework
descriptionComprehensive Ash framework guidelines for Elixir applications. Use when working with Ash resources, domains, actions, queries, changesets, policies, calculations, or aggregates. Covers code interfaces, error handling, validations, changes, relationships, and authorization. Read documentation before using Ash features - do not assume prior knowledge.

Ash Framework Guidelines

Ash is a declarative framework for modeling domains with resources. Read documentation before using features.

Code Interfaces

Define code interfaces on domains - avoid direct Ash.get!/2 calls in LiveViews/Controllers:

# In domain
resource Post do
  define :get_post, action: :read, get_by: [:id]
  define :list_posts, action: :read
  define :create_post, action: :create, args: [:title]
end

# Usage - prefer query option over manual Ash.Query building
posts = MyApp.Blog.list_posts!(
  query: [filter: [status: :published], sort: [published_at: :desc], limit: 10],
  load: [author: :profile, comments: [:author]]
)

post = MyApp.Blog.get_post!(id, load: [comments: [:author]])

Authorization functions are auto-generated: can_create_post?(actor), can_update_post?(actor, post).

Using scopes: Pass scope: socket.assigns.scope in LiveViews; use context parameter in hooks.

Actions

  • Create specific, well-named actions (not generic CRUD)
  • Put business logic inside action definitions
  • Use before_action/after_action for same-transaction logic
  • Use before_transaction/after_transaction for external calls
actions do
  create :sign_up do
    argument :invite_code, :string, allow_nil?: false
    change set_attribute(:joined_at, &DateTime.utc_now/0)
    change relate_actor(:creator)
  end
end

Querying

Important: Ash.Query.filter/2 is a macro - you must require Ash.Query:

require Ash.Query

Post
|> Ash.Query.filter(status == :published)
|> Ash.Query.sort(published_at: :desc)
|> Ash.Query.load([:author, :comments])
|> Ash.Query.limit(10)
|> Ash.read!()

Error Handling

  • Use ! variations (Ash.create!, Domain.action!) when expecting success
  • Prefer ! over pattern matching {:ok, result} = ...
  • Error classes: Forbidden > Invalid > Framework > Unknown

Validations

# Built-in validations
validate compare(:age, greater_than_or_equal_to: 18)
validate match(:email, ~r/@/)
validate one_of(:status, [:active, :pending])

# Conditional validation
validate present(:phone) do
  where eq(:contact_method, "phone")
end

# Skip if prior validations failed
validate expensive_check() do
  only_when_valid? true
end

Avoid redundant validations - don't duplicate attribute constraints (allow_nil? false already validates presence).

Changes

# Built-in changes
change set_attribute(:status, "pending")
change relate_actor(:creator)
change atomic_update(:counter, expr(counter + 1))

# Custom change module
defmodule MyApp.Changes.Slugify do
  use Ash.Resource.Change

  def change(changeset, _opts, _context) do
    title = Ash.Changeset.get_attribute(changeset, :title)
    slug = title |> String.downcase() |> String.replace(~r/[^a-z0-9]+/, "-")
    Ash.Changeset.change_attribute(changeset, :slug, slug)
  end
end

Prefer custom modules over anonymous functions for changes, validations, preparations.

Preparations

Modify queries before execution:

prepare build(sort: [created_at: :desc])
prepare build(filter: [deleted: false])

# Conditional preparation
prepare build(filter: [visible: true]) do
  where argument_equals(:include_hidden, false)
end

Data Layers

use Ash.Resource,
  domain: MyApp.Blog,
  data_layer: AshPostgres.DataLayer  # or :embedded, Ash.DataLayer.Ets

postgres do
  table "posts"
  repo MyApp.Repo
end

Migrations

Run mix ash.codegen <name> after modifying resources. Use --dev during development, final name at end.

Testing

  • Test through code interfaces
  • Use authorize?: false when auth isn't the focus
  • Use Ash.can? to test policies
  • Use raising ! functions

Prevent deadlocks - use unique values for identity fields in concurrent tests:

%{email: "test-#{System.unique_integer([:positive])}@example.com"}

References