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 GitHubgeorgeguimaraes/claude-code-elixir
elixir
January 23, 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