Rails Active Job Continuations: How to Chain Background Jobs the Right Way
Have you ever struggled with running one background job after another in Rails? What if you need to generate a report first, then email it to users? Or import a CSV file and notify users only when it's done?
These are common problems in Rails applications. Until now, developers had to write complex code to handle job dependencies. But Rails 7.1 introduced a game-changing feature: Rails Active Job continuations.
This feature makes chaining background jobs in Rails incredibly simple. Instead of writing messy callback code, you can now chain jobs with a single .then() method. Let's explore how this works and why it's so powerful.
What Are Rails Active Job Continuations?
Rails Active Job continuations let you chain jobs together in a clean, readable way. Here's the basic syntax:
ruby
MyFirstJob.perform_later(args).then(MySecondJob)
That's it! The second job will only run after the first one completes successfully. This feature is part of Rails 7.1 and makes job orchestration much simpler.
Why Use Job Chaining?
You should consider Rails job dependencies when you have:
- Sequential tasks: Like generating a PDF then emailing it
- Complex workflows: Multi-step processes that must happen in order
- Clean code goals: Want to avoid messy callback code
- Reliable processing: Need guaranteed execution order
The Active Job then method solves these problems elegantly.
How to Use Rails Active Job Continuations
Let's build a real example step by step.
Step 1: Create Your Jobs
First, generate two jobs using Rails generators:
rails g job generate_report
rails g job email_report
Step 2: Write the First Job
# app/jobs/generate_report_job.rb
class GenerateReportJob < ApplicationJob
queue_as :default
def perform(user_id)
user = User.find(user_id)
# Generate the report for the user
report = ReportGeneratorService.generate_for(user)
# Pass the report ID to the next job
EmailReportJob.perform_later(user.id, report.id)
end
end
Step 3: Write the Second Job
-->Step 4: Chain Them Together
Now comes the magic part:
GenerateReportJob.perform_later(current_user.id)
.then(EmailReportJob)
That's all you need! The first job generates the report, and the second job emails it automatically.
How Data Flows Between Jobs
Here's a crucial point: Rails Active Job continuations pass the return value from the first job to the second job automatically.
In our example:
- GenerateReportJob#perform returns report.id
- EmailReportJob#perform(report_id) receives that ID as a parameter
This makes the data flow simple and predictable:
class GenerateReportJob < ApplicationJob
queue_as :default
def perform(user_id)
user = User.find(user_id)
report = ReportGeneratorService.generate_for(user)
# Manually enqueue next job with return value
EmailReportJob.perform_later(report.id)
end
end
class EmailReportJob < ApplicationJob
queue_as :default
def perform(report_id)
report = Report.find(report_id)
ReportMailer.send_report(report).deliver_now
end
end
Important: Always return data that can be serialized (like numbers, strings, or simple hashes).
Error Handling with Job Continuations
What happens if something goes wrong? Rails Active Job continuations have smart error handling:
- First job fails: The second job won't run at all
- Second job fails: Only the second job retries (if configured)
- Retries: Work normally for each individual job
You can configure retries like this:
class GenerateReportJob < ApplicationJob
# Retry the job up to 3 times if a StandardError is raised
retry_on StandardError, attempts: 3
def perform(user_id)
user = User.find(user_id)
report = ReportGeneratorService.generate_for(user)
EmailReportJob.perform_later(user.id, report.id)
end
end
Real-World Example: CSV Import and Notifications
Here's a practical example many developers face:
class ImportCsvJob < ApplicationJob
queue_as :default
def perform(user_id, csv_path)
result = CsvImporter.new(csv_path).run
# Explicitly hand off to the next job
NotifyUserJob.perform_later(
user_id: user_id,
success_count: result.success_count
)
end
end
class NotifyUserJob < ApplicationJob
queue_as :default
def perform(user_id:, success_count:)
user = User.find(user_id)
UserMailer.import_complete(user, success_count).deliver_later
end
end
ImportCsvJob.perform_later(current_user.id, upload_path).then(NotifyUserJob)
Clean, simple, and reliable. The user only gets notified if the import succeeds.
Alternatives to Job Continuations
Before Rails 7.1, developers used these approaches:
Option 1: Manual Job Calling
class SecondJob < ApplicationJob
def perform(user_id:, success_count:)
user = User.find(user_id)
UserMailer.import_complete(user, success_count).deliver_later
end
end
Problems:
- Hard to test
- Less reusable
- No automatic sequencing
Option 2: Workflow Gems
Complex workflow gems like Railway or Trailblazer work well for:
- Complex branching logic
- Stateful workflows
- User interaction mixed with background jobs
But for simple job chaining, Rails Active Job continuations are much cleaner.
Testing Your Job Chains
Testing chained jobs is straightforward:
# app/jobs/generate_report_job.rb
class GenerateReportJob < ApplicationJob
def perform(user_id)
user = User.find(user_id)
report = ReportGeneratorService.generate_for(user)
EmailReportJob.perform_later(user.id, report.id)
end
end
Most of the time, you'll test the end result (like checking if an email was sent).
Advanced Features
Chaining Multiple Jobs
You can chain more than two jobs:
class FirstJob < ApplicationJob
def perform(args)
result = HeavyWorkService.run(args)
SecondJob.perform_later(result)
end
end
class SecondJob < ApplicationJob
def perform(result)
processed = OtherService.process(result)
ThirdJob.perform_later(processed)
end
end
class ThirdJob < ApplicationJob
def perform(processed)
FinalizerService.finish(processed)
end
end
Each job passes its return value to the next one in line.
Passing Complex Data
Need to pass multiple values? Return a hash:
def perform(user_id)
# ... do work ...
{ user_id: user_id, result_count: 42, status: 'completed' }
end
Frequently Asked Questions
Q: Do continuations work with Sidekiq or other job backends?
Yes! Since continuations are part of ActiveJob, they work with any supported backend.
Q: Can I pass multiple arguments to the second job?
No. The continuation receives one argument: the return value from the first job. Use a hash for multiple values.
Q: What if the second job fails?
It behaves like any normal job. Your retry and error handling work as expected. The first job isn't affected.
Q: Can I use this in older Rails versions?
No, you need Rails 7.1 or newer. For older versions, you'll need manual solutions.
Conclusion
Rails Active Job continuations transform how we handle chaining background jobs in Rails. The Active Job then method provides a clean, reliable way to manage Rails job dependencies without complex orchestration code. This feature makes your workflows more maintainable and easier to understand.
Ready to modernize your Rails application with better job management? Techdots can help you implement these advanced Rails features and optimize your background job workflows. Contact us today!