Claude Skill

elixir-idioms

OTP/BEAM patterns and Elixir idioms — GenServer, Supervisor, Task, Registry,

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download oliver-kriska-claude-elixir-phoenix-targets_amp_skills_elixir-idioms-9767a82.zip · 22 KB
Part of oliver-kriska/claude-elixir-phoenix — 93 skills

Install

skills CLI npx skills add https://github.com/oliver-kriska/claude-elixir-phoenix/tree/main/targets/amp/skills/elixir-idioms
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install oliver-kriska-claude-elixir-phoenix@llmmart
Git git clone https://github.com/oliver-kriska/claude-elixir-phoenix.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole oliver-kriska/claude-elixir-phoenix collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Elixir Idioms

Reference for writing idiomatic Elixir code with BEAM-aware patterns.

Iron Laws — Never Violate These

  1. NO PROCESS WITHOUT A RUNTIME REASON — Processes model concurrency, state, isolation—NOT code structure
  2. MESSAGES ARE COPIED — Keep messages small (except binaries >64 bytes)
  3. GUARDS USE and/or/not — Never use short-circuit operators in guards (guards require boolean operands)
  4. CHANGESETS FOR EXTERNAL DATA — Use cast/4 for user input, change/2 for internal
  5. RESCUE ONLY FOR EXTERNAL CODE — Never use rescue for control flow
  6. NO DYNAMIC ATOM CREATION — String.to_atom(user_input) causes memory leak (atoms aren't GC'd)
  7. @external_resource FOR COMPILE-TIME FILES — Modules reading files at compile time MUST declare @external_resource
  8. SUPERVISE ALL LONG-LIVED PROCESSES — Never bare GenServer.start_link/Agent.start_link in production. Use supervision trees
  9. WRAP THIRD-PARTY LIBRARY APIs — Always facade external deps behind a project-owned module. Enables swapping without touching callers
  10. MIX TASKS START ONLY WHAT THEY NEED — Mix.Task.run("app.config") + Application.ensure_all_started/1, never Mix.Task.run("app.start") (boots the FULL tree: endpoint port, Oban consuming)
  11. CAPTURE LOCALE BEFORE SPAWNING — Gettext/CLDR locale is process-local. Read it in the caller and pass explicitly; a spawned Task/GenServer starts with the default locale

BEAM Architecture (Why Elixir Works This Way)

  • Processes are cheap (2.6KB) — Spawn liberally for concurrency/isolation
  • Complete memory isolation — No shared state, no locks needed
  • Messages are copied (except binaries >64 bytes) — Keep messages small
  • Per-process GC — No global GC pauses
  • "Let it crash" — Supervisors restart to known-good state

Core Principles

  1. Pattern match over conditionals — Function heads first, then case, then cond
  2. Tagged tuples for expected failures — {:ok, _}/{:error, _} for expected errors, raise for bugs
  3. Pipe operator for data transformation — Start with data, never pipe single calls
  4. Let it crash — Handle expected errors, crash on unexpected ones
  5. Explicit over implicit — Be clear about intentions

Quick Decision Trees

Control Flow

Need patterns? → case (or function heads)
Multiple operations? → with
Boolean conditions? → cond (multiple) or if (single)

Error Handling

Expected failure? → {:ok, _}/{:error, _} tuples
Unexpected/bug? → raise exception (let supervisor handle)
External library? → rescue (only here!)

OTP

Need state?
├─ No → Plain functions
├─ Simple get/update → Agent or ETS
├─ Complex messages/timeouts → GenServer
└─ One-off async → Task

Quick Patterns

# Pattern match in function head
def process(%{status: :active} = user), do: activate(user)
def process(%{status: :inactive} = user), do: deactivate(user)

# with for happy path
with {:ok, user} <- get_user(id),
     {:ok, order} <- create_order(user) do
  {:ok, order}
end

# Task for async
Task.Supervisor.async_nolink(TaskSup, fn -> work() end)
|> Task.yield(5000) || Task.shutdown(task)

Common Pitfalls

Wrong Right
length(list) == 0 list == [] or Enum.empty?(list)
list ++ [item] [item \| list] \|> Enum.reverse()
String.to_atom(input) String.to_existing_atom(input)
spawn(fn -> log(conn) end) ip = conn.ip; spawn(fn -> log(ip) end)
unless condition if !condition (unless deprecated in 1.18)

References

For detailed patterns, see:

  • references/pattern-matching.md - Pattern matching, guards, binary matching
  • references/otp-patterns.md - GenServer, Supervisor, Task, Registry
  • references/error-handling.md - Tagged tuples, rescue, with
  • references/with-and-pipes.md - When to use with and |> (idiomatic patterns)
  • references/troubleshooting.md - Production BEAM debugging (memory, performance, crashes)
  • references/anti-patterns.md - Common mistakes and fixes
  • references/mix-tasks.md - Mix task naming, option parsing, shell output
  • references/elixir-118-features.md - Duration module, dbg improvements (1.18+)
  • references/elixir-120-type-system.md - Gradual type checker, dynamic(), verified bugs as compile warnings (1.20+, OTP 27+)
