Techdots

September 10, 2025

Rails Active Job Continuations: How to Chain Background Jobs the Right Way

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!

Ready to Launch Your AI MVP with Techdots?

Techdots has helped 15+ founders transform their visions into market-ready AI products. Each started exactly where you are now - with an idea and the courage to act on it.

Techdots: Where Founder Vision Meets AI Reality

Book Meeting