style
Elixir coding style and conventions. Use when writing idiomatic Elixir, avoiding bang functions in business logic, using pattern matching for error handling, or designing Ecto schemas as the single source of truth for data validation.
What this skill does
# Elixir Style and Conventions
## When to Activate
Activate when:
- Writing or reviewing Elixir code for idiomatic style
- Deciding between bang (`!`) and non-bang function variants
- Handling errors from external APIs, user input, or DB operations
- Designing Ecto schemas and changesets for validation
- Building Phoenix forms connected to changeset validation
- Chaining multi-step operations with `with` or `case`
- Choosing between `map.key` and `map[:key]` access patterns
- Implementing non-DB data structures (search forms, filter params)
This skill complements `elixir:anti-patterns` — that skill covers what to avoid; this one covers what to do instead.
## Tagged Tuples and Return Conventions
Elixir functions signal success or failure through return values, not exceptions.
**Standard return shapes:**
```elixir
# Two-element ok/error tuples (most common)
{:ok, result}
{:error, reason}
# Bare atoms for side-effect operations
:ok
:error
# Richer error tuples for typed failures
{:error, :not_found}
{:error, :unauthorized}
{:error, %Ecto.Changeset{}}
```
**Why this convention matters:**
Pattern matching on tagged tuples is the foundation of Elixir error handling. When every function in a call chain returns `{:ok, _}` or `{:error, _}`, `with` expressions can thread success values through and exit early on the first failure — without nested `if` or `try/rescue`.
```elixir
# Callers can match exhaustively
case fetch_user(id) do
{:ok, user} -> render_profile(user)
{:error, :not_found} -> send_resp(conn, 404, "Not found")
{:error, reason} -> send_resp(conn, 500, inspect(reason))
end
```
## Logger over IO.puts
All application output goes through `Logger`. Do NOT use `IO.puts/1`, `IO.write/1`, or `:io.format/2` from application code. Logger respects log levels, supports metadata, and routes through the configured backends (console, file, structured log shippers). `IO.puts` writes directly to stdout regardless of log level and bypasses backend configuration.
```elixir
# WRONG — IO.puts("worker started")
# CORRECT
Logger.info("worker started")
Logger.info("worker started", worker_id: id, module: __MODULE__)
```
Flush concerns at shutdown go through Logger configuration (`Logger.flush/0` in an `at_exit`, or the `:logger_std_h` flush settings), not by bypassing Logger to write directly. The only legitimate use of `IO` in app code is reading from / writing to a specific IO device the user requested (e.g., a CLI tool's stdout output, where the OUTPUT IS the contract).
## Bang Functions: When to Avoid
### The Convention
Every standard library function with a `!` variant follows this contract:
| Non-bang | Bang |
|---|---|
| `File.read/1` → `{:ok, content}` or `{:error, reason}` | `File.read!/1` → `content` or raises |
| `Map.fetch/2` → `{:ok, value}` or `:error` | `Map.fetch!/2` → `value` or raises `KeyError` |
| `Repo.insert/1` → `{:ok, struct}` or `{:error, changeset}` | `Repo.insert!/1` → `struct` or raises |
| `Repo.get/2` → `struct` or `nil` | `Repo.get!/2` → `struct` or raises `Ecto.NoResultsError` |
### When Bangs Are Appropriate
Use bang functions when failure represents a programming error or an invalid system state that should crash loudly:
```elixir
# Application startup — missing config is a bug, not a user error
def start(_type, _args) do
api_key = Application.fetch_env!(:my_app, :stripe_api_key)
# ...
end
# Seeds and migrations — invalid data is a developer error
Repo.insert!(%User{email: "[email protected]", role: :admin})
# Pipelines operating on already-validated, known-good data
"hello world"
|> String.split()
|> Enum.map(&String.capitalize/1)
|> Enum.join(" ")
# Tests asserting expected state
user = Repo.get!(User, user_id)
```
### When to Avoid Bangs
Avoid bang functions wherever failure is a normal, expected outcome:
```elixir
# BAD: user input can always fail validation
def create_user(conn, %{"user" => params}) do
user = Repo.insert!(%User{email: params["email"]}) # raises on validation failure
json(conn, %{id: user.id})
end
# GOOD: handle the error path explicitly
def create_user(conn, %{"user" => params}) do
case %User{} |> User.changeset(params) |> Repo.insert() do
{:ok, user} -> json(conn, %{id: user.id})
{:error, changeset} -> conn |> put_status(422) |> json(changeset_errors(changeset))
end
end
```
```elixir
# BAD: external APIs can return 404, 500, network errors
def fetch_payment(payment_id) do
Stripe.PaymentIntent.retrieve!(payment_id) # raises on API error
end
# GOOD: return a tagged tuple, let the caller decide
def fetch_payment(payment_id) do
case Stripe.PaymentIntent.retrieve(payment_id) do
{:ok, payment} -> {:ok, payment}
{:error, %{code: "resource_missing"}} -> {:error, :not_found}
{:error, reason} -> {:error, reason}
end
end
```
**Rule of thumb:** if a user action, external service, or DB constraint could cause the failure, use the non-bang variant and handle it.
## Pattern Matching for Error Handling
### `with` for Chaining Dependent Operations
Use `with` when multiple steps must all succeed, and any failure should short-circuit to an error response:
```elixir
def register_user(params) do
with {:ok, validated} <- validate_registration_params(params),
{:ok, user} <- create_user(validated),
{:ok, _profile} <- create_default_profile(user),
{:ok, _email} <- send_welcome_email(user) do
{:ok, user}
else
{:error, %Ecto.Changeset{} = cs} -> {:error, {:validation_failed, cs}}
{:error, :email_unavailable} -> {:error, :email_taken}
{:error, reason} -> {:error, reason}
end
end
```
The `else` block is optional. Without it, unmatched patterns in `with` clauses propagate the first failing value as the return value of the entire expression.
**Keep `with` flat.** Nesting `with` inside `with` is a sign the function is doing too much:
```elixir
# BAD: nested with — hard to follow
with {:ok, user} <- fetch_user(id) do
with {:ok, order} <- fetch_order(user, order_id) do
{:ok, {user, order}}
end
end
# GOOD: flat with
with {:ok, user} <- fetch_user(id),
{:ok, order} <- fetch_order(user, order_id) do
{:ok, {user, order}}
end
```
### `case` for Single-Expression Branching
Use `case` when branching on one expression with multiple outcomes:
```elixir
def handle_webhook(event_type, payload) do
case event_type do
"payment.succeeded" -> handle_payment_succeeded(payload)
"payment.failed" -> handle_payment_failed(payload)
"customer.created" -> handle_customer_created(payload)
unknown -> Logger.warning("Unhandled webhook: #{unknown}")
end
end
```
### Multi-Clause Function Heads
Use multi-clause functions for structural dispatch — matching on the shape or value of arguments:
```elixir
defmodule MyApp.Notifier do
def notify(%User{email: nil} = user, _message) do
Logger.warning("No email for user #{user.id}, skipping notification")
{:error, :no_email}
end
def notify(%User{} = user, message) do
Mailer.deliver(to: user.email, body: message)
end
def notify({:admin, email}, message) do
Mailer.deliver(to: email, subject: "[ADMIN] " <> message.subject, body: message)
end
end
```
### Avoid `try/rescue` for Expected Errors
`try/rescue` is reserved for exceptions from code outside your control (third-party libraries that raise instead of returning error tuples). It is not idiomatic for expected application errors:
```elixir
# BAD: using rescue for control flow
def parse_integer(str) do
try do
{:ok, String.to_integer(str)}
rescue
ArgumentError -> {:error, :invalid_integer}
end
end
# GOOD: use functions that return ok/error tuples
def parse_integer(str) do
case Integer.parse(str) do
{value, ""} -> {:ok, value}
_ -> {:error, :invalid_integer}
end
end
```
## Ecto as Data Shape Gatekeeper
### The Principle
Validate data once, at the changeset layer. Do not duplicate validation logic in controllers, LiveView callbacks, or service modules.
```
SRelated in Writing & Docs
jax-development
IncludedUse this skill when the user is writing, debugging, profiling, refactoring, reviewing, benchmarking, parallelising, exporting, or explaining JAX code, or when they mention JAX, jax.numpy, jit, grad, value_and_grad, vmap, scan, lax, random keys, pytrees, jax.Array, sharding, Mesh, PartitionSpec, NamedSharding, pmap, shard_map, Pallas, XLA, StableHLO, checkify, profiler, or the JAX repo. It helps turn NumPy or PyTorch-style code into pure functional JAX, fix tracer/control-flow/shape/PRNG bugs, remove recompiles and host-device syncs, choose transforms and sharding strategies, inspect jaxpr/lowering/IR, and benchmark compiled code correctly.
nature-article-writer
IncludedDrafts, rewrites, diagnostically critiques, and style-calibrates primary research manuscripts for Nature and Nature Portfolio journals. Use when the user wants a Nature-style title, summary paragraph or abstract, introduction, results, discussion, methods, figure legends, presubmission enquiry, cover letter, reviewer response, or when a scientific draft sounds generic, jargon-heavy, structurally weak, or AI-ish and needs precise, broad-reader-friendly prose without inventing data, analyses, or references. Best for primary research articles and letters rather than reviews or press releases unless explicitly adapting one.
deckrd
IncludedDocument-driven framework that derives requirements, specifications, implementation plans, and executable tasks from goals through structured AI dialogue. Use when user says "write requirements", "create spec", "plan implementation", "derive tasks", "structure this feature", "break down into tasks", or "document this module". Also use for reverse engineering existing code into docs (/deckrd rev). Do NOT use for direct code writing — use /deckrd-coder after tasks are generated. Do NOT use when the user only wants to run or fix existing code without planning.
clinical-decision-support
IncludedGenerate professional clinical decision support (CDS) documents for pharmaceutical and clinical research settings, including patient cohort analyses (biomarker-stratified with outcomes) and treatment recommendation reports (evidence-based guidelines with decision algorithms). Supports GRADE evidence grading, statistical analysis (hazard ratios, survival curves, waterfall plots), biomarker integration, and regulatory compliance. Outputs publication-ready LaTeX/PDF format optimized for drug development, clinical research, and evidence synthesis.
handling-sf-data
IncludedSalesforce data operations with 130-point scoring. Use this skill to create, update, delete, bulk import/export, generate test data, and clean up org records using sf CLI and anonymous Apex. TRIGGER when: user creates test data, performs bulk import/export, uses sf data CLI commands, needs data factory patterns for Apex tests, or needs to seed/clean records in a Salesforce org. DO NOT TRIGGER when: SOQL query writing only (use querying-soql), Apex test execution (use running-apex-tests), or metadata deployment (use deploying-metadata).
accelint-ac-to-playwright
IncludedConvert and validate acceptance criteria for Playwright test automation. Use when user asks to (1) review/evaluate/check if AC are ready for automation, (2) assess if AC can be converted as-is, (3) validate AC quality for Playwright, (4) turn AC into tests, (5) generate tests from acceptance criteria, (6) convert .md bullets or .feature Gherkin files to Playwright specs, (7) create test automation from requirements. Handles both bullet-style markdown and Gherkin syntax with JSON test plan generation and validation.