Orchestr8
DAG-based job workflow orchestration for Rails. Define multi-step workflows as directed acyclic graphs with parallel execution, dependency tracking, failure handling, and a built-in dashboard. No Redis required — all state lives in your database.
What It Does
- Compose jobs into workflows with explicit dependencies
- Steps with no unmet dependencies run in parallel automatically
- Steps can pass output data to downstream steps
- Configurable failure handling per step (halt the workflow or continue)
- Built-in web dashboard with DAG visualization and real-time status updates
- Works with any Active Job backend (Solid Queue, Sidekiq, GoodJob, etc.)
- Test helper for synchronous, in-process workflow execution in specs
Installation
Add to your Gemfile:
gem "orchestr8"Run the migrations:
bundle exec rails orchestr8:install:migrations
bundle exec rails db:migrateMount the dashboard in config/routes.rb:
mount Orchestr8::Engine => "/orchestr8"Quick Start
1. Define a Workflow
# app/workflows/etl_workflow.rb
class EtlWorkflow < Orchestr8::Workflow
step :fetch, job: FetchDataJob
step :transform, job: TransformJob, after: :fetch
step :load, job: LoadJob, after: :transform
end2. Add Stepable to Your Jobs
class FetchDataJob < ApplicationJob
include Orchestr8::Stepable
def perform(arguments)
data = fetch_data(arguments["url"])
output(data) # share data with downstream steps
end
end
class TransformJob < ApplicationJob
include Orchestr8::Stepable
def perform(arguments)
raw = payloads[:fetch] # read upstream output
output(transform(raw))
end
end3. Run a Workflow
EtlWorkflow.create!(arguments: { "url" => "https://api.example.com/data" })Orchestr8 enqueues ready steps immediately, then re-evaluates and enqueues downstream steps as each step completes.
Step Options
| Option | Type | Default | Description |
|---|---|---|---|
job: |
Class | required | The Active Job class to perform |
after: |
Symbol or Array | [] |
Step name(s) this step depends on |
on_failure: |
:halt / :continue
|
:halt |
What to do when this step fails |
queue: |
String | nil | Override the queue name for this step |
class MyWorkflow < Orchestr8::Workflow
step :fetch, job: FetchJob, queue: "critical"
step :notify, job: NotifyJob, after: :fetch, on_failure: :continue
step :cleanup, job: CleanupJob, after: %i[fetch notify]
endFailure Handling
By default (on_failure: :halt), a failed step prevents downstream steps from running and marks the workflow as failed.
With on_failure: :continue, downstream steps still run even if this step fails. The failed step's output will be nil in payloads.
class ReportWorkflow < Orchestr8::Workflow
step :fetch_primary, job: FetchPrimaryJob
step :fetch_secondary, job: FetchSecondaryJob, on_failure: :continue
step :aggregate, job: AggregateJob, after: %i[fetch_primary fetch_secondary]
endRetry
Retry a single failed step:
step = workflow.steps.find_by(name: "transform")
step.retry!Retry all failed steps in a workflow:
workflow.retry!Both reset the step(s) to pending and re-trigger the scheduler immediately.
Dashboard
Mount the engine in your routes:
mount Orchestr8::Engine => "/orchestr8"The dashboard at /orchestr8 lists all workflows with status and step progress. Each workflow's detail page shows a DAG visualization with real-time status updates via Turbo Streams.
Securing the Dashboard
Configure a base controller class that enforces authentication:
# config/initializers/orchestr8.rb
Orchestr8.configure do |config|
config.base_controller_class = "AdminController"
endThe engine's controllers will inherit from that class, so any before_action defined there (e.g. authenticate_admin!) will apply automatically.
Testing
Use Orchestr8::TestHelper to execute workflows synchronously in specs without a real job backend:
# spec/rails_helper.rb (or in a specific spec)
require "orchestr8/test_helper"
RSpec.describe "My feature" do
include Orchestr8::TestHelper
it "completes the workflow inline" do
workflow = MyWorkflow.create!(arguments: { "key" => "value" })
expect(workflow.reload.status).to eq("completed")
expect(workflow.steps.all? { |s| s.status == "completed" }).to be true
end
endOrchestr8::TestHelper hooks into RSpec's before/after to enable and reset synchronous mode around each example. In synchronous mode the scheduler runs each job inline via perform_now and loops until all reachable steps are processed.
Works With
Orchestr8 uses Active Job and makes no assumptions about the backend.
| Backend | Gem | Notes |
|---|---|---|
| Solid Queue | solid_queue |
Recommended for zero-Redis setups |
| Sidekiq | sidekiq |
Set config.active_job.queue_adapter = :sidekiq
|
| GoodJob | good_job |
Postgres-native, supports concurrency controls |
License
MIT — see LICENSE.