code-quality
Use when refactoring or reviewing Elixir code quality — compiler warnings and Credo first, then the plugin's on-demand…
Use when writing GenServer, Supervisor, Task, Agent, Registry or ETS code — call vs cast, supervision and restart strategies, process naming, timeouts, async work. Not for LiveView async (use phoenix-liveview-essentials).
$ npx -y skills add j-morgan6/elixir-phoenix-guide --skill otp-essentials --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
/otp-essentialsContext preview
The summary Claude sees to decide when to auto-load this skill.
Use when writing GenServer, Supervisor, Task, Agent, Registry or ETS code — call vs cast, supervision and restart strategies, process naming, timeouts, async work. Not for LiveView async (use phoenix-liveview-essentials).
name: otp-essentials description: "Use when writing GenServer, Supervisor, Task, Agent, Registry or ETS code — call vs cast, supervision and restart strategies, process naming, timeouts, async work. Not for LiveView async (use phoenix-liveview-essentials)."
> Verified against Elixir 1.20, Phoenix 1.8, LiveView 1.2 — October 2026
1. **Always use `@impl true`** before GenServer/Supervisor/Agent callbacks 2. **Keep `init/1` fast** — return `{:ok, state, {:continue, term}}` and do DB/network setup in `handle_continue/2`; a slow `init` blocks the supervisor and app boot 3. **Default to `GenServer.call`** — it gives back-pressure and a 5 s timeout. Use `cast` only for cheap state updates you can afford to lose; send slow side effects (email, HTTP) to `Task.Supervisor` or Oban 4. **Wrap every process behind a public API module** — callers never write `GenServer.call(pid, ...)` themselves 5. **One argument shape: `start_link(opts)` with a keyword list** — read it with `Keyword.fetch!/2`; child specs pass `{MyApp.Room, room_id: id}` 6. **Registry for dynamic processes; atoms only for singletons** — never build a name with `String.to_atom/1` 7. **Every long-running process is supervised** — no bare `spawn`/`Task.start`; inside a LiveView use `start_async/3` or `assign_async/3` 8. **Every `Task.async` is awaited** — for collections use `Task.Supervisor.async_stream_nolink/4` with `on_timeout: :kill_task` 9. **Don't rely on `terminate/2` for cleanup** — on supervisor shutdown it runs only if the process traps exits; use links or monitors for cleanup that must happen
---
defmodule MyApp.Cache do
use GenServer
# --- Public API ---
def start_link(opts) do
GenServer.start_link(__MODULE__, opts, name: Keyword.get(opts, :name, __MODULE__))
end
def get(server \\ __MODULE__, key), do: GenServer.call(server, {:get, key})
def put(server \\ __MODULE__, key, value), do: GenServer.call(server, {:put, key, value})
# --- Callbacks ---
@impl true
def init(_opts), do: {:ok, %{}}
@impl true
def handle_call({:get, key}, _from, state), do: {:reply, Map.get(state, key), state}
def handle_call({:put, key, value}, _from, state), do: {:reply, :ok, Map.put(state, key, value)}
end<!-- epg:bad -->
@impl true
def init(_opts) do
items = MyApp.Repo.all(MyApp.Item) # blocks the supervisor until the query returns
{:ok, %{items: items}}
end@impl true
def init(_opts), do: {:ok, %{items: []}, {:continue, :load}}
@impl true
def handle_continue(:load, state), do: {:noreply, %{state | items: MyApp.Repo.all(MyApp.Item)}}<!-- epg:bad -->
# Fire-and-forget through a GenServer: the email is lost if EmailServer crashes
def send_welcome(user), do: GenServer.cast(EmailServer, {:send, user})# Work that may be lost: a supervised task. Work that must happen: an Oban job. def send_welcome(user) do Task.Supervisor.start_child(MyApp.TaskSupervisor, fn -> MyApp.Mailer.deliver_welcome(user) end) end
Return `{:noreply, state}` from `handle_call/3`, keep `from`, and reply later. The server stays responsive while slow work runs elsewhere:
@impl true
def handle_call({:lookup, key}, from, state) do
task = Task.Supervisor.async_nolink(MyApp.TaskSupervisor, fn -> slow_lookup(key) end)
{:noreply, put_in(state.waiting[task.ref], from)}
end
@impl true
def handle_info({ref, result}, state) when is_reference(ref) do
Process.demonitor(ref, [:flush])
{from, waiting} = Map.pop!(state.waiting, ref)
GenServer.reply(from, {:ok, result})
{:noreply, %{state | waiting: waiting}}
end
def handle_info({:DOWN, ref, :process, _pid, reason}, state) do
{from, waiting} = Map.pop!(state.waiting, ref)
GenServer.reply(from, {:error, reason})
{:noreply, %{state | waiting: waiting}}
endTimers, monitors, PubSub and task replies arrive in `handle_info/2`. Once you define `handle_info/2`, an unmatched message raises `FunctionClauseError`; add a final clause if stray messages are possible.
require Logger
@impl true
def handle_info(:tick, state) do
Process.send_after(self(), :tick, 1_000)
{:noreply, %{state | count: state.count + 1}}
end
def handle_info(msg, state) do
Logger.warning("unexpected message: #{inspect(msg)}", server: __MODULE__)
{:noreply, state}
endReply with `{:error, reason}` for failures the caller can act on; let everything else crash so the supervisor restarts with clean state.
require Logger
@impl true
def handle_call({:charge, order_id}, _from, state) do
case Payments.charge(order_id) do
{:ok, receipt} ->
{:reply, {:ok, receipt}, state}
{:error, reason} ->
Logger.error("charge failed: #{inspect(reason)}", order_id: order_id)
{:reply, {:error, reason}, state}
end
end`terminate/2` runs when a callback returns `{:stop, ...}` or raises. On supervisor shutdown it runs **only if the process traps exits** (`Process.flag(:trap_exit, true)` in `init/1`) and its child spec `shutdown` isn't `:brutal_kill`. It never runs on `Process.exit(pid, :kill)` or a VM crash. Put cleanup that must happen in another process that monitors this one.
---
· 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…