Use when writing background jobs or async operations - enforces thin job wrappers (3-5 lines) that delegate to models using _later/_now naming pattern
View on GitHubZempTime/zemptime-marketplace
vanilla-rails
January 20, 2026
Select agents to install to:
npx add-skill https://github.com/ZempTime/zemptime-marketplace/blob/main/vanilla-rails/skills/jobs/SKILL.md -a claude-code --skill vanilla-rails-jobsInstallation paths:
.claude/skills/vanilla-rails-jobs/# Vanilla Rails Jobs
**Jobs are thin wrappers (3-5 lines). ALL business logic lives in models.**
## The Pattern
```ruby
# Model concern - WHERE THE LOGIC LIVES
module Card::ClosureNotifications
extend ActiveSupport::Concern
included do
after_update :notify_watchers_later, if: :just_closed?
end
# _later: Enqueues the job
def notify_watchers_later
Card::ClosureNotificationJob.perform_later(self)
end
# _now: Contains ALL business logic
def notify_watchers_now
watchers.each do |watcher|
CardMailer.closure_notification(watcher, self).deliver_now
Notification.create!(user: watcher, card: self, action: 'closed')
end
end
private
def just_closed?
saved_change_to_status? && closed?
end
end
# Job - ONLY delegates (3 lines)
class Card::ClosureNotificationJob < ApplicationJob
def perform(card)
card.notify_watchers_now
end
end
```
## Why Jobs Stay Thin
**Testability:** Test `_now` synchronously (no job infrastructure needed)
**Reusability:** Call `_now` in console, tests, anywhere
**Debuggability:** Stack traces point to model, not job framework
## Naming Convention
| Method | Purpose |
|--------|---------|
| `action_later` | Enqueues job |
| `action_now` | Actual logic (called by job, ALWAYS create for testing) |
| `action` | No async version |
**Flow:** Callback → `_later` → enqueue job → job calls `_now` → logic executes
## Red Flags - STOP and Fix
If you see ANY of these, you're doing it wrong:
- [ ] Job longer than 5 lines (except ActiveJob config like `retry_on`)
- [ ] Business logic in job (queries, conditionals, loops)
- [ ] Job creates/updates records
- [ ] Job sends emails directly
- [ ] Job calls multiple models directly
- [ ] No `_later`/`_now` naming
- [ ] Passing IDs to job instead of objects
- [ ] Logic split between job and model
- [ ] Job has error handling beyond `retry_on`/`discard_on`
- [ ] Model missing `_now` method ("I don't need it")
**ALL of these mean: Move logic to moIssues Found: