agentsclimarketplace

Writing jobs

Skill chrisrackstallion/pitchd-rails-cursor-plugin/skills/writing-jobs

Write Rails background jobs following DHH/37signals conventions — thin jobs that delegate to model methods, idempotent execution, after_commit timing, and retry discipline. Use when writing ActiveJob subclasses, scheduling background work, handling async processing, or when the user mentions jobs, queues, background processing, perform_later, or ActiveJob.From its SKILL.md

Install
npx -y skills add chrisrackstallion/pitchd-rails-cursor-plugin --skill writing-jobs

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

7.5 KB, as published. Nobody here has run it

Writing Rails Jobs

Reading plugin files: Before your first Read of rules/*.mdc or a sibling skills/*/SKILL.md, resolve the correct path prefix via resolving-plugin-root — these paths (bare or with ../) only resolve correctly against Cursor's workspace root or a raw checkout; a Claude Code plugin install needs the resolved prefix instead.

<objective> Jobs are the async service layer Rails provides. They are thin orchestrators — the same principle that governs controllers applies here: delegate domain logic to models, keep the job itself short, and treat the queue as a delivery mechanism, not a place to build features. A job that reaches directly into business logic belongs on the model; a job that coordinates a model verb asynchronously is exactly right. </objective>

Process

1. Determine What You're Building

TaskAction
New background jobRead references/patterns.md § Job Structure
Async emailUse deliver_later — see rules/mailers.mdc; the mailer IS the job
Retry strategyRead references/patterns.md § Error Handling
Scheduling / recurring workRead references/patterns.md § Scheduling
Job with complex argument passingRead references/patterns.md § Arguments
Writing job specsRead skills/writing-tests/references/support-specs.md § Job Specs
Code reviewRead all references, review against conventions

2. Job Structure

Every job follows this shape:

# app/jobs/notify_subscribers_job.rb
class NotifySubscribersJob < ApplicationJob
  queue_as :default

  def perform(article_id)
    article = Article.find(article_id)
    article.notify_subscribers
  end
end

Key principles:

  • Inherits ApplicationJob — not ActiveJob::Base directly; shared configuration (queue adapter, retry defaults) belongs on ApplicationJob
  • One verb in perform — the job finds the record and calls a model method; domain logic lives on the model
  • Pass IDs, not objects — ActiveRecord objects are not reliably serializable across queue restarts; find the record fresh in perform
  • Named by what happens, not what triggers it — NotifySubscribersJob, not ArticlePublishedJob

3. Enqueue Timing — the after_commit Rule

Never enqueue a job inside a database transaction. If the transaction rolls back, the job still runs — against a record that no longer exists or is in a state the job doesn't expect.

# Bad — inside a transaction, job may run before commit or after rollback
def publish(by: Current.user)
  transaction do
    create_publication!(publisher: by)
    NotifySubscribersJob.perform_later(self)  # unsafe
  end
end

# Good — after_commit fires only when the transaction commits
after_create_commit :notify_subscribers_later

private
  def notify_subscribers_later
    NotifySubscribersJob.perform_later(id)
  end

Use after_create_commit, after_update_commit, or after_commit callbacks (or the model method equivalent) to enqueue work. For mailers specifically, use deliver_later from an after_commit callback or after the transaction.

4. Idempotency

Jobs can be retried — by the queue backend on failure, by a developer manually, or after a deploy with pending jobs. A job that is safe to run twice is idempotent. Design for it from the start.

# Good — idempotent: find_or_create_by is safe to run again
def perform(invitation_id)
  invitation = Invitation.find(invitation_id)
  return if invitation.accepted?   # guard against duplicate execution
  invitation.send_reminder
end

Strategies:

  • Guard clauses — check the record's current state at the start of perform; return early if the work is already done
  • Database uniqueness — unique indexes prevent duplicate records even if a job runs twice
  • Idempotency keys — for external API calls, use the provider's idempotency key feature

5. Decision Framework

Before writing code, ask these questions:

"Does this need a job?"

  • Work that happens synchronously in the request and is fast enough → inline
  • Work that is slow, external, or should not block the HTTP response → job
  • Work that should be retried automatically on failure → job
  • A mailer → deliver_later; not a custom job wrapping a mailer

"What should the job be named?"

  • Verb + noun + Job: ProcessPaymentJob, SyncInventoryJob, SendDigestJob
  • Named after what the job does, not what caused it

"What queue?"

  • default for ordinary background work
  • critical for user-facing work that needs low latency (password resets, transactional email)
  • mailers if your configuration separates mail delivery
  • imports or exports for bulk data operations that are slow and can wait
  • Match queue names to your worker configuration — unused queues are wasted configuration

"How do I handle failure?"

  • Transient error (network timeout, external API down) → retry_on
  • Permanent error (record not found, invalid state) → discard_on
  • Unknown — default retries from ApplicationJob are usually sufficient

6. Anti-Patterns

Anti-PatternInstead
Domain logic in performCall a model method from perform
Passing ActiveRecord objects as argumentsPass IDs; find records in perform
perform_now in production codeperform_later — perform_now blocks the request
Enqueueing inside a transactionafter_commit callback or after save!
One giant job that does everythingSmall focused jobs; orchestrate by chaining or enqueueing more jobs
God job replacing a service layerModel methods handle domain logic; job calls them
Jobs for synchronous fast workInline in the controller or model
Wrapping deliver_later in a jobCall deliver_later directly
Silently rescuing all exceptionsretry_on and discard_on with specific error classes
No idempotency considerationGuard with a state check at the top of perform

7. Naming Conventions

ThingConventionExamples
Job classVerbNounJobNotifySubscribersJob, ProcessPaymentJob
Job fileverb_noun_job.rbnotify_subscribers_job.rb
perform argumentsIDs or primitives(user_id), (article_id, options = {})
Queue namesLowercase noun:default, :critical, :mailers, :imports

8. Verification

Before finishing, verify:

  • Job inherits ApplicationJob, not ActiveJob::Base
  • perform receives IDs or primitives, not ActiveRecord objects
  • Domain logic lives on the model, not in perform
  • Job is not enqueued inside a database transaction — use after_commit
  • Job is idempotent — safe to run twice (guard clause or database uniqueness)
  • retry_on for transient errors, discard_on for permanent failures
  • Queue name matches worker configuration
  • Callers assert have_enqueued_job; the job's own spec calls perform_now (see writing-tests § Job Specs)
  • No perform_now in production paths

References

For detailed patterns and examples, see references/patterns.md.

Keep looking

Skills are one crate of 627,801. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.