# Active Job Coder

> Use when creating or refactoring Active Job background jobs.

- Skill: `majesticlabs-dev/active-job-coder` (Agent Skill)
- Install (CLI): `npx skillmds@latest add majesticlabs-dev/active-job-coder`
- Raw SKILL.md: https://api.skillmd.com/api/skills/majesticlabs-dev/active-job-coder/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: majesticlabs-dev (https://skillmd.com/u/majesticlabs-dev)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/majesticlabs-dev/active-job-coder

---


# Active Job Coder

## Job Design Principles

### 1. Single Responsibility

```ruby
# Good: Focused job that delegates domain work
class SendWelcomeEmailJob < ApplicationJob
  queue_as :default

  def perform(user)
    user.send_welcome_email
  end
end
```

Keep job bodies shallow. The job should restore context, choose queue/retry behavior, and call a model or domain method that owns the actual work.

### 2. Pass Stable Payloads

```ruby
# Good: pass identifiers for external boundaries or very stale-sensitive work
class ProcessOrderJob < ApplicationJob
  def perform(order_id)
    order = Order.find(order_id)
    order.process
  end
end
```

Passing Active Record instances is also valid in Rails when GlobalID semantics are the right contract. Avoid large object graphs, hashes of denormalized state, or relying on `Current` inside `perform`.

### 3. Queue Configuration

```ruby
class CriticalNotificationJob < ApplicationJob
  queue_as :critical
  queue_with_priority 1  # Lower = higher priority
end

class ReportGenerationJob < ApplicationJob
  queue_as :low_priority
  queue_with_priority 50
end
```

The queue also selects the execution model when the backend runs mixed workers, since Solid Queue supports `fibers:` alongside `threads:`. Route by workload shape: I/O-bound jobs such as LLM calls, HTTP requests, and Turbo broadcasts belong on a fiber queue; CPU-bound work and jobs holding a transaction for their whole run belong on a thread queue. See `solid-queue-coder` for the worker config and pool sizing.

## Error Handling & Retry Strategies

```ruby
class ExternalApiJob < ApplicationJob
  queue_as :default

  retry_on Net::OpenTimeout, wait: :polynomially_longer, attempts: 5
  retry_on ActiveRecord::Deadlocked, wait: 5.seconds, attempts: 3
  discard_on ActiveJob::DeserializationError

  def perform(record_id)
    record = Record.find(record_id)
    ExternalApi.sync(record)
  end
end
```

### Custom Error Handling

```ruby
class ImportantJob < ApplicationJob
  rescue_from StandardError do |exception|
    Rails.logger.error("Job failed: #{exception.message}")
    ErrorNotifier.notify(exception, job: self.class.name)
    raise # Re-raise to trigger retry
  end
end
```

## Concurrency Control (Solid Queue)

```ruby
class ProcessUserDataJob < ApplicationJob
  limits_concurrency key: ->(user_id) { user_id }, duration: 15.minutes

  def perform(user_id)
    user = User.find(user_id)
    # Process user data safely
  end
end

class ContactActionJob < ApplicationJob
  limits_concurrency key: ->(contact) { contact.id },
                     duration: 10.minutes,
                     group: "ContactActions"
end
```

## Scheduling & Delayed Execution

```ruby
SendReminderJob.perform_later(user)                              # Immediate
SendReminderJob.set(wait: 1.hour).perform_later(user)            # Delayed
SendReminderJob.set(wait_until: Date.tomorrow.noon).perform_later(user)  # Scheduled

# Bulk enqueue
ActiveJob.perform_all_later(users.map { |u| SendReminderJob.new(u.id) })
```

Set `ActiveJob::Base.enqueue_after_transaction_commit = true` at the application level when jobs depend on records created or changed in the same transaction. That fixes job-before-commit races at the source.

For multi-tenant apps, serialize tenant context at enqueue time and restore it around `perform`; do not infer the tenant from a bare record ID later.

## Anti-Patterns to Avoid

| Anti-Pattern | Problem | Solution |
|--------------|---------|----------|
| Fat jobs | Hard to test and maintain | Extract logic to model classes |
| Large serialized payloads | Expensive, stale data | Pass IDs or GlobalID records with clear freshness semantics |
| No retry strategy | Silent failures | Use `retry_on` with backoff |
| Synchronous calls | Blocks request | Always use `perform_later` |
| No idempotency | Duplicate processing | Design jobs to be re-runnable |
| Ignoring errors | Silent failures | Use `rescue_from` with logging |
| Monolithic steps | Can't resume after failure | Use Continuable pattern with state |
| `Current.user` inside `perform` | Request context is gone | Pass actor or tenant context at enqueue |
| CPU-heavy or long-transaction job on a fiber queue | Blocks the reactor for every other fiber, or pins a connection for the job's lifetime | Route it to a thread worker queue |

## Output Format

When creating or refactoring jobs, provide:

1. **Job Class** - The complete job implementation
2. **Queue Strategy** - Recommended queue and priority
3. **Error Handling** - Retry and failure strategies
4. **Testing** - Example test cases
5. **Considerations** - Concurrency, idempotency notes

