.claude_37signals/skills/job-patterns/SKILL.md
Implements shallow background jobs with _later/_now conventions using Solid Queue. Use when adding background processing, async operations, scheduled tasks, or when user mentions jobs, queues, workers, or background processing. WHEN NOT: Business logic implementation (use model-patterns), controller work (use crud-patterns), or mailer delivery (use mailer-patterns).
npx skillsauth add ThibautBaissac/rails_ai_agents job-patternsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Jobs orchestrate. Models do the work. Background jobs are thin wrappers around model methods.
Tech Stack: Rails 8.2 (edge), Solid Queue, ActiveJob
Pattern: Thin jobs call model methods; models have _later/_now pairs
Commands:
bin/rails generate job NotifyRecipients # Generate job
bundle exec rake solid_queue:start # Run worker
bin/rails runner "puts SolidQueue::Job.count" # Check queue
bin/rails runner "SolidQueue::Job.destroy_all" # Clear jobs
_now in tests)The job receives a model and calls its _now method. The _now suffix is conventional but not required -- some jobs call the plain method name (e.g., notifiable.notify_recipients).
# app/jobs/notify_recipients_job.rb
class NotifyRecipientsJob < ApplicationJob
queue_as :default
def perform(notifiable)
notifiable.notify_recipients_now
end
end
The model defines both _later and _now methods:
# In model or concern
def notify_recipients_later
NotifyRecipientsJob.perform_later(self)
end
def notify_recipients_now
recipients.each do |recipient|
next if recipient == creator
Notification.create!(recipient: recipient, notifiable: self, action: notification_action)
end
end
# Default to sync
def notify_recipients
notify_recipients_now
end
# Trigger async from callbacks
after_create_commit :notify_recipients_later
class NotifyRecipientsJob < ApplicationJob
queue_as :default
def perform(notifiable)
notifiable.notify_recipients_now
end
end
class DeliverBundledNotificationsJob < ApplicationJob
queue_as :default
def perform
Notification::Bundle.deliver_all_now
end
end
class SessionCleanupJob < ApplicationJob
queue_as :low_priority
def perform
Session.cleanup_old_sessions_now
end
end
class TrackEventJob < ApplicationJob
queue_as :default
def perform(eventable, action, options = {})
eventable.track_event_now(action, options)
end
end
class BroadcastUpdateJob < ApplicationJob
queue_as :default
def perform(broadcastable)
broadcastable.broadcast_update_now
end
end
class DispatchWebhookJob < ApplicationJob
queue_as :webhooks
retry_on StandardError, wait: :exponentially_longer, attempts: 5
def perform(webhook, event)
webhook.dispatch_now(event)
end
end
class DispatchWebhookJob < ApplicationJob
discard_on Webhook::InvalidUrl # Don't retry
retry_on StandardError, wait: :exponentially_longer, attempts: 5 # Backoff
retry_on CustomError, wait: 5.minutes, attempts: 3 # Fixed interval
rescue_from Webhook::Timeout do |exception|
webhook.mark_as_slow!
raise exception # Re-raise to trigger retry
end
def perform(webhook, event)
webhook.dispatch_now(event)
end
end
Always set and reset Current context:
class NotifyRecipientsJob < ApplicationJob
before_perform do |job|
notifiable = job.arguments.first
Current.account = notifiable.account
Current.user = notifiable.creator if notifiable.respond_to?(:creator)
end
after_perform { Current.reset }
def perform(notifiable)
notifiable.notify_recipients_now
end
end
# Enqueue in batches
Card.active.pluck(:id).each_slice(100) do |batch|
ProcessCardsJob.perform_later(batch)
end
class ProcessCardsJob < ApplicationJob
def perform(card_ids)
Card.where(id: card_ids).find_each(&:process_now)
end
end
def reindex_later
return if reindex_job_queued?
ReindexBoardJob.perform_later(id)
end
def reindex_job_queued?
SolidQueue::Job.exists?(
job_class: "ReindexBoardJob",
arguments: [id].to_json,
finished_at: nil
)
end
class CommentTest < ActiveSupport::TestCase
test "notify_recipients_now creates notifications" do
comment = comments(:logo_comment)
assert_difference -> { Notification.count }, 2 do
comment.notify_recipients_now
end
end
end
class NotifyRecipientsJobTest < ActiveJob::TestCase
test "enqueues job" do
comment = comments(:logo_comment)
assert_enqueued_with job: NotifyRecipientsJob, args: [comment] do
NotifyRecipientsJob.perform_later(comment)
end
end
end
test "creating comment enqueues notification job" do
card = cards(:logo)
assert_enqueued_with job: NotifyRecipientsJob do
card.comments.create!(body: "Great work!", creator: users(:david))
end
end
See references/solid-queue.md for Solid Queue configuration and
references/recurring-jobs.md for recurring job setup.
_later/_now naming convention (though the _now suffix is not strictly enforced), put business logic in models, set queue priorities, implement retry strategies, test model methods directlyCurrent.reset in jobs, skip retry strategies for unreliable operationsdevelopment
Creates Turbo Streams, Turbo Frames, and morphing patterns for real-time UI updates. Use when adding real-time updates, partial page rendering, form submissions, or broadcasting. WHEN NOT: For Stimulus JavaScript controllers (see stimulus-patterns skill). For general view conventions (see rules/views.md).
testing
Writes Minitest tests with fixtures following 37signals conventions. Uses Minitest (not RSpec) and fixtures (not factories). Use when writing tests, adding test coverage, or creating fixtures. WHEN NOT: For RSpec or FactoryBot patterns (this project uses Minitest + fixtures exclusively). For test configuration/CI setup (see project docs).
tools
Builds focused, single-purpose Stimulus controllers for progressive enhancement. Use when adding JavaScript behavior, UI interactions, form enhancements, or building reusable client-side components. WHEN NOT: For Turbo Stream/Frame patterns (see turbo-patterns skill). For server-side view logic (see rules/views.md).
testing
Implements the state-as-records-not-booleans pattern for rich state tracking. Use when modeling state changes, replacing boolean flags with record-based state, or when user mentions state records, closures, publications, or toggling state. WHEN NOT: Technical flags like cached/processed (use booleans), concern extraction (use concern-patterns), general model work (use model-patterns).