elixir-expert

elixir general engineering rule

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "elixir-expert" with this command: npx skills add oimiragieo/agent-studio/oimiragieo-agent-studio-elixir-expert

Elixir Expert

elixir general engineering rule

When reviewing or writing code, apply these guidelines:

  • Act as an expert senior Elixir engineer.

  • When writing code, use Elixir, Phoenix, Docker, PostgreSQL, Tailwind CSS, LeftHook, Sobelow, Credo, Ecto, ExUnit, Plug, Phoenix LiveView, Phoenix LiveDashboard, Gettext, Jason, Swoosh, Finch, DNS Cluster, File System Watcher, Release Please and ExCoveralls.

Elixir Language Patterns

Pattern Matching

Pattern matching is fundamental to Elixir. Use it for:

Function clauses:

def greet(%User{name: name, role: :admin}), do: "Hello Admin #{name}" def greet(%User{name: name}), do: "Hello #{name}" def greet(_), do: "Hello stranger"

Case statements:

case {status, data} do {:ok, %{id: id} } when id > 0 -> process(id) {:error, reason} -> handle_error(reason) _ -> :unknown end

With statements for chaining:

with {:ok, user} <- fetch_user(id), {:ok, profile} <- fetch_profile(user), {:ok, settings} <- fetch_settings(profile) do {:ok, %{user: user, profile: profile, settings: settings} } end

Guards

Use guards to add constraints to pattern matching:

def categorize(n) when is_integer(n) and n > 0, do: :positive def categorize(n) when is_integer(n) and n < 0, do: :negative def categorize(n) when is_integer(n), do: :zero

def process_map(map) when map_size(map) == 0, do: :empty def process_map(map) when is_map(map), do: :has_data

Pipe Operator

The pipe operator |> improves readability:

Instead of nested calls

result = String.trim(String.downcase(String.reverse(input)))

Use pipes

result = input |> String.reverse() |> String.downcase() |> String.trim()

Pipe into case for handling results:

user_id |> fetch_user() |> case do {:ok, user} -> process_user(user) {:error, :not_found} -> create_user() {:error, reason} -> {:error, reason} end

OTP (Open Telecom Platform) Patterns

GenServer

GenServer is the foundation for stateful processes:

defmodule Counter do use GenServer

Client API

def start_link(initial_value) do GenServer.start_link(MODULE, initial_value, name: MODULE) end

def increment do GenServer.cast(MODULE, :increment) end

def get do GenServer.call(MODULE, :get) end

Server Callbacks

@impl true def init(initial_value) do {:ok, initial_value} end

@impl true def handle_cast(:increment, state) do {:noreply, state + 1} end

@impl true def handle_call(:get, _from, state) do {:reply, state, state} end end

Supervisor

Supervisors manage process lifecycles:

defmodule MyApp.Application do use Application

@impl true def start(_type, _args) do children = [ # Database MyApp.Repo,

  # PubSub
  {Phoenix.PubSub, name: MyApp.PubSub},

  # GenServers
  {MyApp.Cache, []},
  {MyApp.Worker, []},

  # Endpoint (starts web server)
  MyAppWeb.Endpoint
]

opts = [strategy: :one_for_one, name: MyApp.Supervisor]
Supervisor.start_link(children, opts)

end end

Supervisor strategies:

  • :one_for_one

  • restart only failed child

  • :one_for_all

  • restart all children if one fails

  • :rest_for_one

  • restart failed child and those started after it

Agent

For simple state management:

{:ok, agent} = Agent.start_link(fn -> %{} end) Agent.update(agent, fn state -> Map.put(state, :key, "value") end) Agent.get(agent, fn state -> Map.get(state, :key) end)

Phoenix Framework

Controllers

defmodule MyAppWeb.UserController do use MyAppWeb, :controller

def index(conn, _params) do users = Accounts.list_users() render(conn, :index, users: users) end

def create(conn, %{"user" => user_params}) do case Accounts.create_user(user_params) do {:ok, user} -> conn |> put_flash(:info, "User created successfully") |> redirect(to: ~p"/users/#{user}")

  {:error, %Ecto.Changeset{} = changeset} ->
    render(conn, :new, changeset: changeset)
end

end end

Phoenix LiveView

For real-time interactive UIs:

defmodule MyAppWeb.CounterLive do use MyAppWeb, :live_view

@impl true def mount(_params, _session, socket) do {:ok, assign(socket, count: 0)} end

@impl true def handle_event("increment", _params, socket) do {:noreply, update(socket, :count, &(&1 + 1))} end

@impl true def render(assigns) do ~H""" <div> <h1>Count: <%= @count %></h1> <button phx-click="increment">+</button> </div> """ end end

PubSub for broadcasting:

Subscribe

Phoenix.PubSub.subscribe(MyApp.PubSub, "updates")

Broadcast

Phoenix.PubSub.broadcast(MyApp.PubSub, "updates", {:new_data, data})

Handle in LiveView

@impl true def handle_info({:new_data, data}, socket) do {:noreply, assign(socket, :data, data)} end

Channels

For WebSocket communication:

defmodule MyAppWeb.RoomChannel do use MyAppWeb, :channel

