Claude Code Plugins

Community-maintained marketplace

Feedback

ash-framework

@forest/dotfiles
4
0

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.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name ash-framework
description 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.

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