code-quality
Use when refactoring or reviewing Elixir code quality — compiler warnings and Credo first, then the plugin's on-demand…
Use when writing, refactoring or reviewing any Elixir code, even a small function or a quick question about idioms — replacing nested if/case with pattern matching and guards, chaining {:ok, _}/{:error, _} steps with `with`, modern Elixir 1.18+ (type checker, JSON, dbg), Phoenix
$ npx -y skills add j-morgan6/elixir-phoenix-guide --skill elixir-essentials --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
/elixir-essentialsContext preview
The summary Claude sees to decide when to auto-load this skill.
Use when writing, refactoring or reviewing any Elixir code, even a small function or a quick question about idioms — replacing nested if/case with pattern matching and guards, chaining {:ok, _}/{:error, _} steps with `with`, modern Elixir 1.18+ (type checker, JSON, dbg), Phoenix
name: elixir-essentials
description: "Use when writing, refactoring or reviewing any Elixir code, even a small function or a quick question about idioms — replacing nested if/case with pattern matching and guards, chaining {:ok, _}/{:error, _} steps with `with`, modern Elixir 1.18+ (type checker, JSON, dbg), Phoenix project layout. Not for OTP, Ecto, LiveView or tests (use the specific skill)."> Verified against Elixir 1.20, Phoenix 1.8, Ecto 3.14 — October 2026
Not for: OTP, Ecto, LiveView, tests — use the specific skill (`otp-essentials`, `ecto-essentials`, `phoenix-liveview-essentials`, `testing-essentials`).
Phoenix project layout: see [project-structure.md](project-structure.md).
1. **Branch with multi-clause functions, pattern matching and `case`** — never nest `if`; `unless` is soft-deprecated, write `if !cond` 2. **A fallible function returns `{:ok, value} | {:error, reason}` on every path** — never a bare value from one clause and a tuple from another 3. **Error reasons are atoms (or a changeset), not strings** — translate them to messages at the edge (controller, LiveView) 4. **End a `with` on the last call itself, and no catch-all `else`** — `{:ok, x} <- f() do {:ok, x} end` is redundant (Credo `RedundantWithClauseResult`) 5. **Multi-step writes go inside `Repo.transact/2`** — a `with` alone does not roll back the first write when a later step fails 6. **Call `!` functions only when failure is a bug** — never wrap a bang call in `try/rescue`; call the non-bang version and `case` on it 7. **A struct pattern matches `nil` fields** — `%User{name: name}` binds `nil`; add `when is_binary(name)` when you rely on the value 8. **Treat compiler type warnings as bugs** — fix the code; don't add guards or `_ =` just to silence them 9. **Logging: put values you'll filter on in Logger metadata; a human-readable message may interpolate** — every module that logs has `require Logger` 10. **`@impl true` on every callback, clauses of one function grouped together** — split clauses fail `--warnings-as-errors`, and so does a callback without `@impl` once another callback has one
---
<!-- epg:bad -->
# Mixed shapes: callers can't pattern match on the result
def calculate(x) when is_integer(x) and x > 0, do: x * 2
def calculate(_), do: {:error, :invalid_input}def calculate(x) when is_integer(x) and x > 0, do: {:ok, x * 2}
def calculate(_), do: {:error, :invalid_input}
@doc """
Fetches a post owned by the scope's user. Returns `{:ok, post}` or `{:error, :not_found}`.
"""
def fetch_post(%Scope{} = scope, id) do
case Repo.get_by(Post, id: id, user_id: scope.user.id) do
nil -> {:error, :not_found}
post -> {:ok, post}
end
endIf invalid input can only come from a bug, drop the fallback clause and let `FunctionClauseError` crash.
<!-- epg:bad -->
with {:ok, attrs} <- validate(params),
{:ok, post} <- Blog.create_post(scope, attrs) do
{:ok, post}
endwith {:ok, attrs} <- validate(params) do
Blog.create_post(scope, attrs)
endWrites that must succeed or fail together run in one transaction, return atom reasons, and let every error fall through unchanged:
def transfer(%Scope{} = scope, from_id, to_id, amount) do
Repo.transact(fn ->
with {:ok, from} <- fetch_own_account(scope, from_id),
{:ok, to} <- fetch_account(to_id),
:ok <- ensure_funds(from, amount),
{:ok, _from} <- debit(from, amount),
{:ok, _to} <- credit(to, amount) do
{:ok, :transferred}
end
end)
end
# => {:ok, :transferred} | {:error, :not_found | :insufficient_funds | Ecto.Changeset.t()}A catch-all `else error -> {:error, "failed: #{inspect(error)}"}` hides new error shapes and turns them into strings nobody can match on. Add an `else` only to translate specific reasons, and list each one. (Row locking for the balance check: see the `ecto-essentials` skill.)
<!-- epg:bad -->
def parse(body) do
try do
{:ok, Jason.decode!(body)}
rescue
Jason.DecodeError -> {:error, :invalid_json}
end
enddef parse(body) do
case JSON.decode(body) do
{:ok, %{"id" => id}} -> {:ok, id}
{:ok, _other} -> {:error, :missing_id}
{:error, _reason} -> {:error, :invalid_json}
end
enddef display_name(%User{name: name}) when is_binary(name), do: name
def display_name(%User{email: email}), do: emailA 2–3 step `Enum` pipeline is idiomatic. A `for` comprehension is an alternative when you want a single pass, filters, or `into:`/`reduce:`:
for item <- items, transformed = transform(item), valid?(transformed), do: format(transformed)
defmodule MyApp.Uploads do
require Logger
def process_upload(%Scope{} = scope, file) do
case save_file(file) do
{:ok, path} ->
create_record(scope, path)
{:error, reason} = error ->
Logger.wa· Changelog A Claude Code plugin (with Codex support for the skills) that keeps Claude's Elixir and Phoenix code idiomatic and current.
Use when refactoring or reviewing Elixir code quality — compiler warnings and Credo first, then the plugin's on-demand…
Use when preparing releases or deployment config — mix phx.gen.release, runtime.exs vs compile-time config, release migrations,…
Use when writing Ecto changeset validations beyond cast/validate_required — custom and cross-field validations (e.g. end date…
Use when defining schemas, writing queries, or creating migrations — scoped context functions, Repo.transact, upserts, indexes,…
Use when migrating PostgreSQL tables that serve live traffic — index concurrently,…
Use when one form or operation manages parent and child records together — cast_assoc/cast_embed, inputs_for with sort/drop…