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 this skill before writing ANY GenServer, Supervisor, Task, or Agent module.
@impl true before GenServer/Agent callbacks (init, handle_call, handle_cast, handle_info, terminate)init/1 fast — no blocking calls, no DB queries; use handle_continue for expensive setupGenServer.call(pid, ...) directlyTask.async/Task.await with bounded timeouts — never Task.async without a corresponding Task.await or Task.yield:DOWN messages from monitored processes — don't let them go unhandledTask.Supervisor for fire-and-forget supervised workAlways wrap GenServer calls behind a public module API.
❌ Bad — leaks GenServer implementation:
GenServer.call(MyApp.Cache, {:get, key})✅ Good — public API hides the GenServer:
defmodule MyApp.Cache do
use GenServer
# --- Public API ---
def start_link(opts) do
name = Keyword.get(opts, :name, __MODULE__)
GenServer.start_link(__MODULE__, opts, name: name)
end
def get(key, server \\ __MODULE__) do
GenServer.call(server, {:get, key})
end
def put(key, value, server \\ __MODULE__) do
GenServer.cast(server, {:put, key, value})
end
# --- Callbacks ---
@impl true
def init(_opts) do
{:ok, %{}}
end
@impl true
def handle_call({:get, key}, _from, state) do
{:reply, Map.get(state, key), state}
end
@impl true
def handle_cast({:put, key, value}, state) do
{:noreply, Map.put(state, key, value)}
end
endNever block in init/1. Use handle_continue for expensive setup.
❌ Bad — blocks the supervisor:
@impl true
def init(opts) do
data = MyApp.Repo.all(MyApp.Item) # Blocks!
{:ok, %{items: data}}
end✅ Good — returns immediately:
@impl true
def init(opts) do
{:ok, %{items: []}, {:continue, :load_data}}
end
@impl true
def handle_continue(:load_data, state) do
data = MyApp.Repo.all(MyApp.Item)
{:noreply, %{state | items: data}}
end# call — synchronous, caller waits for reply (use for reads, queries)
def get_count(server \\ __MODULE__) do
GenServer.call(server, :get_count)
end
@impl true
def handle_call(:get_count, _from, state) do
{:reply, state.count, state}
end
# cast — asynchronous (use for writes, side effects)
def increment(server \\ __MODULE__) do
GenServer.cast(server, :increment)
end
@impl true
def handle_cast(:increment, state) do
{:noreply, %{state | count: state.count + 1}}
end@impl true
def init(_opts) do
Process.send_after(self(), :tick, 1_000)
{:ok, %{count: 0}}
end
@impl true
def handle_info(:tick, state) do
Process.send_after(self(), :tick, 1_000)
{:noreply, %{state | count: state.count + 1}}
end| Strategy | Behaviour |
|---|---|
:one_for_one | Restart only the failed child (most common) |
:one_for_all | Restart ALL children when one fails |
:rest_for_one | Restart failed child and all children started after it |
Supervisor.start_link(children, strategy: :one_for_one)defmodule MyApp.Application do
use Application
@impl true
def start(_type, _args) do
children = [
MyApp.Repo,
{Phoenix.PubSub, name: MyApp.PubSub},
MyApp.Cache,
MyAppWeb.Endpoint
]
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
Supervisor.start_link(children, opts)
end
enddefmodule MyApp.RoomSupervisor do
use DynamicSupervisor
def start_link(init_arg) do
DynamicSupervisor.start_link(__MODULE__, init_arg, name: __MODULE__)
end
@impl true
def init(_init_arg) do
DynamicSupervisor.init(strategy: :one_for_one)
end
def start_room(room_id) do
spec = {MyApp.Room, room_id: room_id}
DynamicSupervisor.start_child(__MODULE__, spec)
end
def stop_room(pid) do
DynamicSupervisor.terminate_child(__MODULE__, pid)
end
endWhen wiring up a new supervision tree, follow this sequence:
one_for_one unless children are interdependentApplication.start/2 — or to a parent supervisor's child listmix run --no-halt or iex -S mix and confirm no crashes:observer.start() in IEx to view the live supervision treeSupervisor.count_children(MyApp.Supervisor) confirms expected active/specs countsProcess.exit(pid, :kill) and confirm the supervisor restarts the child# Quick verification in IEx
iex> Supervisor.which_children(MyApp.Supervisor)
# [{MyApp.Cache, #PID<0.200.0>, :worker, [MyApp.Cache]}, ...]
iex> Supervisor.count_children(MyApp.Supervisor)
# %{active: 3, specs: 3, supervisors: 0, workers: 3}# Parallel fetch with bounded timeout
task1 = Task.async(fn -> fetch_user_profile(user_id) end)
task2 = Task.async(fn -> fetch_user_posts(user_id) end)
profile = Task.await(task1, 5_000)
posts = Task.await(task2, 5_000)user_ids
|> Task.async_stream(&fetch_user/1, max_concurrency: 4, timeout: 10_000)
|> Enum.map(fn {:ok, result} -> result end)# Add to your supervision tree
{Task.Supervisor, name: MyApp.TaskSupervisor}
# Start supervised tasks
Task.Supervisor.start_child(MyApp.TaskSupervisor, fn ->
send_welcome_email(user)
end)Use Agent for simple state that doesn't need the full GenServer pattern:
defmodule MyApp.Counter do
use Agent
def start_link(initial_value),
do: Agent.start_link(fn -> initial_value end, name: __MODULE__)
def value, do: Agent.get(__MODULE__, & &1)
def increment, do: Agent.update(__MODULE__, &(&1 + 1))
end# In application supervision tree
{Registry, keys: :unique, name: MyApp.Registry}
# In GenServer start_link
def start_link(room_id) do
GenServer.start_link(__MODULE__, room_id,
name: {:via, Registry, {MyApp.Registry, {:room, room_id}}}
)
end
# Lookup
def get_room(room_id) do
case Registry.lookup(MyApp.Registry, {:room, room_id}) do
[{pid, _}] -> {:ok, pid}
[] -> {:error, :not_found}
end
endA GenServer owns the ETS table (ensuring cleanup on crash) while reads bypass it entirely.
defmodule MyApp.EtsCache do
use GenServer
@table :my_app_cache
# --- Public API ---
def start_link(opts) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
# Reads go directly to ETS — no GenServer roundtrip
def get(key) do
case :ets.lookup(@table, key) do
[{^key, value}] -> {:ok, value}
[] -> {:error, :not_found}
end
end
# Writes go through the GenServer to serialize mutations
def put(key, value), do: GenServer.call(__MODULE__, {:put, key, value})
def delete(key), do: GenServer.call(__MODULE__, {:delete, key})
# --- Callbacks ---
@impl true
def init(_opts) do
table = :ets.new(@table, [:named_table, :set, :public, read_concurrency: true])
{:ok, %{table: table}}
end
@impl true
def handle_call({:put, key, value}, _from, state) do
:ets.insert(@table, {key, value})
{:reply, :ok, state}
end
@impl true
def handle_call({:delete, key}, _from, state) do
:ets.delete(@table, key)
{:reply, :ok, state}
end
endKey ETS options:
| Option | Meaning |
|---|---|
:set / :bag | Unique keys vs. duplicate keys allowed |
:public / :protected | Any process reads/writes vs. owner writes, all read |
read_concurrency: true | Optimise for concurrent reads |
write_concurrency: true | Optimise for concurrent writes (trades some read performance) |
| ❌ Don't | ✅ Do |
|---|---|
Block in init/1 with DB queries or heavy setup | Return fast and defer expensive work to handle_continue |
Wrap every failure in defensive try/rescue | Let it crash and rely on the supervisor to restart cleanly |
| Start long-running processes unsupervised | Put them under a supervisor that owns their lifecycle |
| Register processes with dynamically-built atoms | Use Registry — the atom table is finite and never GC'd |
Expose GenServer.call(pid, ...) to callers | Wrap calls behind a public module API |
| Serialize read-heavy shared state through one GenServer | Read directly from ETS; funnel only writes through the server |
Leave :DOWN/monitor messages unhandled | Handle them explicitly in handle_info |
| Predecessor | This Skill | Successor |
|---|---|---|
| elixir-essentials | otp-essentials | telemetry-essentials |
| elixir-essentials | otp-essentials | oban-essentials |
| testing-essentials | otp-essentials | telemetry-essentials |
.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