Curated library of 38 atomic skills, 7 personas, and 1 orchestrator for Elixir and Phoenix development. Organized by category: fundamentals, phoenix, database, testing, auth, infrastructure, quality, security, integrations, tooling, frameworks, personas, and orchestration. Covers core Elixir patterns, Phoenix LiveView, Ecto, OTP, Oban, testing, security, deployment, real-time, and modern tooling (Req, Swoosh, Cachex, Broadway, Ash).
91
91%
Does it follow best practices?
Impact
91%
1.37xAverage score across 56 eval scenarios
Advisory
Suggest reviewing before use
Use this skill before writing ANY Oban worker or enqueuing jobs.
When setting up a new Oban worker, follow these steps in order:
config/config.exsuse Oban.Worker with explicit queue, max_attempts, and unique optionsOban.insert/1 inside a context function, not a LiveViewOban.Testing with assert_enqueued and perform_job, covering all return pathsOban.Worker with explicit queue and max_attempts — never rely on defaults — see Worker DefinitionOban.insert/1 (not Oban.insert!/1) — handle the error tuple instead of raising — see Enqueuing Jobs{:ok, _}, {:error, _}, {:cancel, _}, {:snooze, _} from perform/1 — never raise for expected failures — see Return ValuesSee assets/oban_job_template.ex for a copy-paste worker skeleton (perform/1, max_attempts, unique:, backoff/1, idempotency guard).
defmodule MyApp.Workers.SendWelcomeEmail do
use Oban.Worker,
queue: :mailers,
max_attempts: 3,
unique: [period: 300, fields: [:args], keys: [:user_id]]
@impl Oban.Worker
def perform(%Oban.Job{args: %{"user_id" => user_id}}) do
case MyApp.Accounts.get_user(user_id) do
nil ->
{:cancel, "user #{user_id} not found"}
user ->
MyApp.Mailer.send_welcome(user)
{:ok, :sent}
end
end
end# Basic insert — always handle the result
case MyApp.Workers.SendWelcomeEmail.new(%{user_id: user.id}) |> Oban.insert() do
{:ok, job} -> {:ok, job}
{:error, changeset} -> {:error, changeset}
end
# Schedule for later
%{user_id: user.id}
|> MyApp.Workers.SendWelcomeEmail.new(schedule_in: 3600)
|> Oban.insert()❌ Bad — raises on failure:
MyApp.Workers.SendWelcomeEmail.new(%{user_id: user.id}) |> Oban.insert!()✅ Good — context handles the job:
defmodule MyApp.Accounts do
def register_user(attrs) do
with {:ok, user} <- create_user(attrs) do
MyApp.Workers.SendWelcomeEmail.new(%{user_id: user.id})
|> Oban.insert()
{:ok, user}
end
end
end❌ Bad — LiveView enqueues directly:
def handle_event("register", params, socket) do
MyApp.Workers.SendWelcomeEmail.new(%{user_id: user.id}) |> Oban.insert()
endReturn exactly one of these from perform/1. Use {:error, reason} for retryable failures; never raise.
@impl Oban.Worker
def perform(%Oban.Job{args: args}) do
# Success — job completed, marked as completed
{:ok, result}
# Retryable failure — will retry up to max_attempts
{:error, reason}
# Permanent failure — will NOT retry, marked as cancelled
{:cancel, reason}
# Snooze — reschedule for later (in seconds)
{:snooze, 60}
end# config/config.exs
config :my_app, Oban,
repo: MyApp.Repo,
queues: [
default: 10, # 10 concurrent jobs
mailers: 5, # 5 concurrent email jobs
imports: 2 # 2 concurrent import jobs (resource-heavy)
]
# config/test.exs — use testing mode
config :my_app, Oban,
testing: :inline # Jobs execute immediately in the test process❌ Bad — sends duplicate emails on retry:
@impl Oban.Worker
def perform(%Oban.Job{args: %{"user_id" => user_id}}) do
user = MyApp.Accounts.get_user!(user_id)
MyApp.Mailer.send_welcome(user)
{:ok, :sent}
end✅ Good — check if already processed:
@impl Oban.Worker
def perform(%Oban.Job{args: %{"user_id" => user_id}}) do
user = MyApp.Accounts.get_user!(user_id)
if user.welcome_email_sent_at do
{:ok, :already_sent}
else
with {:ok, _} <- MyApp.Mailer.send_welcome(user),
{:ok, _} <- MyApp.Accounts.mark_welcome_sent(user) do
{:ok, :sent}
end
end
enduse Oban.Worker,
queue: :default,
unique: [
period: 300, # 5-minute uniqueness window
fields: [:args, :queue], # match on these fields
keys: [:user_id], # only compare these arg keys
states: [:available, :scheduled, :executing]
]# Schedule a job for later
%{report_id: report.id}
|> MyApp.Workers.GenerateReport.new(schedule_in: {1, :hour})
|> Oban.insert()
# Cron-based recurring jobs (in config)
config :my_app, Oban,
repo: MyApp.Repo,
queues: [default: 10],
plugins: [
{Oban.Plugins.Cron, crontab: [
{"0 2 * * *", MyApp.Workers.NightlyCleanup},
{"*/15 * * * *", MyApp.Workers.SyncData, args: %{source: "api"}}
]}
]See assets/oban_testing_checklist.md for a copy-paste checklist covering enqueue assertions, perform_job/2, and all return paths.
perform_job/2 — not perform/1. perform_job validates args and simulates the Oban runtime.assert_enqueued/1 — verify jobs were enqueued with correct args.Oban.Testing inline mode in test config — jobs run synchronously in the test process.defmodule MyApp.Workers.SendWelcomeEmailTest do
use MyApp.DataCase, async: true
use Oban.Testing, repo: MyApp.Repo
alias MyApp.Workers.SendWelcomeEmail
test "enqueuing a welcome email job" do
user = user_fixture()
SendWelcomeEmail.new(%{user_id: user.id})
|> Oban.insert()
assert_enqueued(worker: SendWelcomeEmail, args: %{user_id: user.id})
end
test "performing the job sends the email" do
user = user_fixture()
assert {:ok, :sent} =
perform_job(SendWelcomeEmail, %{user_id: user.id})
end
test "cancels if user not found" do
assert {:cancel, _reason} =
perform_job(SendWelcomeEmail, %{user_id: -1})
end
end❌ Bad — large data in args (stored as JSON in database):
SendReport.new(%{
user_id: user.id,
report_data: large_data_structure # Don't do this!
})✅ Good — store IDs, fetch fresh data in worker:
SendReport.new(%{user_id: user.id, report_id: report.id})| ❌ Don't | ✅ Do |
|---|---|
Oban.insert!/1 that raises on failure | Oban.insert/1 and match {:ok, job} / {:error, changeset} |
Non-idempotent perform/1 (re-sends on retry) | Guard with a "already processed" check before side effects |
Store large payloads in args | Store IDs; fetch fresh data inside the worker |
| Enqueue directly from a LiveView/controller | Enqueue from a context function |
raise for an expected failure | Return {:error, reason} (retry) or {:cancel, reason} (stop) |
Omit unique: on jobs that can double-enqueue | Set unique: [period: ..., keys: [...]] to dedupe |
| Predecessor | This Skill | Successor |
|---|---|---|
| ecto-essentials | oban-essentials | testing-essentials |
| elixir-essentials | oban-essentials | telemetry-essentials |
Companion skills:
background-job — persona that orchestrates the full worker workflowbroadway-data-pipelines — high-throughput pipelines fed by external message queuestelemetry-essentials — observe job execution and failures.tessl-plugin
evals
scenario-1
scenario-2
scenario-3
scenario-4
scenario-5
scenario-6
scenario-7
scenario-8
scenario-9
scenario-10
scenario-11
scenario-12
scenario-13
scenario-14
scenario-15
scenario-16
scenario-17
scenario-18
scenario-19
scenario-20
scenario-21
scenario-22
scenario-23
scenario-24
scenario-25
scenario-26
scenario-27
scenario-28
scenario-29
scenario-30
scenario-31
scenario-32
scenario-33
scenario-34
scenario-35
scenario-36
scenario-37
scenario-38
scenario-39
scenario-40
scenario-41
scenario-42
scenario-43
scenario-44
scenario-45
scenario-46
scenario-47
scenario-48
scenario-49
scenario-50
scenario-51
scenario-52
scenario-53
scenario-54
scenario-55
scenario-56
skills
frameworks
ash-framework
infrastructure
orchestration
elixir-skill-router
personas
phoenix
quality
security
security-essentials
tooling
mix-tasks-generators