Conditional Branching
Conditional branching lets a pipeline take different paths at runtime based on application state. The dashboard renders branches as diamond decision nodes.
Runtime branching with branch blocks is uncommon among Ruby workflow gems — most offer only skip_if conditions on individual steps. GoodPipeline's branching evaluates a decision method when the branch is reached, runs the matching arm, marks non-matching arms as skipped_by_branch, and lets downstream steps wait on whichever arm was chosen.
Defining a branch
Use branch inside configure to define a decision point. The by: option names a method on your pipeline class that returns the arm to execute:
class MediaPipeline < GoodPipeline::Pipeline
def configure(media_id:)
run :analyze, AnalyzeJob, with: { media_id: media_id }
branch :format_check, after: :analyze, by: :detect_format do
on :hd do
run :transcode_hd, TranscodeHDJob, with: { media_id: media_id }
run :upscale, UpscaleJob, with: { media_id: media_id }, after: :transcode_hd
end
on :sd do
run :transcode_sd, TranscodeSDJob, with: { media_id: media_id }
end
end
run :publish, PublishJob, after: :format_check
end
private
def detect_format
Media.find(params[:media_id]).hd? ? :hd : :sd
end
endHow it works
- When the branch step's upstream dependencies are satisfied, the coordinator evaluates the
by:method - The method returns a symbol (
:hd,:sd, etc.) that selects which arm to run - Steps in the matching arm are enqueued normally
- Steps in non-matching arms are marked
skipped_by_branch— a terminal status that counts as satisfied for downstream dependency resolution after: :format_checkwaits for whichever arm was chosen to complete before proceeding
The decision method has access to params and can query any application state (database, cache, external APIs). It runs once per branch per pipeline execution — the result is cached.
Multiple steps per arm
Each arm can contain multiple steps with their own after: dependencies:
branch :deploy_strategy, after: :run_tests, by: :pick_strategy do
on :canary do
run :canary_deploy, CanaryDeployJob
run :canary_monitor, CanaryMonitorJob, after: :canary_deploy
run :canary_promote, CanaryPromoteJob, after: :canary_monitor
end
on :blue_green do
run :swap, BlueGreenSwapJob
run :verify, BlueGreenVerifyJob, after: :swap
end
endEmpty arms (if without else)
Arms can be empty for cases where one path does nothing:
branch :quality_check, after: :analyze, by: :needs_processing do
on :yes do
run :process, ProcessJob
run :enhance, EnhanceJob, after: :process
end
on :no # skip — pipeline continues to next step
end
run :save, SaveJob, after: :quality_checkWhen the decision returns :no, all :yes arm steps are skipped and :save proceeds directly. The dashboard shows the empty arm as a direct edge from the diamond to the next step.
Multiple branches
Pipelines can have multiple branches in sequence:
def configure(content_id:)
run :ingest, IngestJob, with: { content_id: content_id }
branch :classify, after: :ingest, by: :content_type do
on :text do
run :extract_text, ExtractTextJob
run :run_nlp, RunNlpJob, after: :extract_text
end
on :image do
run :detect_objects, DetectObjectsJob
run :check_nsfw, CheckNsfwJob, after: :detect_objects
end
end
branch :priority, after: :classify, by: :review_priority do
on :high do
run :fast_review, FastReviewJob
end
on :low do
run :standard_review, StandardReviewJob
end
end
run :publish, PublishJob, after: :priority
endEach branch decides independently. The second branch runs after whichever arm of the first branch completes.
Error handling
If the decision method returns a value that does not match any declared arm (including empty arms), the branch step is marked failed with a ConfigurationError. The error class and message are stored on the step record and the pipeline proceeds through normal failure propagation (:halt, :continue, or :ignore depending on strategy).
branch :check, after: :analyze, by: :pick do
on(:hd) { run :transcode_hd, TranscodeHDJob }
on(:sd) { run :transcode_sd, TranscodeSDJob }
end
# If pick returns :unknown → branch step fails with:
# "branch :check decision returned "unknown", but only :hd, :sd are declared"Limitations
- Nested branches (a branch inside another branch's arm) are not supported. This is validated and rejected with a
ConfigurationError. - Branch order matters — define branches before referencing them in
after:. Arunwithafter: :branch_keymust appear after thebranchdefinition. - Decision methods must be fast — they run inside the coordinator's step processing. Avoid slow external calls.