This skill should be used when the user asks to "add a background job", "process async", "schedule a task", "retry failed jobs", "add email sending", "run this later", "add a cron job", "unique jobs", "batch process", or mentions Oban, Oban Pro, workflows, job queues, cascades, grafting, recorded values, job args, or troubleshooting job failures.
View on GitHubFebruary 1, 2026
Select agents to install to:
npx add-skill https://github.com/georgeguimaraes/claude-code-elixir/blob/main/plugins/elixir/skills/oban-thinking/SKILL.md -a claude-code --skill oban-thinkingInstallation paths:
.claude/skills/oban-thinking/# Oban Thinking
Paradigm shifts for Oban job processing. These insights prevent common bugs and guide proper patterns.
---
# Part 1: Oban (Non-Pro)
## The Iron Law: JSON Serialization
```
JOB ARGS ARE JSON. ATOMS BECOME STRINGS.
```
This single fact causes most Oban debugging headaches.
```elixir
# 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
```
## Error Handling: Let It Crash
**Don't catch errors in Oban jobs.** Let them bubble up to Oban for proper handling.
### Why?
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
### Anti-Pattern
```elixir
# 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
```
### Correct Pattern
```elixir
# 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
end
```
### When to Catch Errors
Only catch errors when you need custom retry logic or want to mark a job as permanently failed:
```elixir
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