Ruby on Rails
Persisting execution state to the database transforms Active Job continuations into native workflow orchestrators, eliminating the need for fragmented job chains and external platforms like Temporal.
AI-assisted summary of Durable orchestration on Rails: From continuation to workflow - Vladimir Dementyev — original source. Check the original for full context.
Source published:
Key ideas
- Active Job continuations, originally designed to help long-running tasks survive application deployments, are increasingly being adopted by the Rails community as an ad-hoc workflow orchestration primitive. (6:10)
- Standard Active Job lacks framework-level support for intermediate step observability, cancellation of future-scheduled jobs, and step-level callbacks, forcing teams to rely on fragmented multi-job chains or database state machines. (11:30)
- Vladimir Dementyev introduced active_job-durable_continuation (aj-dc), an open-source extension that brings database-backed execution persistence, step callbacks, declarative timers, and signal handling to Active Job. (30:30)
- By delivering 'just enough orchestration' through idiomatic Rails conventions, durable continuations allow entire multi-step pipelines and human-in-the-loop workflows to live within a single job class without requiring external engines like Temporal. (33:49)
Detailed notes
## Background Published on October 1, 2026, by the official Ruby on Rails YouTube channel, this conference talk features Vladimir Dementyev of Evil Martians presenting at a Rails World event sponsored by Shopify. The session explores how Active Job continuations evolved from a deployment resilience mechanism into an orchestration primitive, introducing durable continuations to model complex workflows within standard Rails job classes.
## TL;DR Vladimir examines how Rails developers traditionally tackle complex multi-job workflows like pipelines, batches, scheduled timers, and human-in-the-loop signals, often resulting in fragmented architectures or reliance on heavy external tools like Temporal. He argues that while Active Job continuations initially emerged to survive deployment restarts, grassroots community adoption has turned them into an orchestration tool that still lacks observability, step hooks, and uniqueness. To bridge this gap, he introduces durable continuations (`active_job-durable_continuation`), which persist execution state, cursors, and callbacks directly to a database. This lightweight, Rails-native model delivers 'just enough orchestration' to handle complex sequential, delayed, and agentic workflows inside a single cohesive Active Job class.
## Atmosphere & style The presentation carries a pragmatic, seasoned engineering tone infused with Vladimir's characteristic dry wit and deadpan humor, from venting about LLM reasoning nightmares to joking about failed proposal topics. The narrative flows smoothly from architectural war stories of broken deployments and fragmented workers toward an empowering, principled defense of 'batteries included' framework design. It feels like an authoritative masterclass in Rails architecture that demystifies distributed systems jargon into clean, intuitive Ruby code.
## Bulletin - [06:10](https://www.youtube.com/watch?v=4dcv1a7eeMk&t=370s) Active Job continuations, originally designed to help long-running tasks survive application deployments, are increasingly being adopted by the Rails community as an ad-hoc workflow orchestration primitive. - [11:30](https://www.youtube.com/watch?v=4dcv1a7eeMk&t=690s) Standard Active Job lacks framework-level support for intermediate step observability, cancellation of future-scheduled jobs, and step-level callbacks, forcing teams to rely on fragmented multi-job chains or database state machines. - [30:30](https://www.youtube.com/watch?v=4dcv1a7eeMk&t=1830s) Vladimir Dementyev introduced active_job-durable_continuation (aj-dc), an open-source extension that brings database-backed execution persistence, step callbacks, declarative timers, and signal handling to Active Job. - [33:49](https://www.youtube.com/watch?v=4dcv1a7eeMk&t=2029s) By delivering 'just enough orchestration' through idiomatic Rails conventions, durable continuations allow entire multi-step pipelines and human-in-the-loop workflows to live within a single job class without requiring external engines like Temporal.
## Key takeaways - [04:14](https://www.youtube.com/watch?v=4dcv1a7eeMk&t=254s) Active Job continuations were introduced in Rails primarily as a resilience mechanism to let long-running jobs be safely interrupted and resumed across deployments without duplicating side effects. - [06:10](https://www.youtube.com/watch?v=4dcv1a7eeMk&t=370s) Community pull requests demonstrate that developers have repurposed continuations into an orchestration primitive for complex, multi-step operations like LLM pipelines. - [08:22](https://www.youtube.com/watch?v=4dcv1a7eeMk&t=502s) Traditional job chains fragment business logic across multiple distinct job classes, obscuring execution flow, branching logic, and error handling. - [11:30](https://www.youtube.com/watch?v=4dcv1a7eeMk&t=690s) Native continuations store execution state only ephemerally in queue payloads, preventing external callers or monitoring tools from inspecting current step progress or execution history. - [12:26](https://www.youtube.com/watch?v=4dcv1a7eeMk&t=746s) While gems like AcidicJob and Geneva Drive offer durable execution, they either operate outside core Active Job conventions or require adopting foreign domain-specific languages. - [13:42](https://www.youtube.com/watch?v=4dcv1a7eeMk&t=822s) Solid Queue 1.7 introduced native Active Job batches, but parallel fan-out steps cannot yet be coordinated natively inside Active Job continuations. - [16:57](https://www.youtube.com/watch?v=4dcv1a7eeMk&t=1017s) Time-delayed background jobs suffer from invalidation challenges because Active Job provides no standard API to cancel or update scheduled jobs when domain state changes. - [20:26](https://www.youtube.com/watch?v=4dcv1a7eeMk&t=1226s) Waiting for external signals such as inbound webhooks or human approvals typically fractures cohesive workflows into disjointed controllers, mailers, and workers. - [25:35](https://www.youtube.com/watch?v=4dcv1a7eeMk&t=1535s) Durable continuations save workflow state and step transitions directly into database records, ensuring that execution survives worker crashes and restarts deterministically from checkpoints. - [28:41](https://www.youtube.com/watch?v=4dcv1a7eeMk&t=1721s) Idiomatic lifecycle hooks such as after_step callbacks and halt_on handlers allow developers to park recoverable workflows without permanently dropping them like discard_on does. - [28:41](https://www.youtube.com/watch?v=4dcv1a7eeMk&t=1721s) Declarative step timers and Active Record model associations allow long-running lifecycle schedules to reside in a single class while resetting cleanly on domain updates. - [30:30](https://www.youtube.com/watch?v=4dcv1a7eeMk&t=1830s) The wait_step primitive enables human-in-the-loop patterns and agentic conversational loops by pausing execution until a controller signal releases the step or a timeout expires. - [33:49](https://www.youtube.com/watch?v=4dcv1a7eeMk&t=2029s) The active_job-durable_continuation gem provides a lightweight, Rails-native alternative to heavy external workflow engines by providing 'just enough orchestration' inside standard Active Job classes.
## Detailed walkthrough ### [00:10](https://www.youtube.com/watch?v=4dcv1a7eeMk&t=10s) From Action Cable to Background Jobs
Vladimir Dementyev takes the stage, opening with warm thanks to Amanda, the Rails Foundation, and the event organizers for putting together what he jokes could be a "once-in-a-lifetime, twice-in-a-lifetime, or maybe last-in-a-lifetime event and experience." Introducing himself, Vladimir shares that he has been building software with Ruby on Rails and actively contributing to the framework for more than a decade. Over that time, he has authored numerous gems and published a book on Rails architecture. His most recent contributions to core Rails centered on re-architecting Action Cable for upcoming releases to cleanly support alternative server backends—most notably Async Cable—without relying on monkey-patches, alongside a suite of performance and architectural improvements. However, since that particular talk proposal was not selected, he quips that he has to leave Action Cable behind and move to a different topic entirely.
Vladimir introduces his day job as backend lead at Evil Martians, directing a team of engineers helping startups and scaled enterprise companies build software with higher reliability. Most recently, Evil Martians partnered with the Rails Foundation on the Rails AI benchmarks project, which has consumed his past month to the point where his "nightmares are full of LLM session transcripts full of shitty reasoning." Shifting gears away from his own job, he announces the true subject of the presentation: the audience's jobs—specifically, background jobs in Rails.
[02:39](https://www.youtube.com/watch?v=4dcv1a7eeMk&t=159s) The Evolution of Active Job (Rails 4.2 to 8.2)
Looking back over the decade from Rails 4.2 to 8.2, Vladimir traces the evolution of Active Job from its inception in 2014. That was the same year he shipped his very first production Rails application. When Active Job was initially unveiled, his reaction was lukewarm: while it was great that Rails had added background job support out of the box, it felt like nothing more than a thin compatibility layer wrapped around existing tools like Sidekiq and Resque without introducing any novel features. In fact, that original 2014 monolith is still running in production today as a massive enterprise application, and it still does not use Active Job.
Vladimir points out that many legacy applications remain stuck using adapter-specific primitives directly, such as raw Sidekiq workers. He strongly encourages developers to migrate to Active Job because it is no longer just a generic interface facade. Over the past decade, Active Job has steadily matured, introducing foundational abstractions of its own. In the past year, Active Job added a concept that did not exist in any underlying background engine: continuations.
- Active Job was introduced in Rails 4.2 (2014) as a unified wrapper over gems like Sidekiq, Resque, and Delayed Job. - For years, it lagged behind native engine capabilities and offered little reason for mature applications to migrate away from native workers. - The recent arrival of Active Job continuations marked the first time Rails introduced a novel execution primitive that did not originate in the underlying queue engines.
[04:14](https://www.youtube.com/watch?v=4dcv1a7eeMk&t=254s) Continuations: Resilience and Surviving Deployments
Continuations were introduced into Active Job strictly as an infrastructure resilience mechanism. Their intended goal was to make long-running jobs interruptible and resumable by breaking execution into discrete steps tracked by checkpoints and cursors. As documented in the original pull request, the primary driving motivation was surviving application deployments.
To illustrate the problem, Vladimir presents a standard webhook dispatch worker iterating over thousands of records. In a traditional worker without continuations, if a deployment rolls out mid-iteration, the worker process is dropped. When the job processing engine retries the job later, the iteration restarts from the very beginning, leading to duplicate webhook deliveries. For years, Rails developers worked around this by splitting the logic into multiple files: a parent orchestrator job that iterated over records and enqueued hundreds of tiny, atomic per-record jobs. While this fan-out workaround was robust, it was verbose and fragmented cohesive business logic across multiple classes. Continuations solve this by allowing the entire resumable loop—with its cursors and checkpoints—to live inside a single class while ensuring that mid-deploy restarts resume precisely where they left off.
```mermaid flowchart TD accTitle: Deployment Interruption Comparison accDescr: Contrasts traditional background job restart behavior against continuation-based resumption during deployments.
subgraph Traditional["Traditional Background Job"] T1["Process Records 1..50"] --> T_DEP["Deployment / Process Killed"] T_DEP --> T2["Engine Retries Job"] T2 --> T3["Restarts at Record 1<br>(Duplicate Side-Effects)"] end
subgraph Continuations["Active Job Continuation"] C1["Step 1: Checkpoint Saved"] --> C_DEP["Deployment / Process Killed"] C_DEP --> C2["Engine Retries Job"] C2 --> C3["Resumes from Checkpoint<br>(Zero Duplicates)"] end ```
- Without continuations, killed long-running jobs restart from zero, risking duplicate side effects like duplicate outgoing webhooks. - The standard multi-job workaround (orchestrator job enqueuing individual atomic record jobs) adds boilerplate and spreads cohesive logic across multiple files. - Active Job continuations preserve idempotency and resilience within a single job file by persisting cursors as execution checkpoints.
[06:10](https://www.youtube.com/watch?v=4dcv1a7eeMk&t=370s) An Accidental Orchestration Primitive
Paired with Solid Queue, Active Job is now defining modern conventions for background processing in Ruby on Rails. Examining recent community pull requests against Active Job, Vladimir highlights that roughly half of them focus on continuations, submitted not by Rails Core or framework maintainers, but by third-party community developers. This reveals a rapid, grassroots adoption of the feature just one year after its release.
More importantly, how the community actually uses continuations has diverged completely from the authors' original intent. Developers are not reaching for continuations simply to survive deploy restarts; they are using them to coordinate long-running, multi-step business operations requiring isolated state or execution fairness guarantees. Vladimir shares a snippet he relies on in modern AI projects where multi-phase LLM workflows are split into individual continuation steps, allowing worker processes to interleave steps and distribute heavy workloads fairly across the pool. Consequently, a feature created as an infrastructure workaround has effectively transformed into an orchestration primitive—yet it remains too primitive to handle complex workflow patterns cleanly.
[08:22](https://www.youtube.com/watch?v=4dcv1a7eeMk&t=502s) Pipelines and Job Sequences: Why the Primitive Falls Short
To demonstrate the shortcomings of continuations as an orchestration tool, Vladimir examines pipelines and job sequences—common architectural patterns used for file ingestion, ETL data imports, and external synchronization. The standard implementation relies on a chain of distinct job classes: `Job A` executes, and upon completion enqueues `Job B`, which on success enqueues `Job C`, with error handling and branching logic scattered across callbacks. Within a month of writing such a chain, nobody on the team can trace where the flow begins, what path it follows, or how failure states branch out.
Refactoring that pipeline into a single continuation job brings all sequential steps into one file, drastically improving readability. However, major architectural limitations immediately surface:
```mermaid flowchart LR accTitle: Job Chains versus Single-Job Continuation Pipelines accDescr: Compares scattered multi-job pipelines against unified continuation steps inside a single job.
subgraph Chain["Traditional Job Chain (Scattered)"] J1["ExtractJob"] -->|"enqueues"| J2["TransformJob"] J2 -->|"enqueues"| J3["LoadJob"] end
subgraph Continuation["Continuation Job (Single File)"] direction TB S1["step:extract"] --> S2["step:transform"] S2 --> S3["step:load"] end ```
First, to pass data between continuation steps, developers are forced to persist intermediate step states onto database models, turning Active Record records into makeshift orchestration state machines. Second, alternative patterns that use libraries like `activejob-performs` (such as those used in Evil Martians' AnyCable Plus project) introduce sprawling state machine definitions just to re-enqueue a single job across distinct methods.
While Active Job's new attributes API allows keeping ephemeral state inside the job payload across continuations, that payload state remains completely inaccessible to external callers or monitoring tools. Finally, Active Job lacks control-flow step hooks: there are no per-step callbacks (such as `after_step`) and no step-level rescue handlers. Developers must manually write boilerplate—such as invoking Action Cable `broadcast_update` calls—inside every individual step block.
[11:30](https://www.youtube.com/watch?v=4dcv1a7eeMk&t=690s) Limitations of Continuations: Observability and Uniqueness
While continuations offer better developer experience and improve the readability of complex multi-step processes, they still lack critical framework-level capabilities today. The primary missing piece is observability. Because native continuations do not maintain persistent state, there is no way to inspect which step a job is currently executing, how long it has been running, or what the execution history looks like across individual steps.
Beyond step observability and retry logic, long-running workflows inevitably run into the problem of uniqueness constraints. In real-world orchestration, you rarely want duplicate runs of the same multi-step workflow enqueued simultaneously while a previous run is still actively working. Ideally, attempting to enqueue a duplicate should either be rejected or trigger a cancellation and re-enqueue of the active job. However, Active Job provides no native, framework-level API to enforce uniqueness or manage cancellations. While specific queue backends offer proprietary locks or uniqueness add-ons, the lack of a standardized Active Job primitive forces developers to roll their own solutions.
[12:26](https://www.youtube.com/watch?v=4dcv1a7eeMk&t=746s) Existing Workflow Gems: AcidicJob and Geneva Drive
To fill these gaps, the Ruby ecosystem has produced several third-party workflow engines over the years. One notable solution is AcidicJob, which Vladimir notes having used across several client projects. AcidicJob introduces durable state by writing every execution transition directly to the database. This allows developers to restore state, inspect progress, and rely on built-in idempotency and uniqueness guards. Crucially, AcidicJob preserves the familiar Active Job interface: external callers enqueue standard jobs, while the durability mechanics and workflow state remain cleanly encapsulated inside the job class.
Another notable modern entry is the Geneva Drive project. Vladimir praises Geneva Drive for providing virtually every feature expected from a mature workflow engine. However, its primary downside from a Rails perspective is that it operates as an entirely separate abstraction:
> "It's not a Rails thing. It's a custom DSL, and it's a custom entity. It's not strictly compatible with Active Job, so you need to refactor everything to use it."
While Geneva Drive provides robust workflow guarantees, Rails applications benefit far more from tools that integrate directly into the existing Active Job primitives rather than requiring a wholesale architecture rewrite.
[13:42](https://www.youtube.com/watch?v=4dcv1a7eeMk&t=822s) The Batching Pattern and Solid Queue Batches
The next fundamental orchestration pattern is batching: a setup job initializes work, fans out into multiple parallel child jobs, and coordinates a final callback once all child jobs finish. While batches can technically be processed sequentially using continuation cursors—as seen in real-world Spree codebase examples processing CSV rows—batch processing in practice almost always implies parallel execution.
```mermaid flowchart TD accTitle: Batch Coordination Architecture accDescr: Setup job fans out to multiple parallel worker jobs, which report to a coordinator before triggering completion.
subgraph BatchStage["Batch Fan-out & Execution"] Setup["Setup Job"] --> JobA["Worker Job A"] Setup --> JobB["Worker Job B"] Setup --> JobC["Worker Job C"] end
subgraph CoordStage["Coordination & Completion"] JobA --> Coord["Coordinator / Counter"] JobB --> Coord JobC --> Coord Coord --> Complete["On Success / On Failure Job"] end ```
Parallel fan-out introduces the hard problem of coordination. To know when all children have completed, systems traditionally rely on atomic Redis counters or recurring polling sweeps that periodically re-check whether every worker is finished. Historically, this demanded specialized third-party gems, but Solid Queue 1.7 introduced native batch support—a massive achievement credited to outside contributor JP Camara alongside maintainer Rosa Gutiérrez:
- **Solid Queue Batches:** Built entirely around standard Active Job conventions, allowing explicit completion hooks like `on_success` and `on_failure` callback jobs. - **GoodJob Influence:** The API draws architectural inspiration from GoodJob, which has supported native database-backed batches for some time. - **Doximity's Simplekiq:** A Sidekiq-based gem highlighting a syntax very close to Active Job continuations, featuring sequential steps mixed with explicit parallel step blocks. - **Framework Gap:** Parallel steps remain missing from native Active Job continuations because Active Job itself lacks an upstream batching primitive, meaning developers must wait for framework-level batch APIs before continuations can natively coordinate fan-out.
[16:57](https://www.youtube.com/watch?v=4dcv1a7eeMk&t=1017s) Timers, Delays, and the Pain of Scheduled Jobs
Orchestration frequently involves timers, delays, and schedule-driven background tasks such as expiration notices, periodic reminders, and follow-up notifications. In an example from AnyCable's license expiration logic, a single domain workflow ended up fragmented across numerous recurring polling jobs. A cron job ran every ten minutes or hourly just to find expiring records and enqueue follow-up background jobs, introducing unnecessary structural boilerplate for what should be a unified linear flow.
While continuations allow consolidating iterative polling sweeps into a single job class via cursors, developers still struggle when attempting to "park" a job into the future using Active Job's `wait:` or `wait_until:` parameters. The core issue is invalidation:
> "The first problem is that we usually need to invalidate—'Oh, is this job still relevant?'—because there is no, at least Active Job level, API to cancel a job scheduled for the future."
Reaching into adapter-specific APIs to purge scheduled job payloads is brittle, driving most teams back to blunt cron sweeps. Several gems try to improve this: - **Conductor / ChronoForge:** Extends Active Job with DSL decorators like `wait`, allowing an execution to pause at a specific line, record itself in a database table, and re-enqueue seamlessly when the deadline arrives. - **Geneva Drive Timers:** Offers step-level timer configurations and associates workflows directly to ActiveRecord models using handles, providing clean lookup APIs to cancel pending timers whenever records update. - **Temporal:** Takes a durable workflow approach where appointment reminder deadlines can simply be modified in-flight as state updates, avoiding the need to cancel and restart workflows from scratch when target dates shift.
[20:26](https://www.youtube.com/watch?v=4dcv1a7eeMk&t=1226s) Human-in-the-Loop, Signals, and Webhook Workflows
A broader class of delayed workflows revolves around external signals: events that pause execution not for an elapsed duration, but for an external stimulus such as human approval or an inbound webhook. In typical Rails codebases, waiting for a signal forcefully fractures a single cohesive process into disconnected before-and-after fragments scattered across the application.
```mermaid flowchart LR accTitle: Signal-Driven Webhook Fragmentation accDescr: Comparison of a fragmented three-part webhook flow versus a consolidated continuation-style perform method.
subgraph Fragmented["Fragmented Architecture (Status Quo)"] direction TB J1["1. Request Withdrawal Job"] --> W1["2. External Webhook Controller"] W1 --> J2["3. Process Completion Job"] end
subgraph Unified["Unified Continuation Architecture"] direction TB Step1["Step 1: Request withdrawal"] --> Step2["Step 2: wait_for_signal (payment_receipt)"] Step2 --> Step3["Step 3: Credit account"] end ```
In Mastodon, approval flows are split across separate controllers, mailers, and workers, requiring manual ad-hoc cleanup jobs to purge unapproved tasks that never receive their signal. Similarly, in a withdrawal workflow Vladimir encountered in production, the logic was fragmented across three separate files: an initial withdrawal request job, an HTTP webhook controller receiving the vendor's confirmation, and a third background job applying balance credits.
Tools supporting signal primitives—such as Conductor with its `continue_if` step—allow developers to consolidate that entire distributed transaction into a single readable `perform` method: run step one, pause execution until the payment receipt signal arrives, and execute step three. Yet despite the clear readability benefits of writing this inside Ruby, teams frequently bypass Rails entirely and reach for heavy external orchestrators like Temporal. Vladimir laments this trend, arguing that these business flows could easily be expressed with native Rails ergonomics if the framework provided durable primitives.
[22:39](https://www.youtube.com/watch?v=4dcv1a7eeMk&t=1359s) The Philosophy of Durable Continuations
Writing workflows that can be described in plain English in just a few words should not require "hundreds lines of code and dozens of Ruby objects and scary like voodoo magic to make them all work." Yet as workflows grow more sophisticated, Rails developers are increasingly forced to reach for heavy, alien orchestration tooling. This trajectory runs contrary to the core Rails ethos of having "batteries included." Rather than relying on separate orchestrators, external services, or cumbersome proprietary SDKs, Active Job's existing continuation concept can be extended directly into a full-fledged workflow engine.
A year prior, Vladimir floated the idea of evolving continuations into a true orchestration primitive without adding custom gems or bloated external abstractions. The outcome of that concept is **durable continuations** for Active Job. While industry buzzwords—frequently pushed by products like Temporal—often dress durability up as mysterious, advanced tech, the underlying principle is simple: the execution state is saved as records in a database or dedicated store rather than existing purely as a transient payload in a message queue. Surviving hard kills (`SIGKILL`) and node restarts naturally follows once execution state is committed, restorable, and replayable. Crucially, any such extension must adhere strictly to "the Rails way"—feeling like a natural part of the framework that could conceivably be upstreamed into Rails core rather than functioning as a foreign third-party add-on.
```mermaid flowchart LR accTitle: Transient Queue Payload vs Durable Database Execution accDescr: Comparison showing how transient queue payloads lose state on crashes while durable continuations persist execution state to a database. subgraph Traditional["Standard Active Job"] direction TB Q["Queue Payload"] --> W["Worker Memory"] W -.->|"SIGKILL / Crash"| Lost["State Lost"] end subgraph Durable["Durable Continuation"] direction TB DB[("Execution Run DB")] <--> WR["Worker Execution"] WR -->|"Checkpoint Step"| DB DB -->|"Crash Recovery"| Resume["Resume from DB State"] end ```
---
[25:35](https://www.youtube.com/watch?v=4dcv1a7eeMk&t=1535s) Persisting State and Resuming Execution
To illustrate how durable execution works in practice, consider an avatar and card generation pipeline from a conference app. Generating customized attendee cards requires sequentially processing original photo uploads, moderating content against inappropriate uploads, and finally generating the finished visual assets. In standard Active Job continuations, state tracking across these intermediate phases is largely ephemeral.
Under durable continuations, every job execution run maps to a dedicated database record that tracks overall status, individual steps, and arbitrary run-level attributes:
* **Enqueued State:** The moment a job is enqueued, a record is written to the database; the system already recognizes the job's existence and planned arguments before worker execution begins. * **In-Progress Execution:** As background workers pick up the job, the run status updates to running, and each step's progress and results are committed sequentially. * **External Observability:** Persisted job attributes remain readable by outside processes throughout execution, allowing other application features to query run data, track percent completion, or trigger downstream operations. * **Direct Inspection and Control API:** Similar to the developer experience of tools like GoodJob, jobs expose an intuitive API to query execution state by identifiers (inferred automatically from arguments or explicitly declared). Runs can be inspected, canceled, or manually retried.
This model significantly hardens resumability. In an un-durable webhook dispatcher job that iterates through subscribers, an unexpected hard crash or unhandled exception between steps will restart the job from the very beginning if the progress cursor was never committed to a database. Under durable continuations, the cursor and step metadata are committed to the database, ensuring that when the worker restarts, execution picks up exactly where it was interrupted.
---
[28:41](https://www.youtube.com/watch?v=4dcv1a7eeMk&t=1721s) Rails Ergonomics: Callbacks, Halting, and Timers
Durable continuations elevate workflow definitions from bare-bones background wrappers into rich, first-class Rails objects. Instead of cluttering the bodies of individual step methods with logging, metrics, or auxiliary notifications, developers can leverage familiar lifecycle hooks:
* **`after_step` Callbacks:** Provide idiomatic Rails-style hooks to run cross-cutting logic cleanly after any step finishes. * **`halt_on` Handler:** Unlike Active Job's standard `discard_on` (which permanently drops the entire job upon failure), `halt_on` gracefully parks the workflow after a specific step. If the underlying issue is recoverable, the parked workflow can be cleanly resumed once the environment or data is repaired. * **Built-in Uniqueness:** Because each active execution run is stored in the database, workflow uniqueness can be enforced natively without third-party Redis locks or custom plugins.
```mermaid flowchart TD accTitle: Durable Step Lifecycle and Error Handling accDescr: Flow diagram showing step execution, after_step callbacks, halt_on error handling, and resumption. Start["Job Step Invoked"] --> Run{"Execute Step"} Run -->|"Success"| CB["Run after_step Callbacks"] CB --> Next["Advance to Next Step"] Run -->|"Recoverable Error"| Halt["halt_on: Park Workflow"] Halt -->|"Condition Resolved"| Resume["job.resume"] Resume --> Run ```
Timers and deferred scheduling also condense dramatically. Traditional multi-stage schedules—such as a software license expiration reminder lifecycle—historically required external cron managers triggering multiple independent job classes. With durable continuations, the entire multi-month sequence lives inside a single class using declarative step delays:
* Steps support explicit timing constraints such as `wait:` (duration) and `wait_until:` (specific timestamp). * If a customer renews their subscription mid-lifecycle, re-enqueuing the unique job automatically replaces the pending schedule and resets the lifecycle workflow. * ActiveRecord models can integrate directly via helpers like `has_one_performed`, exposing seamless associations between domain records and their associated durable background runs.
---
[30:30](https://www.youtube.com/watch?v=4dcv1a7eeMk&t=1830s) Signals, Human-in-the-Loop, and the Open-Source Release
Complex asynchronous orchestrations often require waiting on external events or manual human intervention. In heavyweight workflow engines like Temporal, implementing signals and pauses requires coordinating multiple external concepts and verbose SDK boilerplate. Durable continuations condense this into standard, readable Ruby code.
In a moderation pipeline where an AI image generator yields a borderline safety score, automated processing must pause until a human reviewer manually approves or rejects publishing the badge to social ads. Traditional Rails implementations lean on messy status enums, polling loops, or fragmented state machines that struggle with concurrency, lack execution history, and cannot enforce review deadlines:
```mermaid flowchart LR accTitle: Human in the Loop Approval Flow accDescr: Pipeline workflow showing automated moderation, a parked wait step awaiting approval, and subsequent controller signal resumption. Mod["Moderation Check"] --> Review{"Borderline Score?"} Review -->|"Yes"| WaitStep["wait_step: Parks Job Execution"] WaitStep -->|"Timeout Exceeded"| Expired["Handle Timeout / Fallback"] Review -->|"No"| Gen["Generate Assets"] Controller["Admin Controller Signal"] -.->|"Approve / Release"| WaitStep WaitStep --> Gen ```
* **`wait_step` Primitive:** Parks the job for a configurable timeout period using empty-step timer mechanisms. If no input arrives before the deadline, it releases automatically. * **Signal Dispatch:** When an administrator approves the action, a simple controller call emits a signal to the waiting execution run, releasing the park and resuming downstream steps immediately. * **Agentic Workflows (RubyLLM 2.0):** Emerging conversational workflows in libraries like RubyLLM frequently require human-in-the-loop loops for approving tool calls or external actions. Instead of juggling disparate background workers, the entire conversational turn loop can be halted with `halt_on` and resumed on approval. While chat messages hold the application-level state, durable continuations manage the underlying control flow state.
Vladimir wraps up by introducing the open-source gem: `active_job-durable_continuation` (or `aj-dc`—humorously clarifying it has "nothing related to music"). Beyond standard workflow features, the gem bundles ready-made AI development skills powered by Rails Hyperdrive, allowing coding agents to inspect existing Rails jobs, identify migration candidates, and refactor traditional background workers into durable continuations automatically.
[33:49](https://www.youtube.com/watch?v=4dcv1a7eeMk&t=2029s) The Rails Way: Bringing "Just Enough Orchestration" to Active Job
Vladimir concludes by encouraging attendees to review the implementation details in their own time, deadpanning that he certainly does not "recommend not reading code." The driving philosophy behind introducing durable continuations to Active Job is pragmatic restraint: delivering "just enough orchestration" so teams can stay true to the Rails way. Rather than taking on the operational overhead of heavy external orchestration engines or introducing cryptic gems to stitch together complex multi-job flows, Rails developers can solve these problems natively within the framework they already know, creating solutions that are cleaner, simpler, and far better integrated.
With additional workflow features already on the roadmap, Vladimir leaves the audience with a clear call to action. He urges engineers to pay closer attention to Active Job continuations and inspect their existing codebases for brittle multi-job patterns—such as manual pipelines, batching hacks, or ad-hoc polling loops—that would be better modeled as cohesive workflows. By consolidating that logic into durable workflows, developers can make long-running processes reliable, deploy-safe, and straightforward to maintain.
```mermaid flowchart LR accTitle: Adopting Durable Workflows in Rails accDescr: Three-step progression from identifying fragmented jobs to adopting durable workflows natively in Active Job. A["Identify Multi-Job Patterns<br/>(Pipelines, Batches, Timers)"] --> B["Extract into Workflows<br/>(Stay on the Rails Way)"] B --> C["Leverage Durable Continuations<br/>(Active Job Primitive)"] ```
- **Avoid External Sprawl:** Eliminates the need to reach for hosted workflow platforms or specialized, unfamiliar gems for standard background job orchestration. - **Audience Takeaway:** Audit existing background job chains, look for hidden orchestration needs, and experiment with durable continuations to keep full end-to-end workflows contained within standard job classes.
## Notable quotes > [00:10](https://www.youtube.com/watch?v=4dcv1a7eeMk&t=10s) once-in-a-lifetime, twice-in-a-lifetime, or maybe last-in-a-lifetime event and experience — Vladimir Dementyev
> [00:10](https://www.youtube.com/watch?v=4dcv1a7eeMk&t=10s) nightmares are full of LLM session transcripts full of shitty reasoning — Vladimir Dementyev
> [12:26](https://www.youtube.com/watch?v=4dcv1a7eeMk&t=746s) It's not a Rails thing. It's a custom DSL, and it's a custom entity. It's not strictly compatible with Active Job, so you need to refactor everything to use it. — Vladimir Dementyev
> [16:57](https://www.youtube.com/watch?v=4dcv1a7eeMk&t=1017s) The first problem is that we usually need to invalidate—'Oh, is this job still relevant?'—because there is no, at least Active Job level, API to cancel a job scheduled for the future. — Vladimir Dementyev
> [22:39](https://www.youtube.com/watch?v=4dcv1a7eeMk&t=1359s) hundreds lines of code and dozens of Ruby objects and scary like voodoo magic to make them all work — Vladimir Dementyev
> [22:39](https://www.youtube.com/watch?v=4dcv1a7eeMk&t=1359s) batteries included — Vladimir Dementyev
> [30:30](https://www.youtube.com/watch?v=4dcv1a7eeMk&t=1830s) nothing related to music — Vladimir Dementyev
> [33:49](https://www.youtube.com/watch?v=4dcv1a7eeMk&t=2029s) recommend not reading code — Vladimir Dementyev
> [33:49](https://www.youtube.com/watch?v=4dcv1a7eeMk&t=2029s) just enough orchestration — Vladimir Dementyev
## Mentioned Vladimir Dementyev, Amanda, Rails Foundation, Shopify, Ruby on Rails, Action Cable, Async Cable, Evil Martians, Active Job, Solid Queue, Sidekiq, Resque, Delayed Job, activejob-performs, AnyCable, AnyCable Plus, AcidicJob, Geneva Drive, Spree, JP Camara, Rosa Gutiérrez, GoodJob, Doximity, Simplekiq, Conductor, ChronoForge, Temporal, Mastodon, Active Record, RubyLLM, Rails Hyperdrive, active_job-durable_continuation
## Video description Active Job continuations were built to help long-running jobs survive deploys. Now they're becoming an orchestration primitive. Vladimir Dementyev (Evil Martians) looks at common multi-job patterns like pipelines, batches, timers and human-in-the-loop approvals, and at the tools Rails developers reach for today. He then introduces durable continuations for Active Job, a Rails-way extension that adds persisted state, step callbacks, uniqueness, timers and signals, so an entire workflow can live in a single job class.
***
Thank you Shopify for sponsoring the editing and post-production of these videos. Check out insights from the Engineering team at: https://shopify.engineering/
#RailsWorld #RubyOnRails #ActiveJob #BackgroundJobs #Workflows #SolidQueue
[Source](https://www.youtube.com/watch?v=4dcv1a7eeMk)