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).
73
91%
Does it follow best practices?
Impact
—
No eval scenarios have been run
Advisory
Suggest reviewing before use
Use this skill before writing ANY .ex or .exs file.
@impl true before every callback function (mount, handle_event, handle_info, etc.){:ok, result} | {:error, reason} tuples for fallible operationswith for 2+ sequential fallible operations instead of nested case? suffix for predicate functions, ! suffix for dangerous functions@doc and @moduledoc for all public APIsString.to_atom/1 on user input (atom table exhaustion)for comprehensions before chaining 3+ Enum operations✅ Good — prefer multi-clause functions with pattern matching over if/else:
def handle_response(%{status: 200, body: body}), do: {:ok, body}
def handle_response(%{status: 404}), do: {:error, :not_found}
def handle_response(_), do: {:error, :unknown}def process_user(user) do
user
|> validate_user()
|> transform_user()
|> save_user()
end❌ Bad (nested case):
def create_post(params) do
case validate_params(params) do
{:ok, valid_params} ->
case create_changeset(valid_params) do
{:ok, changeset} ->
Repo.insert(changeset)
error -> error
end
error -> error
end
end✅ Good (with):
def create_post(params) do
with {:ok, valid_params} <- validate_params(params),
{:ok, changeset} <- create_changeset(valid_params),
{:ok, post} <- Repo.insert(changeset) do
{:ok, post}
end
enddef transfer_money(from_id, to_id, amount) do
with {:ok, from_account} <- get_account(from_id),
{:ok, to_account} <- get_account(to_id),
:ok <- validate_balance(from_account, amount),
{:ok, _} <- debit(from_account, amount),
{:ok, _} <- credit(to_account, amount) do
{:ok, :transfer_complete}
else
{:error, :insufficient_funds} ->
{:error, "Not enough money in account"}
{:error, :not_found} ->
{:error, "Account not found"}
error ->
{:error, "Transfer failed: #{inspect(error)}"}
end
enddef calculate(x) when is_integer(x) and x > 0 do
x * 2
end
def calculate(_), do: {:error, :invalid_input}❌ Bad (multiple passes):
list
|> Enum.map(&transform/1)
|> Enum.filter(&valid?/1)
|> Enum.map(&format/1)✅ Good (single pass):
for item <- list,
transformed = transform(item),
valid?(transformed) do
format(transformed)
end| Element | Convention | Example |
|---|---|---|
| Module names | PascalCase | MyApp.Accounts.User |
| Function names | snake_case | create_user/1 |
| Variables | snake_case | user_name |
| Atoms | :snake_case | :not_found |
| Predicate functions | end with ? | valid?, empty? |
| Dangerous functions | end with ! | save!, update! |
def fetch_user(id) do
case Repo.get(User, id) do
nil -> {:error, :not_found}
user -> {:ok, user}
end
end
# Usage
case fetch_user(123) do
{:ok, user} -> IO.puts("Found: #{user.name}")
{:error, :not_found} -> IO.puts("User not found")
endUse ! suffix for functions that raise on failure. Prefer the non-bang variant in application logic; use bang in tests or when failure is truly unrecoverable.
def process_data(nil), do: {:error, :no_data}
def process_data([]), do: {:error, :empty_list}
def process_data(data) when is_list(data) do
{:ok, Enum.map(data, &transform/1)}
end✅ Good (trust your types):
def get_username(%User{name: name}), do: nameIf the user is nil or missing a name, it's a bug that should crash and be fixed.
Successor skills to apply after this one:
otp-essentials insteadecto-essentials insteadwith statements) — this skill is for enforcing fundamentals, not advanced idiomstypespec-dialyzer instead