Files (claude-elixir-phoenix)
  • references
    • anti-patterns.md 4.6 KB
      # Anti-Patterns Reference
      
      ## Memory & Performance
      
      ```elixir
      # WRONG: length/1 for empty check (O(n))
      length(list) == 0
      
      # RIGHT: Pattern match or Enum.empty?
      list == []
      Enum.empty?(list)
      
      # WRONG: ++ to append (O(n))
      list ++ [item]
      
      # RIGHT: Prepend and reverse, or use different structure
      [item | list] |> Enum.reverse()
      
      # WRONG: Dynamic atom creation (memory leak - atoms aren't GC'd)
      String.to_atom(user_input)
      
      # RIGHT: Explicit mapping or existing atoms
      defp status_atom("ok"), do: :ok
      defp status_atom("error"), do: :error
      # Or: String.to_existing_atom(input)
      
      # WRONG: Sending unnecessary data (copies entire vars between processes)
      spawn(fn -> log_ip(conn.remote_ip) end)  # Copies entire conn!
      GenServer.cast(pid, {:process, large_struct.id})  # Copies entire struct!
      
      # RIGHT: Extract minimal data before spawning or sending
      ip = conn.remote_ip
      spawn(fn -> log_ip(ip) end)
      id = large_struct.id
      GenServer.cast(pid, {:process, id})
      ```
      
      ## Message Handling
      
      ```elixir
      # WRONG: Selective receive without reference (O(n) mailbox scan)
      receive do
        {:response, data} -> data  # Scans entire mailbox
      end
      
      # RIGHT: Reference-based (compiler optimizes)
      ref = make_ref()
      send(server, {self(), ref, :request})
      receive do
        {^ref, response} -> response  # Compiler uses receive marker
      end
      ```
      
      ## Code Organization
      
      ```elixir
      # WRONG: String keys internally
      %{"name" => value}
      
      # RIGHT: Atom keys internally
      %{name: value}
      
      # WRONG: Macro when function works
      defmacro sum(a, b), do: quote do: unquote(a) + unquote(b)
      
      # RIGHT: Just use a function
      def sum(a, b), do: a + b
      ```
      
      ## OTP Anti-Patterns
      
      ```elixir
      # ANTI-PATTERN: GenServer for stateless computation
      def add(a, b), do: GenServer.call(__MODULE__, {:add, a, b})
      def handle_call({:add, a, b}, _from, state), do: {:reply, a + b, state}
      
      # CORRECT: Just use functions
      def add(a, b), do: a + b
      
      # ANTI-PATTERN: Single GenServer bottleneck
      # All requests serialize through one process
      
      # CORRECT: Use ETS for reads, GenServer for writes
      # Or partition into multiple processes
      ```
      
      ## Assertiveness (from official Elixir anti-patterns)
      
      ```elixir
      # WRONG: Non-assertive map access — nil on missing required key
      user[:email]  # Returns nil silently if :email missing
      
      # RIGHT: Assert required keys exist
      user.email    # Raises KeyError — fail fast
      # Use [:key] ONLY for truly optional keys
      config[:timeout] || 5000
      
      # WRONG: Catch-all hides bugs
      case fetch_user(id) do
        {:ok, user} -> process(user)
        _ -> :error  # What failed? Why?
      end
      
      # RIGHT: Match known cases explicitly
      case fetch_user(id) do
        {:ok, user} -> process(user)
        {:error, :not_found} -> {:error, :not_found}
      end
      
      # WRONG: Boolean obsession — multiple related booleans
      %{is_admin: true, is_editor: false, is_viewer: false}
      
      # RIGHT: Single atom field
      %{role: :admin}
      # Or enum-like pattern in schema:
      # field :role, Ecto.Enum, values: [:admin, :editor, :viewer]
      ```
      
      ## Stream vs Enum
      
      ```elixir
      # Stream processes lazily—only computes what's needed
      1..1_000_000
      |> Stream.map(&(&1 * 3))
      |> Stream.filter(&(rem(&1, 2) != 0))
      |> Enum.take(5)  # Only processes ~5 elements
      
      # Enum processes eagerly—entire collection each step
      1..1_000_000
      |> Enum.map(&(&1 * 3))      # Creates 1M list
      |> Enum.filter(&(rem(&1, 2) != 0))  # Creates another list
      |> Enum.take(5)
      ```
      
      **Use Enum** for small/medium collections, immediate results.
      **Use Stream** for large collections, multiple transformations, memory constraints.
      
      ## Pipe Operator Misuse
      
      ```elixir
      # AVOID: Pipe with single step
      user |> do_something()  # Just: do_something(user)
      
      # AVOID: Start with function call
      String.upcase("hello") |> String.split()  # Start with "hello"
      
      # DO: Use tap/1 for side effects (returns original value)
      user
      |> validate()
      |> tap(&Logger.info("Validated: #{&1.name}"))  # Returns user
      |> persist()
      
      # DO: Use then/1 for transformations
      user
      |> validate()
      |> persist()
      |> then(&{:ok, &1})  # Transforms to tagged tuple
      ```
      
      ## Binary Handling
      
      ```elixir
      # ANTI-PATTERN: Small sub-binary keeps large parent alive
      <<small::binary-size(100), _::binary>> = one_gb_binary
      
      # DO: Copy if keeping only the small part
      small = :binary.copy(small)
      ```
      
      ## Tail Recursion
      
      For tail call optimization, recursive call must be the **last operation**:
      
      ```elixir
      # Tail recursive (optimized - constant stack)
      def sum(list), do: do_sum(list, 0)
      defp do_sum([], acc), do: acc
      defp do_sum([head | tail], acc), do: do_sum(tail, head + acc)
      
      # Not tail recursive (builds stack - O(n) memory)
      def factorial(0), do: 1
      def factorial(n), do: n * factorial(n - 1)  # Multiplication after recursion
      ```
      
      **Rule of thumb**: Use Enum for 95% of cases—cleaner and well-tested.
      
    • elixir-118-features.md 3.6 KB
      # Elixir 1.18 Features Reference
      
      > **Official changelog**: <https://github.com/elixir-lang/elixir/blob/main/CHANGELOG.md>
      > **HexDocs**: <https://hexdocs.pm/elixir/> — Use `hexdocs-fetcher` for latest API docs.
      
      ## Duration Module (Elixir 1.18+)
      
      Native duration representation without external dependencies.
      
      ### Creating Durations
      
      ```elixir
      # From keyword list
      Duration.new!(hour: 2, minute: 30)
      #=> %Duration{hour: 2, minute: 30}
      
      # Common units
      Duration.new!(day: 7)
      Duration.new!(week: 1)
      Duration.new!(second: 3600)
      
      # Negative durations
      Duration.new!(hour: -1)
      ```
      
      ### Duration Arithmetic
      
      ```elixir
      # Adding durations
      Duration.add(Duration.new!(hour: 1), Duration.new!(minute: 30))
      #=> %Duration{hour: 1, minute: 30}
      
      # Adding to DateTime/NaiveDateTime
      DateTime.add(DateTime.utc_now(), Duration.new!(hour: 24))
      
      # Subtracting
      DateTime.add(DateTime.utc_now(), Duration.negate(Duration.new!(day: 7)))
      ```
      
      ### Duration in Oban Jobs
      
      ```elixir
      # Schedule job for specific duration from now
      defmodule MyApp.Workers.ReminderWorker do
        use Oban.Worker
      
        def schedule_reminder(user_id, delay_duration) do
          scheduled_at = DateTime.add(DateTime.utc_now(), delay_duration)
      
          %{user_id: user_id}
          |> new(scheduled_at: scheduled_at)
          |> Oban.insert()
        end
      end
      
      # Usage
      ReminderWorker.schedule_reminder(user.id, Duration.new!(day: 3))
      ```
      
      ### Duration in Cache TTLs
      
      ```elixir
      # With Cachex or similar
      def get_user_with_cache(user_id) do
        Cachex.fetch(:users_cache, user_id,
          ttl: Duration.to_milliseconds(Duration.new!(hour: 1))
        )
      end
      
      # Convert to different units
      Duration.to_seconds(Duration.new!(hour: 2))
      #=> 7200
      
      Duration.to_milliseconds(Duration.new!(minute: 5))
      #=> 300_000
      ```
      
      ### Anti-patterns
      
      ```elixir
      # AVOID: Magic numbers for time
      Process.send_after(self(), :timeout, 3600_000)  # What unit? Confusing!
      
      # PREFER: Duration makes intent clear
      Process.send_after(self(), :timeout,
        Duration.to_milliseconds(Duration.new!(hour: 1)))
      
      # AVOID: Manual arithmetic
      scheduled_at = DateTime.add(now, 7 * 24 * 60 * 60, :second)
      
      # PREFER: Readable duration
      scheduled_at = DateTime.add(now, Duration.new!(week: 1))
      ```
      
      ## Enhanced dbg/2 (Elixir 1.18+)
      
      ### Pipeline Debugging
      
      ```elixir
      # dbg shows each pipeline step
      users
      |> Enum.filter(&(&1.active))
      |> dbg()  # Shows filter result
      |> Enum.map(&(&1.name))
      |> dbg()  # Shows map result
      |> Enum.sort()
      ```
      
      ### Customizing Output
      
      ```elixir
      # In IEx or tests, customize dbg behavior
      # config/dev.exs
      config :elixir, :dbg_callback, {MyApp.Debug, :custom_dbg, []}
      
      # Custom module
      defmodule MyApp.Debug do
        def custom_dbg(code, options, env) do
          # Custom formatting, logging, etc.
          Macro.dbg(code, options, env)
        end
      end
      ```
      
      ## Calendar.strftime/2 Improvements
      
      ```elixir
      # ISO week numbers
      Calendar.strftime(~D[2026-02-05], "%G-W%V")
      #=> "2026-W06"
      
      # 12-hour format with AM/PM
      Calendar.strftime(~T[14:30:00], "%I:%M %p")
      #=> "02:30 PM"
      
      # Full datetime formatting
      Calendar.strftime(DateTime.utc_now(), "%Y-%m-%d %H:%M:%S %Z")
      #=> "2026-02-05 10:30:45 UTC"
      ```
      
      ## Deprecations in 1.18
      
      ### `unless/2` Deprecated
      
      `unless` is soft-deprecated in Elixir 1.18. The formatter
      automatically rewrites `unless condition` to `if !condition`.
      
      ```elixir
      # Deprecated — formatter will rewrite
      unless valid?(input) do
        {:error, :invalid}
      end
      
      # Current — what the formatter produces
      if !valid?(input) do
        {:error, :invalid}
      end
      ```
      
      **Rule**: Never write `unless` — always use `if !` or
      pattern match instead. This avoids unnecessary formatter churn.
      
      ## Compatibility Notes
      
      - Duration requires Elixir 1.18+
      - For projects on 1.17 or earlier, use Timex or manual arithmetic
      - Check version in mix.exs: `{:elixir, "~> 1.18"}`
      
    • elixir-120-type-system.md 5.9 KB
      # Elixir 1.20 Type System Reference
      
      > **Blog**: <https://elixir-lang.org/blog/2026/06/03/elixir-v1-20-0-released/>
      > **Changelog**: <https://elixir.hexdocs.pm/1.20.0/changelog.html#type-system-improvements>
      > **HexDocs**: use `hexdocs-fetcher` for the latest API docs.
      
      ## TL;DR
      
      Elixir 1.20 (released 2026-06-03) completed its **first type-system
      milestone**: the compiler now infers types and gradually type-checks **every**
      program **without any annotations**. It reports **dead/redundant code** and
      **verified bugs** — typing violations guaranteed to fail at runtime. Low false
      positives by design.
      
      **Requires OTP 27+.** No new struct/typespec syntax — that is a *future*
      milestone. There is **nothing new to write**; this release is about
      *interpreting new compiler diagnostics*.
      
      ## The one thing that changes day-to-day
      
      Type violations are emitted as **`mix compile` warnings** — built into the
      compiler. **Not** Dialyzer, **not** a separate tool, **no PLT**.
      
      > On Elixir 1.20+, `mix compile --warnings-as-errors` **now fails the build on
      > type violations.** This is what `phx-verify`, `phx-work` checkpoints, and
      > the "fix CI" pattern run everywhere — so a verified bug becomes a hard
      > failure, not a silent warning.
      
      When a `--warnings-as-errors` build that previously passed starts failing after
      a toolchain bump to 1.20, **suspect a newly-detected type violation before
      assuming the code regressed** — the code didn't change, the checker got smarter.
      
      ## The `dynamic()` mental model
      
      Unlike `any()` in most gradual languages ("anything goes, never flag"),
      Elixir's `dynamic()` is a **refinable range**:
      
      - **Compatibility** — a call is flagged **only when the supplied and accepted
        types are disjoint**. `dynamic(integer() or binary())` passed to `/` (wants a
        number) is fine — `integer()` overlaps. Passed to `Map.fetch!/2` (wants a
        map) it is flagged — disjoint.
      - **Narrowing** — usage refines the type. `data.a + data.b` narrows `data` to
        `%{..., a: number(), b: number()}` (the `...` = "may have other keys").
      
      ```elixir
      # value_or_error :: dynamic(integer() or binary()) at runtime
      Map.fetch!(value_or_error, :some_key)  # ⚠ violation: map ⟂ int|binary
      
      def add_a_and_b(data) do
        data.a + data   # ⚠ data narrowed to %{..., a: number()}, then used AS a number
      end
      ```
      
      ## What the checker catches
      
      | Kind | Example that warns |
      |------|--------------------|
      | **Verified bug** | calling `User.name(%{})` when it needs `%{..., name: term()}` |
      | **Disjoint call** | `String.upcase/1` on a value the checker proved is an integer |
      | **Dead clause** | a `case` clause that can never match given prior clauses |
      | **Bad field access** | `x.foo` after a guard proved `x` has **no** `:foo` key |
      | **Out-of-bounds** | `elem(x, 3)` after `tuple_size(x) < 3` |
      
      ## Inference you get for free
      
      **Guards** infer unions/intersections/negations:
      
      ```elixir
      def f(x, y) when is_list(x) and is_integer(y)        # x :: list, y :: integer
      def f({:ok, x} = y) when is_binary(x) or is_integer(x)
      def f(x) when is_map_key(x, :foo)        # x :: %{..., foo: dynamic()}
      def f(x) when not is_map_key(x, :foo)    # x :: %{..., foo: not_set()} → x.foo warns
      def f(x) when tuple_size(x) < 3          # elem(x, 3) warns
      ```
      
      **Clauses / occurrence typing** (`case`, `cond`, `with`) — earlier clauses
      refine later ones:
      
      ```elixir
      case System.get_env("SOME_VAR") do
        nil   -> :not_found
        value -> {:ok, String.upcase(value)}   # value :: binary() (nil already excluded)
      end
      ```
      
      **Maps** track non-atom keys by domain and typed stdlib ops:
      
      ```elixir
      %{123 => "hello", 456.0 => :ok}   # %{integer() => binary(), float() => :ok}
      Map.put(map, :key, 123)           # %{..., key: integer()}
      Map.delete(map, :key)             # %{..., key: not_set()}
      Map.replace(map, :key, 123)       # %{..., key: if_set(integer())}
      ```
      
      ## How to read & fix a type violation
      
      1. **Read the message literally** — it states the *accepted* vs *supplied*
         type. The fix is to make them overlap, not to silence the warning.
      2. **Check the narrowing chain** — the warning usually points at where the
         variable was *refined* (a guard, a prior clause, a field access), not just
         where it blew up.
      3. **It is almost always a real bug.** False positives are rare by design
         (disjoint-only). Prefer fixing the code over restructuring to dodge the
         checker.
      4. **Genuinely dynamic boundary?** If a value really is runtime-typed
         (external input), the disjoint-only rule already tolerates it — only a
         provably-impossible call warns.
      
      ## Compiler type checker vs Dialyzer
      
      They are **different tools** — keep both in mind:
      
      | | Compiler type checker (1.20+) | Dialyzer / dialyxir |
      |--|------------------------------|----------------------|
      | Runs in | `mix compile` (built-in) | `mix dialyzer` (separate, needs PLT) |
      | Setup | none | PLT build (slow first run) |
      | Basis | set-theoretic types, `dynamic()` | success typing |
      | Annotations | none needed | reads `@spec` |
      | Caught by `--warnings-as-errors` | **yes** | no |
      | Best at | disjoint calls, dead clauses, map/guard narrowing | `@spec` mismatches, contract violations, opaque misuse |
      
      Guidance: the compiler checker is now the **first line** of type safety and is
      always on. Dialyzer remains valuable for `@spec`/contract checking and opaque
      types — it is **complementary, not redundant**.
      
      ## Bonus: faster compiles
      
      New compiler option `:module_definition` (`:compiled` default, or
      `:interpreted`) can speed up large-project builds. Set in `mix.exs`:
      
      ```elixir
      elixirc_options: [module_definition: :interpreted]
      ```
      
      Does not change emitted `.beam` files — only how `defmodule` bodies execute at
      compile time (slight stacktrace-precision tradeoff).
      
      ## Compatibility notes
      
      - Type checking ships in Elixir **1.20+** on **OTP 27+** (compatible to OTP 29).
      - No code changes required to benefit — recompile and read the warnings.
      - No annotation syntax yet; typed structs and signatures are the next milestone.
      - Check version in `mix.exs`: `{:elixir, "~> 1.20"}`.
      
    • error-handling.md 2.7 KB
      # Error Handling Reference
      
      ## Decision Tree
      
      ```
      Is failure expected in normal operation?
      ├─ Yes → {:ok, _}/{:error, _} tuples
      │   ├─ Chaining operations? → with
      │   └─ Need both variants? → Provide foo/foo!
      └─ No (unexpected/bug) → raise exception
          └─ Supervision handles recovery
      ```
      
      ## When to Use Each
      
      | Use Exceptions | Use Error Tuples |
      |----------------|------------------|
      | Programming bugs | Expected failures |
      | Truly unexpected errors | User input validation |
      | Can't recover gracefully | Caller should handle |
      | "Let it crash" scenarios | Control flow decisions |
      
      ## Tagged Tuple Pattern
      
      ```elixir
      # Return error tuples for expected failures
      def divide(a, b) when b != 0, do: {:ok, a / b}
      def divide(_, 0), do: {:error, :division_by_zero}
      
      # Bang variant raises for callers who want to crash
      def divide!(a, b) do
        case divide(a, b) do
          {:ok, result} -> result
          {:error, reason} -> raise ArgumentError, "Cannot divide: #{reason}"
        end
      end
      ```
      
      ## With for Happy Path
      
      ```elixir
      # PREFER: with for multi-step operations
      def create_order(params) do
        with {:ok, user} <- get_user(params.user_id),
             {:ok, product} <- get_product(params.product_id),
             {:ok, order} <- Orders.create(user, product) do
          {:ok, order}
        end
      end
      
      # AVOID: with for single operation
      with {:ok, user} <- get_user(id), do: user  # Just use case!
      
      # AVOID: Complex else clauses - normalize errors in helpers
      with {:ok, a} <- normalize_step1(),
           {:ok, b} <- normalize_step2(a) do
        {:ok, b}
      end
      # Non-matching tuples pass through unchanged
      ```
      
      ## Assertive Map Access
      
      ```elixir
      # DON'T: Silent nil for missing required keys
      {point[:x], point[:y]}  # Returns {nil, nil} if keys missing!
      
      # DO: Use .key for required keys (raises KeyError if missing)
      {point.x, point.y}
      
      # DO: Pattern match
      def plot(%{x: x, y: y}), do: {x, y}  # Match fails if keys missing
      ```
      
      ## Rescue Only for External Code
      
      ```elixir
      # DO: Rescue external library exceptions
      def safe_parse(json) do
        {:ok, Jason.decode!(json)}
      rescue
        e in Jason.DecodeError -> {:error, e.message}
      end
      
      # DON'T: Rescue for control flow or catch all
      try do
        risky_operation()
      rescue
        _ -> :error  # Never do this - masks programming errors
      end
      ```
      
      ## Control Flow Decision Tree
      
      ```
      Need to match against patterns?
      ├─ Yes → case
      │   └─ Multiple dependent operations? → with
      └─ No (boolean conditions)
          ├─ Single condition? → if
          └─ Multiple conditions? → cond
      ```
      
      | Construct | Use When |
      |-----------|----------|
      | Function heads | First choice - most idiomatic |
      | `case` | Pattern matching single value |
      | `cond` | Multiple boolean conditions |
      | `with` | Chaining operations returning tagged tuples |
      | `if` | Single boolean check |
      
    • mix-tasks.md 2.9 KB
      # Mix Task Patterns
      
      > **Official docs**: <https://hexdocs.pm/mix/Mix.Task.html>
      > **Mix guides**: <https://github.com/elixir-lang/elixir/tree/main/lib/mix/lib/mix>
      
      ## Module Naming Convention
      
      Mix task module names map directly to the CLI command:
      
      ```elixir
      # mix my_app.validate → Mix.Tasks.MyApp.Validate
      defmodule Mix.Tasks.MyApp.Validate do
        @shortdoc "Validate configuration"
        @moduledoc "Detailed description..."
        use Mix.Task
      
        @impl Mix.Task
        def run(args) do
          # Parse args, do work
        end
      end
      ```
      
      **Rules:**
      
      - Module name segments map to `.`-separated CLI words
      - `CamelCase` in module → `snake_case` in CLI
      - `@shortdoc` is REQUIRED (shows in `mix help`)
      - `@moduledoc` for detailed `mix help my_app.validate`
      
      ## Option Parsing
      
      ```elixir
      @impl Mix.Task
      def run(args) do
        {opts, _rest, _invalid} =
          OptionParser.parse(args,
            strict: [
              dry_run: :boolean,
              type: :string,
              format: :string,
              verbose: :boolean
            ],
            aliases: [d: :dry_run, t: :type, f: :format, v: :verbose]
          )
      
        # Access with Keyword.get
        dry_run? = Keyword.get(opts, :dry_run, false)
        format = Keyword.get(opts, :format, "text")
      end
      ```
      
      ## Shell Output
      
      ```elixir
      # Prefer Mix.shell() for testability
      Mix.shell().info("Processing #{count} items...")
      Mix.shell().error("Failed: #{reason}")
      
      # For colored output
      Mix.shell().info([:green, "✓ ", :reset, "All checks passed"])
      
      # Progress reporting
      Enum.each(items, fn item ->
        Mix.shell().info("  #{item.name}... #{status}")
      end)
      ```
      
      ## App Startup (Iron Law #10)
      
      NEVER `Mix.Task.run("app.start")` — it boots the FULL supervision tree:
      the endpoint binds its port and Oban starts consuming jobs, inside what
      should be a one-off task. Start only what the task needs:
      
      ```elixir
      def run(args) do
        # Load config without starting the app
        Mix.Task.run("app.config")
      
        # Start only the dependency apps + repo this task uses
        {:ok, _} = Application.ensure_all_started(:ecto_sql)
        {:ok, _} = MyApp.Repo.start_link()
      
        # Your logic here
      end
      ```
      
      ## Chaining Tasks
      
      ```elixir
      # Run another task (after the startup above)
      Mix.Task.run("ecto.migrate")
      
      # Re-run a task that already ran this VM session
      Mix.Task.rerun("ecto.migrate")
      ```
      
      ## Credo Complexity
      
      Mix tasks often trigger Credo complexity warnings because the
      `run/1` function handles arg parsing + logic. Split into:
      
      ```elixir
      def run(args) do
        args |> parse_opts() |> validate_opts() |> execute()
      end
      
      defp parse_opts(args), do: ...
      defp validate_opts(opts), do: ...
      defp execute(opts), do: ...
      ```
      
      ## Testing Mix Tasks
      
      ```elixir
      defmodule Mix.Tasks.MyApp.ValidateTest do
        use ExUnit.Case, async: true
      
        test "runs successfully with valid args" do
          Mix.Tasks.MyApp.Validate.run(["--type", "full"])
        end
      
        test "handles missing args gracefully" do
          assert_raise Mix.Error, fn ->
            Mix.Tasks.MyApp.Validate.run(["--invalid"])
          end
        end
      end
      ```
      
    • otp-patterns.md 10.6 KB
      # OTP Patterns Reference
      
      > **Official docs**: <https://hexdocs.pm/elixir/GenServer.html> | <https://hexdocs.pm/elixir/Supervisor.html>
      > **Elixir guides**: <https://hexdocs.pm/elixir/introduction.html> (see OTP section)
      
      ## Contents
      
      - [Core Rule](#core-rule-no-process-without-a-runtime-reason)
      - [BEAM Architecture Context](#beam-architecture-context)
      - [Decision Tree](#decision-tree)
      - [Quick Reference Table](#quick-reference-table)
      - [Plain Functions](#plain-functions-default-choice)
      - [Agent: Simple State](#agent-simple-state)
      - [ETS](#ets-shared-state-without-serialization)
      - [GenServer](#genserver-complex-state-management)
      - [Task](#task-one-off-async-work)
      - [Supervisor](#supervisor-fault-tolerance)
      - [DynamicSupervisor](#dynamicsupervisor-on-demand-children)
      - [Registry](#registry-dynamic-process-naming)
      - [Common Scenarios](#common-scenarios)
      - [Resource Cleanup](#resource-cleanup-tryafter-not-tryrescue)
      - [Anti-Patterns](#anti-patterns)
      
      ## Core Rule: NO PROCESS WITHOUT A RUNTIME REASON
      
      Processes model **runtime properties**, not code organization:
      
      - ✓ Concurrency needs
      - ✓ Shared resources requiring serialized access
      - ✓ Error isolation domains
      - ✓ State that survives between operations
      - ✗ Code organization (MAJOR ANTI-PATTERN)
      - ✗ Stateless computation
      - ✗ Namespacing
      
      ## BEAM Architecture Context
      
      Understanding these fundamentals explains WHY patterns exist:
      
      - **Processes are cheap**: 2.6KB each, ~134M possible per VM
      - **Complete isolation**: Each has own stack/heap/mailbox
      - **Messages are copied**: Keep messages small (except binaries >64 bytes)
      - **Per-process GC**: No global GC pauses
      - **Preemptive scheduling**: Fair CPU time via reductions
      - **"Let it crash"**: Focus on happy path, supervisors restart to known-good state
      
      ## Decision Tree
      
      ```
      Need to maintain state?
      ├─ No → Use plain functions
      └─ Yes
          ├─ Simple get/update only? → Agent or ETS
          ├─ Complex message handling? → GenServer
          │   ├─ Need timeouts/monitors? → GenServer
          │   └─ Children started dynamically? → DynamicSupervisor
          └─ One-off async work? → Task
      ```
      
      ## Quick Reference Table
      
      | Need | Solution | Notes |
      |------|----------|-------|
      | Stateless computation | Functions | Default choice |
      | Simple get/set state | Agent | No monitors/timers |
      | Fast key-value lookups | ETS | Many readers, no serialization |
      | Complex state/coordination | GenServer | Monitors, timers, handle_info |
      | One-off async work | Task | Task.Supervisor for production |
      | Dynamic worker pool | DynamicSupervisor + Registry | Per-user/session processes |
      | Connection pool | GenServer | Checkout/checkin with monitors |
      | Fault tolerance | Supervisor | Always supervise! |
      
      ---
      
      ## Plain Functions (Default Choice)
      
      ```elixir
      # DO: Stateless computation
      defmodule Calculator do
        def add(a, b), do: a + b
        def multiply(a, b), do: a * b
      end
      
      # DON'T: GenServer for stateless work
      defmodule Calculator do
        use GenServer
        def add(a, b), do: GenServer.call(__MODULE__, {:add, a, b})
        def handle_call({:add, a, b}, _from, state), do: {:reply, a + b, state}
      end
      ```
      
      ## Agent: Simple State
      
      **Use when**: Only get/update operations, no monitors/timers
      
      **Don't use when**: Need handle_info, monitors, distributed system
      
      ```elixir
      defmodule Counter do
        use Agent
      
        def start_link(initial) do
          Agent.start_link(fn -> initial end, name: __MODULE__)
        end
      
        def value, do: Agent.get(__MODULE__, & &1)
        def increment, do: Agent.update(__MODULE__, &(&1 + 1))
        def reset, do: Agent.update(__MODULE__, fn _ -> 0 end)
      end
      ```
      
      ## ETS: Shared State Without Serialization
      
      **Use when**: Many concurrent readers/writers, key-value pairs, performance critical
      
      **Don't use when**: Need complex coordination, complex relationships
      
      ```elixir
      # Create table (usually in Application.start/2)
      :ets.new(:my_cache, [:named_table, :public, read_concurrency: true])
      
      # Use from anywhere
      :ets.insert(:my_cache, {:key, value})
      [{:key, value}] = :ets.lookup(:my_cache, :key)
      :ets.delete(:my_cache, :key)
      ```
      
      ## GenServer: Complex State Management
      
      **Use for**: Complex coordination, serializing access, managing external resources, monitors/timers
      
      **Don't use for**: Code organization, stateless computation, simple get/update
      
      ```elixir
      defmodule ConnectionPool do
        use GenServer
      
        # Client API
        def start_link(opts) do
          GenServer.start_link(__MODULE__, opts, name: __MODULE__)
        end
      
        def checkout, do: GenServer.call(__MODULE__, :checkout)
        def checkin(conn), do: GenServer.cast(__MODULE__, {:checkin, conn})
      
        # Server callbacks
        @impl GenServer
        def init(opts) do
          {:ok, %{available: [], checked_out: %{}, max: opts[:max] || 10}}
        end
      
        @impl GenServer
        def handle_call(:checkout, {pid, _ref}, state) do
          ref = Process.monitor(pid)  # Clean up if caller crashes
          # ... checkout logic
          {:reply, conn, updated_state}
        end
      
        @impl GenServer
        def handle_info({:DOWN, _ref, :process, pid, _reason}, state) do
          # Clean up crashed process's connections
          {:noreply, cleanup_for_pid(state, pid)}
        end
      end
      ```
      
      ### Expensive Initialization
      
      ```elixir
      # DO: Use handle_continue (OTP 21+)
      @impl GenServer
      def init(args) do
        {:ok, initial_state, {:continue, :load_data}}
      end
      
      @impl GenServer
      def handle_continue(:load_data, state) do
        # GUARANTEED to run before any messages
        {:noreply, %{state | data: load_expensive_data()}}
      end
      
      # DON'T: send(self(), :init) - not guaranteed order
      ```
      
      ## Task: One-Off Async Work
      
      ```elixir
      # Fire-and-forget (won't crash caller)
      Task.Supervisor.start_child(MyApp.TaskSupervisor, fn ->
        do_background_work()
      end)
      
      # Async with timeout and error handling
      task = Task.Supervisor.async_nolink(MyApp.TaskSupervisor, fn ->
        might_fail()
      end)
      
      case Task.yield(task, 5000) || Task.shutdown(task) do
        {:ok, result} -> result
        {:exit, reason} -> handle_error(reason)
        nil -> handle_timeout()
      end
      
      # Concurrent collection processing
      urls
      |> Task.async_stream(&fetch_url/1, max_concurrency: 10, timeout: 30_000)
      |> Enum.map(fn {:ok, result} -> result end)
      ```
      
      ## Supervisor: Fault Tolerance
      
      **Always supervise processes**. Never use `{:ok, pid} = GenServer.start_link(...)` in production.
      
      ```elixir
      children = [
        {Registry, keys: :unique, name: MyApp.Registry},
        {MyApp.CacheServer, []},
        {DynamicSupervisor, name: MyApp.WorkerSupervisor, strategy: :one_for_one},
        {Task.Supervisor, name: MyApp.TaskSupervisor}
      ]
      
      Supervisor.start_link(children,
        strategy: :one_for_one,
        max_restarts: 3,
        max_seconds: 5
      )
      ```
      
      ### Restart Strategies
      
      | Strategy | Behavior | Use When |
      |----------|----------|----------|
      | `:one_for_one` | Restart only crashed child | Children are independent |
      | `:one_for_all` | Restart all children | Children are tightly coupled |
      | `:rest_for_one` | Restart crashed + those started after | Later depend on earlier |
      
      ### Restart Types
      
      - `:permanent` - Always restart (default for most workers)
      - `:temporary` - Never restart (one-off tasks)
      - `:transient` - Restart only on abnormal exit
      
      ## DynamicSupervisor: On-Demand Children
      
      ```elixir
      # Combined with Registry for named access
      def start_worker(user_id) do
        name = {:via, Registry, {MyApp.WorkerRegistry, user_id}}
        spec = {Worker, name: name, user_id: user_id}
        DynamicSupervisor.start_child(MyApp.WorkerSupervisor, spec)
      end
      
      def find_worker(user_id) do
        case Registry.lookup(MyApp.WorkerRegistry, user_id) do
          [{pid, _}] -> {:ok, pid}
          [] -> {:error, :not_found}
        end
      end
      ```
      
      ## Registry: Dynamic Process Naming
      
      ```elixir
      # Unique registration (no atom explosion)
      name = {:via, Registry, {MyApp.Registry, "user:#{user_id}"}}
      GenServer.start_link(UserWorker, args, name: name)
      
      # Lookup
      case Registry.lookup(MyApp.Registry, "user:#{user_id}") do
        [{pid, _}] -> {:ok, pid}
        [] -> {:error, :not_found}
      end
      
      # Pub/sub broadcast (duplicate keys registry)
      Registry.dispatch(PubSubRegistry, "topic", fn entries ->
        for {pid, _} <- entries, do: send(pid, {:broadcast, message})
      end)
      ```
      
      ---
      
      ## Common Scenarios
      
      ### Cache
      
      **Pattern**: ETS (not GenServer)
      **Why**: Many concurrent readers/writers, no coordination needed
      
      ### Rate Limiting
      
      **Pattern**: ETS + optional GenServer for cleanup
      **Why**: ETS for fast lookups, GenServer only if need periodic cleanup
      
      ### Background Job
      
      **Pattern**: Task.Supervisor (or Oban for persistence)
      **Why**: Don't need long-lived state, just async execution
      
      ### Connection Pool
      
      **Pattern**: GenServer
      **Why**: Need serialization, monitors, complex coordination
      
      ### User Session State
      
      **Pattern**: DynamicSupervisor + Registry + GenServer
      **Why**: Dynamic per-user processes, fault isolation, named access
      
      ---
      
      ## Resource Cleanup: try/after (Not try/rescue)
      
      When wrapping code in instrumentation spans, telemetry, or any
      resource that needs guaranteed cleanup:
      
      ```elixir
      # CORRECT: try/after — runs ALWAYS
      def instrumented_call(args) do
        span = Tracer.start_span("operation")
        try do
          do_work(args)
        after
          Tracer.finish_span(span)
        end
      end
      
      # WRONG: try/rescue — only runs on exception
      def instrumented_call(args) do
        span = Tracer.start_span("operation")
        try do
          do_work(args)
        rescue
          e -> Tracer.finish_span(span); reraise e, __STACKTRACE__
        end
        Tracer.finish_span(span)  # Missed if do_work throws non-exception exit
      end
      ```
      
      **Rule**: `try/rescue` only catches exceptions. `try/after`
      runs unconditionally — use it for spans, file handles, locks,
      and any cleanup that must happen regardless of outcome.
      
      ## Anti-Patterns
      
      ### 1. GenServer for Code Organization
      
      ```elixir
      # ANTI-PATTERN
      defmodule Calculator do
        use GenServer
        def add(a, b), do: GenServer.call(__MODULE__, {:add, a, b})
      end
      
      # CORRECT
      defmodule Calculator do
        def add(a, b), do: a + b
      end
      ```
      
      ### 2. Scattered Process Interfaces
      
      ```elixir
      # ANTI-PATTERN: Calling Agent from multiple modules
      Agent.get(MyApp.Cache, &Map.get(&1, key))  # In various modules
      
      # CORRECT: Encapsulate in single module
      defmodule MyApp.Cache do
        use Agent
        def get(key), do: Agent.get(__MODULE__, &Map.get(&1, key))
      end
      ```
      
      ### 3. Sending Unnecessary Data in Closures
      
      ```elixir
      # ANTI-PATTERN: Captures entire struct
      spawn(fn -> log_ip(conn.remote_ip) end)  # Copies entire conn!
      
      # CORRECT: Extract before spawning
      ip = conn.remote_ip
      spawn(fn -> log_ip(ip) end)
      ```
      
      ### 4. Unsupervised Processes
      
      ```elixir
      # ANTI-PATTERN
      {:ok, pid} = GenServer.start_link(MyServer, [])
      
      # CORRECT
      children = [{MyServer, []}]
      Supervisor.start_link(children, strategy: :one_for_one)
      ```
      
      ### 5. Global Singleton Bottleneck
      
      ```elixir
      # ANTI-PATTERN: Global singleton for per-user data
      GenServer.call(UserStateServer, {:get_user, user_id})
      
      # CORRECT: DynamicSupervisor + Registry for per-user processes
      name = {:via, Registry, {MyApp.Registry, "user:#{user_id}"}}
      ```
      
    • pattern-matching.md 1.9 KB
      # Pattern Matching Reference
      
      ## Function Heads (Preferred)
      
      ```elixir
      def process(%{status: :active} = user), do: activate(user)
      def process(%{status: :inactive} = user), do: deactivate(user)
      def process(_user), do: {:error, :unknown_status}
      ```
      
      ## Pin Operator
      
      Match against existing values:
      
      ```elixir
      expected = :ok
      ^expected = get_result()  # Only matches if returns :ok
      
      # Essential for dynamic map keys
      key = :user_id
      %{^key => value} = data
      ```
      
      ## is_non_struct_map/1 Guard (Elixir 1.17+)
      
      Structs ARE maps, so `is_map/1` matches both:
      
      ```elixir
      # DO: Distinguish plain maps from structs
      def process(data) when is_non_struct_map(data), do: handle_map(data)
      def process(%User{} = user), do: handle_user(user)
      
      # DON'T: This matches structs too!
      def process(%{} = data), do: handle_data(data)  # Matches User struct!
      ```
      
      ## Custom Guards
      
      ```elixir
      defguard is_positive_integer(n) when is_integer(n) and n > 0
      
      def process(n) when is_positive_integer(n), do: n * 2
      ```
      
      ## Binary Pattern Matching
      
      ```elixir
      # UTF-8 prefix matching
      "hello " <> rest = "hello world"  # rest = "world"
      
      # Protocol parsing
      <<header::8, length::32-big, payload::binary-size(length), rest::binary>> = data
      
      # ANTI-PATTERN: Small sub-binary keeps large parent alive
      <<small::binary-size(100), _::binary>> = one_gb_binary
      # DO: Copy if keeping only the small part
      small = :binary.copy(small)
      ```
      
      ## Guards: Allowed Operations
      
      Guards must be pure, deterministic:
      
      - Type checks: `is_atom/1`, `is_binary/1`, `is_integer/1`, `is_list/1`, `is_map/1`
      - Comparisons: `==`, `!=`, `<`, `>`, `<=`, `>=`
      - Arithmetic: `+`, `-`, `*`, `/`, `abs/1`, `div/2`, `rem/2`
      - Value access: `hd/1`, `tl/1`, `elem/2`, `tuple_size/1`, `map_size/1`, `length/1`
      
      Guards use `and`/`or`/`not`, never short-circuit operators (they require boolean operands)
      
      ```elixir
      # CORRECT
      def process(n) when is_integer(n) and n > 0, do: n * 2
      
      # WRONG - compile error
      def process(n) when is_integer(n) && n > 0, do: n * 2
      ```
      
    • troubleshooting.md 4.5 KB
      # BEAM Troubleshooting Playbook
      
      Production debugging for Phoenix/BEAM applications. For code bugs, see `deep-bug-investigator` agent.
      
      ## Quick Diagnosis
      
      | Symptom | Likely Cause | Section |
      |---------|--------------|---------|
      | High memory, growing | Process leak, ETS bloat | Memory Issues |
      | Slow responses | N+1, GenServer bottleneck | Performance |
      | Random crashes | Unhandled errors, supervisor | Crashes |
      | Timeouts | DB pool, GenServer call | Timeouts |
      | Node unresponsive | Scheduler block, long GC | BEAM Issues |
      
      ## Memory Issues
      
      ### With Tidewave
      
      ```elixir
      # Top memory processes
      mcp__tidewave__project_eval """
      Process.list()
      |> Enum.map(fn pid -> {pid, Process.info(pid, :memory)} end)
      |> Enum.filter(fn {_, mem} -> mem != nil end)
      |> Enum.map(fn {pid, {:memory, mem}} -> {pid, mem} end)
      |> Enum.sort_by(&elem(&1, 1), :desc)
      |> Enum.take(10)
      |> Enum.map(fn {pid, mem} ->
        info = Process.info(pid, [:registered_name, :current_function])
        {info[:registered_name] || pid, div(mem, 1024), info[:current_function]}
      end)
      """
      
      # ETS table sizes
      mcp__tidewave__project_eval """
      :ets.all()
      |> Enum.map(fn t -> {t, :ets.info(t, :memory) * :erlang.system_info(:wordsize)} end)
      |> Enum.sort_by(&elem(&1, 1), :desc)
      |> Enum.take(10)
      |> Enum.map(fn {t, mem} -> {t, div(mem, 1024)} end)
      """
      ```
      
      ### Without Tidewave
      
      ```bash
      # Attach to running node
      iex --sname debug --remsh myapp@hostname
      
      # In IEx
      :observer.start()
      ```
      
      ### Common Causes
      
      1. **LiveView socket assigns** - Use `temporary_assigns` or streams
      2. **ETS table growth** - Check for missing cleanup
      3. **Process mailbox** - GenServer not keeping up
      4. **Binary memory** - Large binaries not GC'd (use `:binary.copy/1`)
      
      ## Performance Issues
      
      ### N+1 Query Detection
      
      ```elixir
      # config/dev.exs - log slow queries
      config :my_app, MyApp.Repo,
        log: :debug,
        stacktrace: true
      ```
      
      ### GenServer Bottleneck
      
      ```elixir
      mcp__tidewave__project_eval """
      pid = Process.whereis(MyApp.SomeServer)
      {:message_queue_len, len} = Process.info(pid, :message_queue_len)
      IO.puts("Queue length: \#{len}")  # >100 = problem
      """
      ```
      
      **Signs:**
      
      - Message queue growing
      - Call timeouts
      - Single process high CPU
      
      **Solutions:**
      
      - Pool of workers (Poolboy)
      - ETS for read-heavy (GenServer writes, ETS reads)
      - Partition by key (Registry + DynamicSupervisor)
      
      ### Slow Ecto Queries
      
      ```sql
      -- With Tidewave
      mcp__tidewave__execute_sql_query """
      SELECT query, calls, mean_time, total_time
      FROM pg_stat_statements
      ORDER BY mean_time DESC
      LIMIT 10
      """
      
      -- Missing indexes
      mcp__tidewave__execute_sql_query """
      SELECT relname, seq_scan, idx_scan
      FROM pg_stat_user_tables
      WHERE seq_scan > idx_scan
      ORDER BY seq_scan DESC
      """
      ```
      
      ## Crashes
      
      ### Common Crash Patterns
      
      | Error | Cause | Fix |
      |-------|-------|-----|
      | `noproc` | Process died/not started | Check supervisor, handle `{:error, _}` |
      | `timeout` | GenServer.call timeout | Increase timeout or use cast |
      | `badmatch` | Pattern match failed | Add catch-all clause |
      | `function_clause` | No matching function | Check guards, add fallback |
      | `badarg` | Wrong argument type | Validate input |
      
      ### Supervisor Tree Analysis
      
      ```elixir
      mcp__tidewave__project_eval """
      Supervisor.which_children(MyApp.Supervisor)
      |> Enum.map(fn {id, pid, type, _} ->
        %{id: id, pid: pid, type: type}
      end)
      """
      ```
      
      ## Timeouts
      
      ### DB Pool Exhaustion
      
      **Symptoms:** Random timeouts, "connection not available"
      
      **Check:**
      
      ```elixir
      # Current pool size
      Application.get_env(:my_app, MyApp.Repo)[:pool_size]
      ```
      
      **Solutions:**
      
      - Increase `pool_size`
      - Add `queue_target` and `queue_interval`
      - Find long-running queries
      - Use `Repo.checkout/1` properly for transactions
      
      ### GenServer Call Timeout
      
      ```elixir
      # Default is 5000ms
      GenServer.call(pid, :request, 10_000)  # Increase if needed
      
      # Or use cast + handle_info for async
      GenServer.cast(pid, {:request, self()})
      ```
      
      ## BEAM Issues
      
      ### Scheduler Utilization
      
      ```elixir
      mcp__tidewave__project_eval """
      :scheduler.utilization(1000)  # Sample for 1 second
      """
      ```
      
      ### Long GC Pauses
      
      ```elixir
      # Enable GC logging
      :erlang.system_flag(:long_gc, 50)  # Log GC > 50ms
      ```
      
      ### Dirty Schedulers Blocked
      
      ```elixir
      mcp__tidewave__project_eval """
      :erlang.statistics(:dirty_cpu_run_queue_lengths)
      """
      ```
      
      ## Quick Checklist
      
      ```markdown
      ## Troubleshooting: [Issue]
      
      ### Symptoms
      - [ ] High memory?
      - [ ] Slow responses?
      - [ ] Timeouts?
      - [ ] Crashes?
      
      ### Checked
      - [ ] Logs: `mcp__tidewave__get_logs level: :error`
      - [ ] Memory: Top processes
      - [ ] Queries: Slow query log
      - [ ] Processes: GenServer queue lengths
      
      ### Root Cause
      [...]
      
      ### Fix
      [...]
      ```
      
    • with-and-pipes.md 7.2 KB
      # With Statement and Pipe Operator Guide
      
      ## Contents
      
      - [Why These Are Idiomatic](#why-these-are-idiomatic)
      - [Pipe Operator](#pipe-operator-)
      - [With Statement](#with-statement)
      - [Real-World Examples from Production Code](#real-world-examples-from-production-code)
      - [Summary](#summary)
      - [Anti-Pattern: Avoiding Pipes and With](#anti-pattern-avoiding-pipes-and-with)
      
      ## Why These Are Idiomatic
      
      Both `with` and `|>` are core Elixir idioms that experienced developers expect. Avoiding them leads to less readable, non-idiomatic code.
      
      ## Pipe Operator `|>`
      
      ### When to Use Pipes
      
      ```elixir
      # ✅ Data transformation chains
      params
      |> Map.get("user")
      |> normalize_email()
      |> String.downcase()
      |> String.trim()
      
      # ✅ Query building
      User
      |> where(active: true)
      |> where([u], u.role in ^roles)
      |> order_by(desc: :inserted_at)
      |> limit(10)
      |> Repo.all()
      
      # ✅ Changeset chains
      %User{}
      |> User.changeset(params)
      |> put_change(:status, :pending)
      |> validate_required([:email])
      |> Repo.insert()
      
      # ✅ Stream processing
      1..1000
      |> Stream.map(&expensive_operation/1)
      |> Stream.filter(&valid?/1)
      |> Enum.take(10)
      ```
      
      ### When NOT to Use Pipes
      
      ```elixir
      # ❌ Single function call - no pipe needed
      name |> String.upcase()
      # ✅ Just call the function
      String.upcase(name)
      
      # ❌ When data isn't the first argument
      Enum.map(list, &String.upcase/1) |> Enum.join(", ")
      # ✅ Use variable or reorder
      list
      |> Enum.map(&String.upcase/1)
      |> Enum.join(", ")
      
      # ❌ Complex branching mid-pipe
      data
      |> transform()
      |> (fn x -> if condition, do: a(x), else: b(x) end).()  # Ugly!
      # ✅ Use case or separate function
      data
      |> transform()
      |> handle_condition(condition)
      
      defp handle_condition(x, true), do: a(x)
      defp handle_condition(x, false), do: b(x)
      ```
      
      ### Pipe Style Rules
      
      ```elixir
      # Start with data, not a function call
      # ❌ Wrong
      get_user(id)
      |> process()
      
      # ✅ Right
      id
      |> get_user()
      |> process()
      
      # Or just use variables for clarity
      user = get_user(id)
      process(user)
      ```
      
      ## With Statement
      
      ### When to Use With
      
      ```elixir
      # ✅ Multiple dependent operations that can fail
      def create_order(params) do
        with {:ok, user} <- get_user(params.user_id),
             {:ok, product} <- get_product(params.product_id),
             :ok <- check_inventory(product),
             {:ok, order} <- Orders.create(user, product, params) do
          {:ok, order}
        end
      end
      
      # ✅ Authorization chains
      def update_post(user, post_id, params) do
        with {:ok, post} <- get_post(post_id),
             :ok <- authorize(user, :update, post),
             {:ok, updated} <- Posts.update(post, params) do
          {:ok, updated}
        end
      end
      
      # ✅ Validations that depend on each other
      def process_upload(params) do
        with {:ok, file} <- validate_file_exists(params),
             {:ok, metadata} <- extract_metadata(file),
             :ok <- validate_file_type(metadata),
             {:ok, processed} <- process_file(file, metadata) do
          {:ok, processed}
        end
      end
      ```
      
      ### When NOT to Use With
      
      ```elixir
      # ❌ Single operation - use case instead
      with {:ok, user} <- get_user(id) do
        {:ok, user}
      end
      
      # ✅ Just use case
      case get_user(id) do
        {:ok, user} -> {:ok, user}
        {:error, _} = error -> error
      end
      
      # ❌ No failure handling needed - use pipes
      with data <- fetch_data(),
           processed <- process(data),
           formatted <- format(processed) do
        formatted
      end
      
      # ✅ Use pipes
      fetch_data()
      |> process()
      |> format()
      ```
      
      ### With Patterns
      
      ```elixir
      # Pattern match in with clauses
      with %User{active: true} = user <- get_user(id),
           %Subscription{status: :active} <- get_subscription(user) do
        {:ok, user}
      else
        %User{active: false} -> {:error, :user_inactive}
        %Subscription{} -> {:error, :subscription_inactive}
        nil -> {:error, :not_found}
      end
      
      # Bare expressions (always match)
      with {:ok, user} <- get_user(id),
           # This always succeeds, just assigns
           email = user.email,
           {:ok, _} <- send_notification(email) do
        :ok
      end
      ```
      
      ### Handling Else Clauses
      
      **Official anti-pattern**: Complex `else` in `with` — keep else to 1-2 clauses max. Normalize errors in helpers instead.
      
      ```elixir
      # ✅ Simple: let non-matches pass through
      def create_user(params) do
        with {:ok, validated} <- validate(params),
             {:ok, user} <- Repo.insert(validated) do
          {:ok, user}
        end
        # {:error, changeset} passes through unchanged
      end
      
      # ✅ When you need to transform errors
      def create_user(params) do
        with {:ok, validated} <- validate(params),
             {:ok, user} <- Repo.insert(validated) do
          {:ok, user}
        else
          {:error, %Ecto.Changeset{} = changeset} ->
            {:error, :validation_failed, changeset}
          {:error, reason} ->
            {:error, :creation_failed, reason}
        end
      end
      
      # ❌ Avoid complex else clauses - normalize in helpers
      # Instead of many else branches, make helpers return consistent errors
      defp validate(params) do
        case Validator.validate(params) do
          :ok -> {:ok, params}
          {:error, reasons} -> {:error, {:validation, reasons}}
        end
      end
      ```
      
      ## Real-World Examples from Production Code
      
      ### Context Function with Authorization
      
      ```elixir
      def update_deal(broker, deal_id, params) do
        with {:ok, deal} <- get_deal(deal_id),
             :ok <- Bodyguard.permit(Deal, :update_deal, broker, deal),
             {:ok, updated} <- do_update_deal(deal, params) do
          broadcast_update(updated)
          {:ok, updated}
        end
      end
      ```
      
      ### Query Building
      
      ```elixir
      def list_articles(profile_id, opts \\ []) do
        Article
        |> where(profile_id: ^profile_id)
        |> filter_by_search(opts[:search])
        |> filter_by_status(opts[:status])
        |> filter_by_rating(opts[:rating])
        |> apply_sort(opts[:sort_by], opts[:sort_order])
        |> maybe_limit(opts[:limit])
        |> Repo.all()
      end
      ```
      
      ### Changeset Operations
      
      ```elixir
      def confirm_guest(guest, params) do
        guest
        |> Guest.confirm_changeset(params)
        |> maybe_update_attendees(params)
        |> Repo.update()
      end
      ```
      
      ### Multi-Step Billing Check
      
      ```elixir
      def check_feature_access(user, feature) do
        with true <- billing_enforced?(),
             {:ok, subscription} <- get_active_subscription(user),
             true <- feature_included?(subscription, feature) do
          :ok
        else
          false -> {:error, :feature_not_available}
          {:error, :no_subscription} -> {:error, :subscription_required}
        end
      end
      ```
      
      ## Summary
      
      | Pattern | Use When | Avoid When |
      |---------|----------|------------|
      | `\|>` pipe | Data transformation, query building, changesets | Single calls, complex branching |
      | `with` | Multiple dependent fallible operations | Single operation, no failures possible |
      | `case` | Pattern matching single value | Chaining multiple operations |
      | Nested `case` | Never | Always - use `with` instead |
      
      ## Anti-Pattern: Avoiding Pipes and With
      
      ```elixir
      # ❌ This is NOT more readable
      result1 = function1(data)
      result2 = function2(result1)
      result3 = function3(result2)
      final = function4(result3)
      
      # ✅ Pipes are clearer for transformations
      final =
        data
        |> function1()
        |> function2()
        |> function3()
        |> function4()
      
      # ❌ Nested case is hard to follow
      case get_user(id) do
        {:ok, user} ->
          case get_subscription(user) do
            {:ok, sub} ->
              case check_feature(sub, feature) do
                :ok -> {:ok, user}
                error -> error
              end
            error -> error
          end
        error -> error
      end
      
      # ✅ With flattens the happy path
      with {:ok, user} <- get_user(id),
           {:ok, sub} <- get_subscription(user),
           :ok <- check_feature(sub, feature) do
        {:ok, user}
      end
      ```
      
  • SKILL.md 4.6 KB
    ---
    name: elixir-idioms
    description: OTP/BEAM patterns and Elixir idioms — GenServer, Supervisor, Task, Registry,
      pattern matching, with chains, pipes. Use when designing processes or debugging
      BEAM issues.
    ---
    
    # Elixir Idioms
    
    Reference for writing idiomatic Elixir code with BEAM-aware patterns.
    
    ## Iron Laws — Never Violate These
    
    1. **NO PROCESS WITHOUT A RUNTIME REASON** — Processes model concurrency, state, isolation—NOT code structure
    2. **MESSAGES ARE COPIED** — Keep messages small (except binaries >64 bytes)
    3. **GUARDS USE `and`/`or`/`not`** — Never use short-circuit operators in guards (guards require boolean operands)
    4. **CHANGESETS FOR EXTERNAL DATA** — Use `cast/4` for user input, `change/2` for internal
    5. **RESCUE ONLY FOR EXTERNAL CODE** — Never use rescue for control flow
    6. **NO DYNAMIC ATOM CREATION** — `String.to_atom(user_input)` causes memory leak (atoms aren't GC'd)
    7. **@external_resource FOR COMPILE-TIME FILES** — Modules reading files at compile time MUST declare `@external_resource`
    8. **SUPERVISE ALL LONG-LIVED PROCESSES** — Never bare `GenServer.start_link`/`Agent.start_link` in production. Use supervision trees
    9. **WRAP THIRD-PARTY LIBRARY APIs** — Always facade external deps behind a project-owned module. Enables swapping without touching callers
    10. **MIX TASKS START ONLY WHAT THEY NEED** — `Mix.Task.run("app.config")` + `Application.ensure_all_started/1`, never `Mix.Task.run("app.start")` (boots the FULL tree: endpoint port, Oban consuming)
    11. **CAPTURE LOCALE BEFORE SPAWNING** — Gettext/CLDR locale is process-local. Read it in the caller and pass explicitly; a spawned Task/GenServer starts with the default locale
    
    ## BEAM Architecture (Why Elixir Works This Way)
    
    - **Processes are cheap (2.6KB)** — Spawn liberally for concurrency/isolation
    - **Complete memory isolation** — No shared state, no locks needed
    - **Messages are copied** (except binaries >64 bytes) — Keep messages small
    - **Per-process GC** — No global GC pauses
    - **"Let it crash"** — Supervisors restart to known-good state
    
    ## Core Principles
    
    1. **Pattern match over conditionals** — Function heads first, then `case`, then `cond`
    2. **Tagged tuples for expected failures** — `{:ok, _}`/`{:error, _}` for expected errors, raise for bugs
    3. **Pipe operator for data transformation** — Start with data, never pipe single calls
    4. **Let it crash** — Handle expected errors, crash on unexpected ones
    5. **Explicit over implicit** — Be clear about intentions
    
    ## Quick Decision Trees
    
    ### Control Flow
    
    ```
    Need patterns? → case (or function heads)
    Multiple operations? → with
    Boolean conditions? → cond (multiple) or if (single)
    ```
    
    ### Error Handling
    
    ```
    Expected failure? → {:ok, _}/{:error, _} tuples
    Unexpected/bug? → raise exception (let supervisor handle)
    External library? → rescue (only here!)
    ```
    
    ### OTP
    
    ```
    Need state?
    ├─ No → Plain functions
    ├─ Simple get/update → Agent or ETS
    ├─ Complex messages/timeouts → GenServer
    └─ One-off async → Task
    ```
    
    ## Quick Patterns
    
    ```elixir
    # Pattern match in function head
    def process(%{status: :active} = user), do: activate(user)
    def process(%{status: :inactive} = user), do: deactivate(user)
    
    # with for happy path
    with {:ok, user} <- get_user(id),
         {:ok, order} <- create_order(user) do
      {:ok, order}
    end
    
    # Task for async
    Task.Supervisor.async_nolink(TaskSup, fn -> work() end)
    |> Task.yield(5000) || Task.shutdown(task)
    ```
    
    ## Common Pitfalls
    
    | Wrong | Right |
    |-------|-------|
    | `length(list) == 0` | `list == []` or `Enum.empty?(list)` |
    | `list ++ [item]` | `[item \| list] \|> Enum.reverse()` |
    | `String.to_atom(input)` | `String.to_existing_atom(input)` |
    | `spawn(fn -> log(conn) end)` | `ip = conn.ip; spawn(fn -> log(ip) end)` |
    | `unless condition` | `if !condition` (unless deprecated in 1.18) |
    
    ## References
    
    For detailed patterns, see:
    
    - `references/pattern-matching.md` - Pattern matching, guards, binary matching
    - `references/otp-patterns.md` - GenServer, Supervisor, Task, Registry
    - `references/error-handling.md` - Tagged tuples, rescue, with
    - `references/with-and-pipes.md` - When to use `with` and `|>` (idiomatic patterns)
    - `references/troubleshooting.md` - Production BEAM debugging (memory, performance, crashes)
    - `references/anti-patterns.md` - Common mistakes and fixes
    - `references/mix-tasks.md` - Mix task naming, option parsing, shell output
    - `references/elixir-118-features.md` - Duration module, dbg improvements (1.18+)
    - `references/elixir-120-type-system.md` - Gradual type checker, `dynamic()`, verified bugs as compile warnings (1.20+, OTP 27+)
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related