Ductwork Documentation

Durable workflows for Ruby

Ductwork is a durable execution worflow orchestration engine and framework. You define a workflow as a series of steps; Ductwork runs them for you, on processes and threads it supervises inside your own application, and persists every step and every state transition to your relational database before acting on it. When a worker, a host, or the database itself goes away mid-flight, the work isn’t lost. It resumes from the last committed state, automatically, with no message broker and no operator intervention.

What Makes Ductwork Different

It runs your workflows; it isn’t just a way to describe them. Ductwork supervises its own worker and advancer processes alongside your Rails app. You trigger a pipeline; Ductwork claims, executes, and advances each step. There is no separate scheduler or runtime to stand up.

Your database is the source of truth. Work is rows, not in-memory messages or broker queues. A triggered pipeline writes durable run, branch, step, and job records. No work exists only inside a running process, so a crash can’t lose it.

Automatic recovery from process and host failure. Every worker heartbeats. When one stops, a reaper re-enqueues its in-flight work and a healthy worker picks it up. Fencing tokens and guarded commits ensure a recovered “zombie” process can never corrupt the work that moved on without it.

At-least-once execution you can reason about. Every step is guaranteed to run; infrastructure failures (OOM, spot reclaim, deploy) never consume a step’s application retry budget. Make steps idempotent (Ductwork gives you Step#idempotency_key) and the durability contract is simple and explicit.

No new infrastructure. No Kafka, no Temporal server, no separate worker tier to operate. If you have a Rails app and a relational database, you have everything Ductwork needs.

An Example Workflow

Each step below is a checkpoint. Ductwork persists its result before advancing, so a failure at any point resumes from there, not from the top.

class EnrichAllUsersDataPipeline < Ductwork::Pipeline
  define do |pipeline|
    pipeline.start(QueryUsersRequiringEnrichment)           # Begin with a single step
            .expand(to: LoadUserData)                       # Fan out to process each user
            .divide(to: [FetchDataFromSourceA,              # Split into parallel branches
                         FetchDataFromSourceB])
            .combine(into: CollateUserData)                 # Bring branches back together
            .chain(to: UpdateUserData)                      # Sequential processing
            .divert(to: { success: NotifyUser,              # Conditional branching
                          otherwise: FlagForReview })
            .converge(into: FinalizeUserRecord)             # Merge conditional branches
            .collapse(into: ReportUserEnrichmentSuccess)    # Final aggregation
  end
end

# Trigger from anywhere in your Rails app, then walk away.
# Ductwork runs every step to completion across process and host failures.
EnrichAllUsersDataPipeline.trigger(days_outdated: 7)

A step is just a class. Because Ductwork may re-run a step after a crash, the durability contract is explicit: make side effects idempotent, and return JSON-serializable values. The persisted return value is the hand-off to the next step.

class UpdateUserData < Ductwork::Step
  def execute(user_data)
    # `step.idempotency_key` is stable across retries and crash re-runs,
    # so external side effects can be made safely repeatable.
    ExternalCrm.upsert(user_data, key: step.idempotency_key)

    { user_id: user_data["id"], status: "updated" }  # durable hand-off to the next step
  end
end

Core Concepts

  • Getting Started - Install Ductwork and run your first pipeline
  • Defining Pipelines - The DSL for connecting steps and managing workflow
  • Durability - How Ductwork persists, claims, fences, and recovers every unit of work
  • Concurrency - Ductwork’s multi-process, multi-threaded execution model

When to Use Ductwork

Ductwork is built for workflows where finishing matters: work that spans multiple steps and external systems and must survive deploys and failures:

  • Multi-step integrations - Coordinate calls across services and APIs where a partial failure must resume, not restart
  • Data enrichment pipelines - Fetch, transform, and enrich records from multiple sources with durable checkpoints between stages
  • ETL processes - Extract, transform, and load with crash recovery instead of re-running the whole batch
  • Batch operations - Process large datasets in concurrent chunks where each chunk’s progress is persisted
  • Report generation - Aggregate data from multiple sources into a final artifact that only emits once

If your “workflow” is a single background job, a plain job queue is enough. Reach for Ductwork when the work has multiple stages and losing progress is expensive.

Ready to Start?

Jump into the Installation Guide and run your first durable pipeline in minutes, or read how durability works first.

Have questions? Open an issue on GitHub or upgrade to Pro for custom support.


Copyright © 2025-2026 Pen and Paper Solutions LLC
Made in Chicago, IL ❤️