Curated library of 38 atomic skills, 7 personas, and 1 orchestrator for Elixir and Phoenix development. Organized by category: fundamentals, phoenix, database, testing, auth, infrastructure, quality, security, integrations, tooling, frameworks, personas, and orchestration. Covers core Elixir patterns, Phoenix LiveView, Ecto, OTP, Oban, testing, security, deployment, real-time, and modern tooling (Req, Swoosh, Cachex, Broadway, Ash).
91
91%
Does it follow best practices?
Impact
91%
1.37xAverage score across 56 eval scenarios
Advisory
Suggest reviewing before use
use Ash.Resource for domain resources — never manually implement protocolsdefaults [:read, :create] without understanding what they exposeAsh.Changeset.for_create/3 and Ash.Changeset.for_update/3 — not bare struct manipulationmix ash_postgres.generate_migrations before manual migration — let Ash generate the schemamix compile and confirm no Spark.Error.DslError before proceedingFollow this sequence when starting a new Ash project:
{:ash, "~> 3.0"} and {:ash_postgres, "~> 2.0"} to mix.exsuse Ecto.Repo to use AshPostgres.Repo, otp_app: :my_appuse Ash.Domain and resources do ... enduse Ash.Resource, domain: MyApp.Domain, data_layer: AshPostgres.DataLayertable and repo in the postgres do blockuuid_primary_key, attribute, timestamps() in the attributes do blockbelongs_to, has_many, many_to_many in relationships do blockactions do with defaults, create, update, read blockspolicies do block with authorize_if or forbid_if rulesmix ash_postgres.generate_migrations then mix ash_postgres.migrateDomain.create!(resource, attributes) to verify the resource worksdefmodule MyApp.Blog.Post do
use Ash.Resource,
domain: MyApp.Blog,
data_layer: AshPostgres.DataLayer
postgres do
table "posts"
repo MyApp.Repo
end
attributes do
uuid_primary_key :id
attribute :title, :string do
allow_nil? false
constraints [max_length: 255]
end
attribute :body, :string do
allow_nil? false
end
attribute :status, :atom do
constraints [one_of: [:draft, :published, :archived]]
default :draft
end
timestamps()
end
relationships do
belongs_to :author, MyApp.Accounts.User do
allow_nil? false
end
has_many :comments, MyApp.Blog.Comment
end
actions do
defaults [:read, :destroy]
create :create do
primary? true
accept [:title, :body, :status, :author_id]
end
update :publish do
accept []
change set_attribute(:status, :published)
end
read :published do
filter expr(status == :published)
end
end
end# Create a post
post =
MyApp.Blog.Post
|> Ash.Changeset.for_create(:create, %{
title: "Hello World",
body: "This is my first post",
author_id: user.id
})
|> MyApp.Blog.create!()
# Read posts
posts =
MyApp.Blog.Post
|> Ash.Query.for_read(:published)
|> Ash.Query.filter(author_id == ^user.id)
|> MyApp.Blog.read!()
# Update post
post
|> Ash.Changeset.for_update(:publish)
|> MyApp.Blog.update!()policies do
policy action_type(:read) do
authorize_if relates_to_actor_via(:author)
authorize_if expr(status == :published)
end
policy action_type(:create) do
authorize_if actor_present()
end
policy action(:update) do
authorize_if relates_to_actor_via(:author)
end
policy action(:destroy) do
authorize_if relates_to_actor_via(:author)
end
endDebugging authorization failures: If a call raises Ash.Error.Forbidden, enable policy breakdown logging:
# config/dev.exs
config :ash, :policies, log_policy_breakdowns: :errorAdd {:ash_phoenix, "~> 2.0"} to deps. See AshPhoenix docs for full LiveView and form component examples.
# Build form from changeset in mount
form =
post
|> Ash.Changeset.for_update(:update, %{})
|> AshPhoenix.Form.for_update()
|> to_form()
# Handle save event — reassign form on error
case Blog.update(Ash.Changeset.for_update(post, :update, params)) do
{:ok, post} -> {:noreply, put_flash(socket, :info, "Saved.") |> assign(post: post)}
{:error, cs} -> {:noreply, assign(socket, form: cs |> AshPhoenix.Form.for_update() |> to_form())}
endAdd {:ash_json_api, "~> 1.0"} to deps. See AshJsonApi docs for pagination, includes, and error serialization.
# In your resource
use Ash.Resource,
domain: MyApp.Blog,
data_layer: AshPostgres.DataLayer,
extensions: [AshJsonApi.Resource]
json_api do
type "post"
routes do
base "/posts"
get :read
index :published
post :create
patch :publish
end
end# router.ex
scope "/api/json" do
pipe_through :api
forward "/", AshJsonApi.Router, domains: [MyApp.Blog]
endaggregates do
count :comment_count, :comments
count :published_comment_count, :comments do
filter expr(status == :published)
end
end
# Use in queries
MyApp.Blog.Post
|> Ash.Query.filter(comment_count > 0)
|> MyApp.Blog.read!()| ❌ Don't | ✅ Do |
|---|---|
Rely on defaults [:read, :create] without knowing what they expose | Define actions explicitly and accept only the intended attributes |
Build filters with string interpolation ("status == '#{s}'") | Use pinned expressions: Ash.Query.filter(status == ^status) |
| Alter the DB schema by hand before defining the resource | Define the resource first, then mix ash_postgres.generate_migrations |
| Skip policy blocks on resources with sensitive data | Add policies do ... end with explicit authorize_if/forbid_if |
| Manipulate structs directly for writes | Use Ash.Changeset.for_create/3 and Ash.Changeset.for_update/3 |
| Rescue a generic error and lose context | Match specific types: Ash.Error.Forbidden, Ash.Error.Query.NotFound |
| Offset-paginate large result sets | Use keyset pagination (Ash.Query.page(after: ...)) |
create :create do
accept [:title, :body, :author_id]
validate str_length(:title, min: 1, max: 255) do
message "Title must be between 1 and 255 characters"
end
endFor multi-field or conditional logic, implement a custom Ash.Resource.Validation module:
defmodule MyApp.Validations.TitleNotBlank do
use Ash.Resource.Validation
@impl true
def validate(changeset, _opts, _context) do
case Ash.Changeset.get_attribute(changeset, :title) do
nil -> {:error, field: :title, message: "can't be blank"}
"" -> {:error, field: :title, message: "can't be blank"}
_ -> :ok
end
end
end^ for safe interpolation, never string interpolation# NEVER: Ash.Query.filter("status == '#{params["status"]}'"}) -- injection risk
MyApp.Blog.Post
|> Ash.Query.filter(status == ^status and author_id == ^current_user.id)
|> Ash.Query.sort([inserted_at: :desc])Ash.Error.Query.NotFound explicitlycase MyApp.Blog.Post |> Ash.get(id) do
{:ok, post} -> {:ok, post}
{:error, %Ash.Error.Query.NotFound{}} -> {:error, :not_found}
{:error, error} -> {:error, error}
endcase MyApp.Blog.Post
|> Ash.Changeset.for_create(params)
|> MyApp.Blog.create() do
{:ok, post} -> {:ok, post}
{:error, %Ash.Error.InvalidInput{fields: fields}} -> {:error, :validation, fields}
{:error, %Ash.Error.Forbidden{}} -> {:error, :unauthorized}
{:error, %Ash.Error.Changeset{errors: errors}} -> {:error, :invalid_changeset, errors}
{:error, error} ->
Logger.error("Unexpected error: #{inspect(error)}")
{:error, :internal_error}
endMyApp.Blog.Post
|> Ash.Query.page(limit: 20, after: last_inserted_at)
|> MyApp.Blog.read!()Always create the Ash resource first, then let Ash generate migrations — never alter the DB schema before defining the resource.
# Step 1: Create Ash resource matching existing schema
defmodule MyApp.Blog.Post do
use Ash.Resource, domain: MyApp.Blog, data_layer: AshPostgres.DataLayer
postgres do
table "posts"
repo MyApp.Repo
end
end
# Step 2: Generate and run migration
# mix ash_postgres.generate_migrations
# mix ash_postgres.migrate
# Step 3: Update context to delegate to Ash
def get_post!(id) do
MyApp.Blog.Post |> Ash.get!(id)
end| Predecessor | This Skill | Successor |
|---|---|---|
| elixir-essentials | ash-framework | phoenix-json-api |
| ecto-essentials | ash-framework | phoenix-authorization-patterns |
Companion skills:
.tessl-plugin
evals
scenario-1
scenario-2
scenario-3
scenario-4
scenario-5
scenario-6
scenario-7
scenario-8
scenario-9
scenario-10
scenario-11
scenario-12
scenario-13
scenario-14
scenario-15
scenario-16
scenario-17
scenario-18
scenario-19
scenario-20
scenario-21
scenario-22
scenario-23
scenario-24
scenario-25
scenario-26
scenario-27
scenario-28
scenario-29
scenario-30
scenario-31
scenario-32
scenario-33
scenario-34
scenario-35
scenario-36
scenario-37
scenario-38
scenario-39
scenario-40
scenario-41
scenario-42
scenario-43
scenario-44
scenario-45
scenario-46
scenario-47
scenario-48
scenario-49
scenario-50
scenario-51
scenario-52
scenario-53
scenario-54
scenario-55
scenario-56
skills
frameworks
ash-framework
infrastructure
orchestration
elixir-skill-router
personas
phoenix
quality
security
security-essentials
tooling
mix-tasks-generators