ecto
Design and debug Elixir persistence with Ecto. Use for schemas, changesets, queries, preloads, transactions, migrations, multi-tenancy, and data access through…
Build and debug durable background jobs with Oban and Oban Pro. Use for workers, job argument serialization, retries, scheduled or recurring jobs, uniqueness, batches, and workflows. Use otp for in-memory tasks and process supervision.
$ npx -y skills add georgeguimaraes/claude-code-elixir --skill oban --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
/obanContext preview
The summary Claude sees to decide when to auto-load this skill.
Build and debug durable background jobs with Oban and Oban Pro. Use for workers, job argument serialization, retries, scheduled or recurring jobs, uniqueness, batches, and workflows. Use otp for in-memory tasks and process supervision.
name: oban description: Build and debug durable background jobs with Oban and Oban Pro. Use for workers, job argument serialization, retries, scheduled or recurring jobs, uniqueness, batches, and workflows. Use otp for in-memory tasks and process supervision.
Handle durable jobs, serialization, failures, and workflow composition with Oban.
---
JOB ARGS ARE JSON. ATOMS BECOME STRINGS.
This single fact causes most Oban debugging headaches.
# Creating - atom keys are fine
MyWorker.new(%{user_id: 123})
# Processing - must use string keys (JSON converted atoms to strings)
def perform(%Oban.Job{args: %{"user_id" => user_id}}) do
# ...
end**Don't catch errors in Oban jobs.** Let them bubble up to Oban for proper handling.
1. **Automatic logging**: Oban logs the full error with stacktrace 2. **Automatic retries**: Jobs retry with exponential backoff 3. **Visibility**: Failed jobs appear in Oban Web dashboard 4. **Consistency**: Error states are tracked in the database
# Bad: Swallowing errors
def perform(%Oban.Job{} = job) do
case do_work(job.args) do
{:ok, result} -> {:ok, result}
{:error, reason} ->
Logger.error("Failed: #{reason}")
{:ok, :failed} # Silently marks as complete!
end
end# Good: Let errors propagate
def perform(%Oban.Job{} = job) do
result = do_work!(job.args) # Raises on failure
{:ok, result}
end
# Or return error tuple - Oban treats as failure
def perform(%Oban.Job{} = job) do
case do_work(job.args) do
{:ok, result} -> {:ok, result}
{:error, reason} -> {:error, reason} # Oban will retry
end
endOnly catch errors when you need custom retry logic or want to mark a job as permanently failed:
def perform(%Oban.Job{} = job) do
case external_api_call(job.args) do
{:ok, result} -> {:ok, result}
{:error, :not_found} -> {:cancel, :resource_not_found} # Don't retry
{:error, :rate_limited} -> {:snooze, 60} # Retry in 60 seconds
{:error, _} -> {:error, :will_retry} # Normal retry
end
endUse `{:snooze, seconds}` for polling external state instead of manual retry logic:
def perform(%Oban.Job{} = job) do
if external_thing_finished?(job.args) do
{:ok, :done}
else
{:snooze, 5} # Check again in 5 seconds
end
endFor simple sequential chains (JobA → JobB → JobC), have each job enqueue the next:
def perform(%Oban.Job{} = job) do
result = do_work(job.args)
# Enqueue next job on success
NextWorker.new(%{data: result}) |> Oban.insert()
{:ok, result}
end**Don't reach for Oban Pro Workflows for linear chains.**
Prevent duplicate jobs with the `unique` option:
use Oban.Worker, queue: :default, unique: [period: 60] # Only one job with same args per 60 seconds # Or scope uniqueness to specific fields unique: [period: 300, keys: [:user_id]]
**Gotcha:** Uniqueness is checked on insert, not execution. Two identical jobs inserted 61 seconds apart will both run.
For millions of records, **chunk work into batches** rather than one job per item:
# Bad: One job per contact (millions of jobs = database strain)
Enum.each(contacts, &ContactWorker.new(%{id: &1.id}) |> Oban.insert())
# Good: Chunk into batches
contacts
|> Enum.chunk_every(100)
|> Enum.each(&BatchWorker.new(%{contact_ids: Enum.map(&1, fn c -> c.id end)}) |> Oban.insert())Use bulk inserts without uniqueness constraints for maximum throughput.
---
Unlike regular job args, **cascade context preserves atoms**:
# Creating - atom keys
Workflow.put_context(%{score_run_id: id})
# Processing - atom keys still work!
def my_cascade(%{score_run_id: id}) do
# ...
end
# Dot notation works too
def later_step(context) do
context.score_run_id
context.previous_result
end| | Creating | Processing | |-----------------|----------|--------------| | Regular jobs | atoms ok | strings only | | Cascade context | atoms ok | atoms ok |
Reserve Workflows for:
**Don't use Workflows for simple A → B → C chains.**
When you need a parent workflow to wait for a sub-workflow to complete before continuing, use `add_graft` instead of `add_workflow`.
| Method | Sub-workflow completes before deps run? | Output accessible? | |--------|----------------------------------------|-------------------| | `add_workflow` | No - just inserts jobs | No | | `add_graft` | Yes - waits for all jobs | Yes, via recorded values |
Don't couple unrelated concerns (e.g., notifications) to domain-specific workflows (e.g., scoring). Instead, create a higher-level orchestrator:
# Bad: Notification logic buried in AggregateScores
defmodule AggregateScores do
def workflow(score_run_id) do
Workflow.new()
|> Workflow.add(:aggregate, AggregateJob.new(...))
|> Workflow.add(:send_notification, SendEmail.new(...), deps: :aggregate) # Wrong place!
end
end
# Good: Higher-level workflow composes scoring + notification
defmodule FullRunWithNotifications do
def workflow(site_url, opts) do
notification_opts = build_notification_opts(opts)
Workflow.new()
|> Workflow.put_context(%{notification_opts: notification_opts})
|> Workflow.add_graft(:scoring, &graft_full_run/1)
|> Workflow.add_cascade(:send_notification, &send_notification/1, deps: :scorElixir development guidance for coding agents, with optional Mix checks and Expert language server integration. Install the elixir-dev plugin for five skills covering language idioms, Phoenix interfaces, Ecto persistence, OTP processes, and Oban jobs.
Design and debug Elixir persistence with Ecto. Use for schemas, changesets, queries, preloads, transactions, migrations, multi-tenancy, and data access through…
Write and refactor idiomatic Elixir functions, modules, and data structures. Use for pattern matching, control flow, error handling, protocols, behaviours, and…
Design and debug Elixir runtime concurrency, process state, and supervision. Use for GenServer, Supervisor, Task, Registry, ETS, process bottlenecks, and…
Build and debug Phoenix web interfaces and HTTP endpoints. Use for LiveView lifecycle and data loading, components, forms, routes, controllers, Plug, channels,…