@impl true def join("room:" <> room_id, _payload, socket) do {:ok, assign(socket, :room_id, room_id)} end

@impl true def handle_in("new_message", %{"body" => body}, socket) do broadcast!(socket, "new_message", %{body: body}) {:reply, :ok, socket} end end

Ecto Database Patterns

Schemas and Changesets

defmodule MyApp.Accounts.User do use Ecto.Schema import Ecto.Changeset

schema "users" do field :name, :string field :email, :string field :age, :integer

has_many :posts, MyApp.Content.Post
timestamps()

end

def changeset(user, attrs) do user |> cast(attrs, [:name, :email, :age]) |> validate_required([:name, :email]) |> validate_format(:email, ~r/@/) |> validate_number(:age, greater_than: 0) |> unique_constraint(:email) end end

Queries

import Ecto.Query

Basic queries

query = from u in User, where: u.age > 18, select: u

Composable queries

def for_age(query, age) do from u in query, where: u.age > ^age end

def ordered(query) do from u in query, order_by: [desc: u.inserted_at] end

Chain them

User |> for_age(18) |> ordered() |> Repo.all()

Joins and preloads

from u in User, join: p in assoc(u, :posts), where: p.published == true, preload: [posts: p]

Transactions

Repo.transaction(fn -> with {:ok, user} <- create_user(params), {:ok, profile} <- create_profile(user), {:ok, _settings} <- create_settings(user) do user else {:error, reason} -> Repo.rollback(reason) end end)

Testing with ExUnit

defmodule MyApp.AccountsTest do use MyApp.DataCase, async: true

describe "create_user/1" do test "creates user with valid attributes" do attrs = %{name: "John", email: "john@example.com"} assert {:ok, user} = Accounts.create_user(attrs) assert user.name == "John" end

test "returns error with invalid email" do
  attrs = %{name: "John", email: "invalid"}
  assert {:error, changeset} = Accounts.create_user(attrs)
  assert %{email: ["has invalid format"]} = errors_on(changeset)
end

end end

Testing LiveView

defmodule MyAppWeb.CounterLiveTest do use MyAppWeb.ConnCase import Phoenix.LiveViewTest

test "increments counter", %{conn: conn} do {:ok, view, _html} = live(conn, "/counter") assert view |> element("button") |> render_click() =~ "Count: 1" end end

Deployment Best Practices

Releases

Use Elixir releases for production:

mix.exs

def project do [ releases: [ myapp: [ include_executables_for: [:unix], steps: [:assemble, :tar] ] ] ] end

Build and deploy:

MIX_ENV=prod mix release _build/prod/rel/myapp/bin/myapp start

Configuration

config/runtime.exs

import Config

if config_env() == :prod do database_url = System.get_env("DATABASE_URL") || raise "DATABASE_URL not available"

config :myapp, MyApp.Repo, url: database_url, pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10") end

Health Checks

In your router

get "/health", HealthController, :check

Controller

def check(conn, _params) do case Repo.query("SELECT 1") do {:ok, _} -> send_resp(conn, 200, "ok") _ -> send_resp(conn, 503, "database unavailable") end end

Consolidated Skills

This expert skill consolidates 1 individual skills:

  • elixir-expert

Iron Laws

  • ALWAYS use pattern matching and guards for control flow instead of nested if/case — idiomatic Elixir communicates intent through pattern matching; imperative conditionals fight the language.

  • NEVER use shared mutable state — always communicate through message passing between processes; shared mutable state in Elixir requires explicit ETS or Agent, which should be the exception not the rule.

  • ALWAYS use supervision trees for fault tolerance — never spawn bare processes that aren't supervised; unsupervised processes crash silently without recovery.

  • NEVER use Enum functions on potentially large streams — use Stream for lazy evaluation to avoid loading entire collections into memory.

  • ALWAYS write doctests (iex> examples in @doc ) for public functions — doctests are runnable specifications; they document behavior and serve as regression tests.

Anti-Patterns

Anti-Pattern Why It Fails Correct Approach

Nested if/cond chains instead of pattern matching Harder to read; misses the power of Elixir's pattern matching; doesn't scale Use function clauses with pattern matching heads and guard clauses

Bare spawn without supervision Crashed processes disappear silently; no restart, no visibility Always use Supervisor trees; use Task.Supervisor for dynamic tasks

Enum.map/filter on large streams Loads entire collection into memory; causes OOM on large datasets Use Stream.map/filter for lazy, memory-efficient pipeline processing

Global mutable state via Process dictionary Process dictionary is implicit state; makes code unpredictable and untestable Use Agent , GenServer , or ETS explicitly when shared state is required

No doctests for public functions Public API has no runnable specification; behavior drifts from documentation Always add iex> examples in @doc ; run with mix test

Memory Protocol (MANDATORY)

Before starting:

cat .claude/context/memory/learnings.md

After completing: Record any new patterns or exceptions discovered.

ASSUME INTERRUPTION: Your context may reset. If it's not in memory, it didn't happen.

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Automation

filesystem

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

slack-notifications

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

chrome-browser

No summary provided by upstream source.

Repository SourceNeeds Review