Claude-skill-registry ash-framework
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.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/ash-framework" ~/.claude/skills/majiayu000-claude-skill-registry-ash-framework && rm -rf "$T"
skills/data/ash-framework/SKILL.mdAsh 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
for same-transaction logicbefore_action/after_action - Use
for external callsbefore_transaction/after_transaction
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!
) when expecting successDomain.action! - Prefer
over pattern matching!{:ok, result} = ... - Error classes:
>Forbidden
>Invalid
>FrameworkUnknown
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
when auth isn't the focusauthorize?: false - Use
to test policiesAsh.can? - Use raising
functions!
Prevent deadlocks - use unique values for identity fields in concurrent tests:
%{email: "test-#{System.unique_integer([:positive])}@example.com"}
References
- Authorization & Policies: See references/policies.md
- Relationships: See references/relationships.md
- Calculations & Aggregates: See references/calculations.md