Agent skill

elixir-thinking

This skill should be used when the user asks to "implement a feature in Elixir", "refactor this module", "should I use a GenServer here?", "how should I structure this?", "use the pipe operator", "add error handling", "make this concurrent", or mentions protocols, behaviours, pattern matching, with statements, comprehensions, structs, or coming from an OOP background. Contains paradigm-shifting insights.

Stars 138
Forks 12

Install this agent skill to your Project

npx add-skill https://github.com/georgeguimaraes/claude-code-elixir/tree/main/plugins/elixir/skills/elixir-thinking

SKILL.md

Elixir Thinking

Mental shifts required before writing Elixir. These contradict conventional OOP patterns.

The Iron Law

NO PROCESS WITHOUT A RUNTIME REASON

Before creating a GenServer, Agent, or any process, answer YES to at least one:

  1. Do I need mutable state persisting across calls?
  2. Do I need concurrent execution?
  3. Do I need fault isolation?

All three are NO? Use plain functions. Modules organize code; processes manage runtime.

The Three Decoupled Dimensions

OOP couples behavior, state, and mutability together. Elixir decouples them:

OOP Dimension Elixir Equivalent
Behavior Modules (functions)
State Data (structs, maps)
Mutability Processes (GenServer)

Pick only what you need. "I only need data and functions" = no process needed.

"Let It Crash" = "Let It Heal"

The misconception: Write careless code. The truth: Supervisors START processes.

  • Handle expected errors explicitly ({:ok, _} / {:error, _})
  • Let unexpected errors crash → supervisor restarts

Control Flow

Pattern matching first:

  • Match on function heads instead of if/else or case in bodies
  • %{} matches ANY map—use map_size(map) == 0 guard for empty maps
  • Avoid nested case—refactor to single case, with, or separate functions

Error handling:

  • Use {:ok, result} / {:error, reason} for operations that can fail
  • Avoid raising exceptions for control flow
  • Use with for chaining {:ok, _} / {:error, _} operations

Be explicit about expected cases:

  • Avoid _ -> nil catch-alls—they silently swallow unexpected cases
  • Avoid value && value.field nil-punning—obscures actual return types
  • When a case has {:ok, nil} -> nil alongside {:ok, value} -> value.field, use with instead:
elixir
# Verbose
case get_run(id) do
  {:ok, nil} -> nil
  {:ok, run} -> run.recommendations
end

# Prefer
with {:ok, %{recommendations: recs}} <- get_run(id), do: recs

Polymorphism

For Polymorphism Over... Use Contract
Modules Behaviors Upfront callbacks
Data Protocols Upfront implementations
Processes Message passing Implicit (send/receive)

Behaviors = default for module polymorphism (very cheap at runtime) Protocols = only when composing data types, especially built-ins Message passing = only when stateful by design (IO, file handles)

Use the simplest abstraction: pattern matching → anonymous functions → behaviors → protocols → message passing. Each step adds complexity.

When justified: Library extensibility, multiple implementations, test swapping. When to stay coupled: Internal module, single implementation, pattern matching handles all cases.

Data Modeling Replaces Class Hierarchies

OOP: Complex class hierarchy + visitor pattern. Elixir: Model as data + pattern matching + recursion.

elixir
{:sequence, {:literal, "rain"}, {:repeat, {:alternation, "dogs", "cats"}}}

def interpret({:literal, text}, input), do: ...
def interpret({:sequence, left, right}, input), do: ...
def interpret({:repeat, pattern}, input), do: ...

Defaults and Options

Use /3 variants (Keyword.get/3, Map.get/3) instead of case statements branching on nil:

elixir
# WRONG
case Keyword.get(opts, :chunker) do
  nil -> chunker()
  config -> parse_chunker_config(config)
end

# RIGHT
Keyword.get(opts, :chunker, :default) |> parse_chunker_config()

Don't create helper functions to merge config defaults. Inline the fallback:

elixir
# WRONG
defp merge_defaults(opts), do: Keyword.merge([repo: Application.get_env(:app, :repo)], opts)

# RIGHT
def some_function(opts) do
  repo = opts[:repo] || Application.get_env(:app, :repo)
end

Idioms

  • Process dictionary is typically unidiomatic—pass state explicitly
  • Reserve is_thing names for guards only
  • Use structs over maps when shape is known: defstruct [:name, :age]
  • Prepend to lists [new | list] not list ++ [new]
  • Use dbg/1 for debugging—prints formatted value with context
  • Use built-in JSON module (Elixir 1.18+) instead of Jason

Testing

