code-quality
Use when refactoring or reviewing Elixir code quality — compiler warnings and Credo first, then the plugin's on-demand…
Use when writing or enqueuing Oban background jobs — worker options, return values, idempotency, uniqueness, transactional enqueue, cron/pruner/lifeline config, Oban.Testing.
$ npx -y skills add j-morgan6/elixir-phoenix-guide --skill oban-essentials --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
/oban-essentialsContext preview
The summary Claude sees to decide when to auto-load this skill.
Use when writing or enqueuing Oban background jobs — worker options, return values, idempotency, uniqueness, transactional enqueue, cron/pruner/lifeline config, Oban.Testing.
name: oban-essentials description: "Use when writing or enqueuing Oban background jobs — worker options, return values, idempotency, uniqueness, transactional enqueue, cron/pruner/lifeline config, Oban.Testing."
> Verified against Elixir 1.20, Ecto 3.14, Oban 2.24, Req 0.7 — October 2026
1. **`use Oban.Worker` with explicit `queue:` and `max_attempts:`**, and match string-keyed args in `perform/1` 2. **Return `:ok`/`{:ok, value}` for success, `{:error, reason}` to retry, `{:cancel, reason}` to stop for good, `{:snooze, period}` to re-run later** — prefer tuples over raising 3. **Enqueue in the same transaction as the data the job needs** — `Oban.insert/2` inside `Repo.transact/2`, or `Oban.insert/3` in an `Ecto.Multi`; handle the result (no `Oban.insert!` in app code) 4. **Make workers idempotent** — a job can run more than once (retries, Lifeline rescues); record completion and send an idempotency key to external APIs 5. **Uniqueness keeps `:worker` in `fields`** — omit `fields` (default `[:args, :queue, :worker]`) or use `[:worker, :args]`; narrow with `keys:`, pick a state group such as `:incomplete`. `Oban.insert_all/2` ignores `unique` in OSS 6. **One `config :my_app, Oban` block with the 2.24 service keys** — `cron:`, `pruner:`, `lifeline:`; always enable `pruner` and `lifeline` 7. **Args hold IDs and small JSON values only** — fetch fresh data in `perform/1` 8. **Bound every job** — `timeout/1` for long work; outbound HTTP uses `retry: false` and a host allowlist; cap snoozes with `meta["snoozed"]` 9. **Test with `Oban.Testing`** — `testing: :manual`, `perform_job/3`, `assert_enqueued`, `drain_queue`; never call `perform/1` directly 10. **Workflows, Batches, global and rate limits, DynamicCron and bulk unique inserts are Oban Pro** — don't write OSS code that assumes them
---
defmodule MyApp.Workers.SendWelcomeEmail do
use Oban.Worker,
queue: :mailers,
max_attempts: 5,
unique: [period: {5, :minutes}, keys: [:user_id]]
alias MyApp.Accounts
@impl Oban.Worker
def perform(%Oban.Job{args: %{"user_id" => user_id}}) do
case Accounts.get_user(user_id) do
nil -> {:cancel, :user_not_found}
%{welcome_sent_at: %DateTime{}} -> :ok
user -> Accounts.deliver_welcome(user)
end
end
endEnqueue from the context, in the transaction that writes the data. If the transaction rolls back, the job is never inserted; if it commits, the job can't run before the row exists:
# The generated register_user/1 plus the job. If you added a registration_changeset/3
# (the phoenix-auth-customization skill), insert with that instead of email_changeset/2.
def register_user(attrs) do
Repo.transact(fn ->
with {:ok, user} <- %User{} |> User.email_changeset(attrs) |> Repo.insert(),
{:ok, _job} <- Oban.insert(SendWelcomeEmail.new(%{user_id: user.id})) do
{:ok, user}
end
end)
endThe `Ecto.Multi` form, when steps are built dynamically:
Ecto.Multi.new()
|> Ecto.Multi.insert(:user, User.email_changeset(%User{}, attrs))
|> Oban.insert(:welcome_job, fn %{user: user} -> SendWelcomeEmail.new(%{user_id: user.id}) end)
|> Repo.transact()# config/config.exs
config :my_app, Oban,
repo: MyApp.Repo,
queues: [default: 10, mailers: 5, imports: 2],
cron: [
crontab: [
{"0 2 * * *", MyApp.Workers.NightlyCleanup},
{"*/15 * * * *", MyApp.Workers.SyncData, args: %{source: "api"}}
]
],
pruner: [max_age: {7, :days}],
lifeline: [rescue_after: {30, :minutes}]
# config/test.exs: no queues or plugins run; jobs stay in the table for assertions
config :my_app, Oban, testing: :manual@impl Oban.Worker
def timeout(_job), do: to_timeout(minute: 2)
@impl Oban.Worker
def backoff(%Oban.Job{attempt: attempt}), do: trunc(:math.pow(2, attempt) * 10)· Changelog A Claude Code plugin (with Codex support for the skills) that keeps Claude's Elixir and Phoenix code idiomatic and current.
Use when refactoring or reviewing Elixir code quality — compiler warnings and Credo first, then the plugin's on-demand…
Use when preparing releases or deployment config — mix phx.gen.release, runtime.exs vs compile-time config, release migrations,…
Use when writing Ecto changeset validations beyond cast/validate_required — custom and cross-field validations (e.g. end date…
Use when defining schemas, writing queries, or creating migrations — scoped context functions, Repo.transact, upserts, indexes,…
Use when migrating PostgreSQL tables that serve live traffic — index concurrently,…
Use when one form or operation manages parent and child records together — cast_assoc/cast_embed, inputs_for with sort/drop…