AgentStack
SKILL verified MIT Self-run

Style

skill-vinnie357-claude-skills-style · by vinnie357

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.

No reviews yet
0 installs
1 views
0.0% view→install

Install

$ agentstack add skill-vinnie357-claude-skills-style

✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access Used
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README — it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/skill-vinnie357-claude-skills-style)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
2d ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming — see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps — measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Style? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

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:

# 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.

# 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.

# 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!/1content or raises | | Map.fetch/2{:ok, value} or :error | Map.fetch!/2value or raises KeyError | | Repo.insert/1{:ok, struct} or {:error, changeset} | Repo.insert!/1struct or raises | | Repo.get/2struct or nil | Repo.get!/2struct 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:

# 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: "admin@example.com", 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:

# 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
# 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:

def register_user(params) do
  with {:ok, validated}  {: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:

# BAD: nested with — hard to follow
with {:ok, user}  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:

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:

# 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

Validate data once, at the changeset layer — schema defines shape, changeset validates, the context/controller route on {:ok, _} / {:error, changeset}, and the form template renders changeset errors directly. Includes the duplicated-validation anti-pattern and cast vs change. This is the duplicated surface the dedicated /elixir:ecto skill already owns in depth; see references/ecto-data-shapes.md.

Embedded Schemas for Non-DB Data

Non-DB data (search forms, filter panels, API query params) still gets full changeset validation via embedded_schema or a schemaless changeset — a continuation of the gatekeeper principle above: see references/ecto-data-shapes.md.

Pipe Operator Conventions

When to Pipe

Pipe when chaining data transformations where each step passes its result to the next:

# Good use of pipe: transforming a value through a sequence of steps
def normalize_email(email) do
  email
  |> String.trim()
  |> String.downcase()
  |> String.replace(~r/\+.*@/, "@")
end

When Not to Pipe

Avoid piping a single function call — it adds visual noise without clarity benefit:

# BAD: unnecessary pipe
result = value |> some_function()

# GOOD: direct call
result = some_function(value)

Avoid piping side effects that don't transform data:

# BAD: pipe into a side effect
user |> send_welcome_email()

# GOOD: explicit call
send_welcome_email(user)

Map and Struct Access

Elixir provides two access patterns with different semantics:

# Dot access — raises KeyError if key missing
# Use for required fields on known structs
user.email        # raises if :email key not present
config.timeout    # use when the field must exist

# Bracket access — returns nil if key missing
# Use for optional fields on dynamic maps
params[:email]    # returns nil if missing, no crash
opts[:timeout]    # safe for optional config keys

Struct fields always use dot access — structs enforce their shape at compile time, so a missing key is a programming error:

# Always use dot access for structs
%User{} = user
user.email   # correct
user[:email] # valid but unusual — prefer dot for structs

Dynamic maps from external sources use bracket access for optional fields:

def build_query(filters) do
  base_query = from(u in User)

  base_query
  |> maybe_filter_role(filters[:role])
  |> maybe_filter_after(filters[:created_after])
end

Anti-Fabrication

Apply core:anti-fabrication when generating code examples or making claims about library behavior. Verify function signatures and return types against actual documentation. Do not fabricate error codes, changeset validator names, or Ecto API details.

Key Principles

  • Return {:ok, result} / {:error, reason} tuples to enable exhaustive pattern matching
  • Use bang functions only when failure indicates a programming error or invalid system state
  • Use with for multi-step operations where each step depends on the previous
  • Let the Ecto changeset own all data validation — do not duplicate it in controllers or LiveView
  • Use embedded_schema or schemaless changesets for non-DB data that still needs validation
  • Pipe for transformation chains; call directly for single operations and side effects
  • Reserve try/rescue for third-party code that raises; use tagged tuples for your own error paths

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet — be the first.

Versions

  • v0.1.0 Imported from the upstream source.