Always prefix mix commands with unbuffer to get ANSI colors and prevent stdout block-buffering in non-TTY environments (e.g. unbuffer mix test). Install: brew install expect (macOS) or apt install expect (Linux).

Prefer pattern matching over imperative assertions. Never use assert length + Enum.at/List.last/hd. Pattern match checks length and content in one shot:

elixir
# Bad
assert length(students) == 2
assert Enum.at(students, 0).name == "Alice"
assert Enum.at(students, 1).name == "Bob"

# Good
assert [%{name: "Alice"}, %{name: "Bob"}] = students

Test behavior, not implementation. Test use cases / public API. Refactoring shouldn't break tests.

Test your code, not the framework. If deleting your code doesn't fail the test, it's tautological.

Keep tests async. async: false means you've coupled to global state. Fix the coupling:

Problem Solution
Application.put_env Pass config as function argument
Feature flags Inject via process dictionary or context
ETS tables Create per-test tables with unique names
External APIs Use Mox with explicit allowances
File system operations Use @tag :tmp_dir (see below)

Use tmp_dir for file tests. ExUnit creates unique temp directories per test, async-safe:

elixir
@tag :tmp_dir
test "writes file", %{tmp_dir: tmp_dir} do
  path = Path.join(tmp_dir, "test.txt")
  File.write!(path, "content")
  assert File.read!(path) == "content"
end

Directory is auto-cleaned before each run. Works with @moduletag :tmp_dir for all tests in module.

Common Rationalizations

Excuse Reality
"I need a process to organize this code" Modules organize code. Processes are for runtime.
"GenServer is the Elixir way" Plain functions are also the Elixir way.
"I'll need state eventually" YAGNI. Add process when you need it.
"It's just a simple wrapper process" Simple wrappers become bottlenecks.
"This is how I'd structure it in OOP" Rethink from data flow.

Red Flags - STOP and Reconsider

  • Creating process without answering the three questions
  • Using GenServer for stateless operations
  • Wrapping a library in a process "for safety"
  • One process per entity without runtime justification
  • Reaching for protocols when pattern matching works

Any of these? Re-read The Iron Law.

Expand your agent's capabilities with these related and highly-rated skills.

georgeguimaraes/claude-code-elixir

otp-thinking

This skill should be used when the user asks to "add background processing", "cache this data", "run this async", "handle concurrent requests", "manage state across requests", "process jobs from a queue", "this GenServer is slow", or mentions GenServer, Supervisor, Agent, Task, Registry, DynamicSupervisor, handle_call, handle_cast, supervision trees, fault tolerance, "let it crash", or choosing between Broadway and Oban.

138 12
Explore
georgeguimaraes/claude-code-elixir

oban-thinking

This skill should be used when the user asks to "add a background job", "process async", "schedule a task", "retry failed jobs", "add email sending", "run this later", "add a cron job", "unique jobs", "batch process", or mentions Oban, Oban Pro, workflows, job queues, cascades, grafting, recorded values, job args, or troubleshooting job failures.

138 12
Explore
georgeguimaraes/claude-code-elixir

ecto-thinking

This skill should be used when the user asks to "add a database table", "create a new context", "query the database", "add a field to a schema", "validate form input", "fix N+1 queries", "preload this association", "separate these concerns", or mentions Repo, changesets, migrations, Ecto.Multi, has_many, belongs_to, transactions, query composition, or how contexts should talk to each other.

138 12
Explore
georgeguimaraes/claude-code-elixir

phoenix-thinking

This skill should be used when the user asks to "add a LiveView page", "create a form", "handle real-time updates", "broadcast changes to users", "add a new route", "create an API endpoint", "fix this LiveView bug", "why is mount called twice?", or mentions handle_event, handle_info, handle_params, mount, channels, controllers, components, assigns, sockets, or PubSub. Essential for avoiding duplicate queries in mount.

138 12
Explore
georgeguimaraes/claude-code-elixir

using-elixir-skills

This skill should be used when the user works on any .ex or .exs file, mentions Elixir/Phoenix/Ecto/OTP, the project has a mix.exs, or asks "which skill should I use", "new to Elixir", "help with Elixir". Routes to the correct thinking skill BEFORE exploring code. Triggers on "implement", "add", "fix", "refactor" in Elixir projects.

138 12
Explore
davila7/claude-code-templates

verl-rl-training

Provides guidance for training LLMs with reinforcement learning using verl (Volcano Engine RL). Use when implementing RLHF, GRPO, PPO, or other RL algorithms for LLM post-training at scale with flexible infrastructure backends.

23,776 2,298
Explore

Didn't find tool you were looking for?

Be as detailed as possible for better results