Workflows¶
Compose multiple tasks into powerful, sequential pipelines. Workflows provide a declarative way to build complex business processes with conditional execution, shared context, and flexible error handling.
Since workflows are Task subclasses, they inherit all Task features: attributes, callbacks, middlewares, settings, and returns. Use these to validate workflow-level inputs, set up shared state, or track workflow outcomes.
class OnboardingWorkflow < CMDx::Task
include CMDx::Workflow
register :middleware, CMDx::Middlewares::Correlate
before_execution :load_user
on_failed :notify_admin!
required :user_id, type: :integer
returns :onboarded_at
task CreateProfile
task SetupPreferences
task SendWelcome
private
def load_user
context.user = User.find(user_id)
end
def notify_admin!
AdminMailer.onboarding_failed(context.user).deliver_later
end
end
Declarations¶
Tasks run in declaration order (FIFO), sharing a common context across the pipeline.
Warning
Don't define a work method in workflows—the module handles execution automatically. Attempting to do so raises a RuntimeError.
Task¶
task and tasks are aliases—use either interchangeably.
class OnboardingWorkflow < CMDx::Task
include CMDx::Workflow
task CreateUserProfile
task SetupAccountPreferences
tasks SendWelcomeEmail, SendWelcomeSms, CreateDashboard
end
Group¶
Group related tasks to share configuration:
Important
Settings and conditionals apply to all tasks in the group.
class ContentModerationWorkflow < CMDx::Task
include CMDx::Workflow
# Screening phase
tasks ScanForProfanity, CheckForSpam, ValidateImages, breakpoints: ["skipped"]
# Review phase
tasks ApplyFilters, ScoreContent, FlagSuspicious
# Decision phase
tasks PublishContent, QueueForReview, NotifyModerators
end
Conditionals¶
Conditionals support multiple syntaxes for flexible execution control:
class ContentAccessCheck
def call(task)
task.context.user.can?(:publish_content)
end
end
class OnboardingWorkflow < CMDx::Task
include CMDx::Workflow
# If and/or Unless
task SendWelcomeEmail, if: :email_configured?, unless: :email_disabled?
# Proc
task SendWelcomeEmail, if: -> { Rails.env.production? && self.class.name.include?("Premium") }
# Lambda
task SendWelcomeEmail, if: proc { context.features_enabled? }
# Class or Module
task SendWelcomeEmail, unless: ContentAccessCheck
# Instance
task SendWelcomeEmail, if: ContentAccessCheck.new
# Conditional applies to all tasks of this declaration group
tasks SendWelcomeEmail, CreateDashboard, SetupTutorial, if: :email_configured?
private
def email_configured?
context.user.email_address == true
end
def email_disabled?
context.user.communication_preference == :disabled
end
end
Halt Behavior¶
By default, skipped tasks don't stop the workflow—they're treated as no-ops. Configure breakpoints globally via workflow_breakpoints or per-task to customize this behavior.
class AnalyticsWorkflow < CMDx::Task
include CMDx::Workflow
task CollectMetrics # If fails → workflow stops
task FilterOutliers # If skipped → workflow continues
task GenerateDashboard # Only runs if no failures occurred
end
Task Configuration¶
Configure halt behavior for the entire workflow:
class SecurityWorkflow < CMDx::Task
include CMDx::Workflow
# Halt on both failed and skipped results
settings(workflow_breakpoints: ["skipped", "failed"])
task PerformSecurityScan
task ValidateSecurityRules
end
class OptionalTasksWorkflow < CMDx::Task
include CMDx::Workflow
# Never halt, always continue
settings(breakpoints: [])
task TryBackupData
task TryCleanupLogs
task TryOptimizeCache
end
Group Configuration¶
Different task groups can have different halt behavior:
class SubscriptionWorkflow < CMDx::Task
include CMDx::Workflow
task CreateSubscription, ValidatePayment, breakpoints: ["skipped", "failed"]
# Never halt, always continue
task SendConfirmationEmail, UpdateBilling, breakpoints: []
end
Rollback in Workflows¶
Each task in a workflow handles its own rollback independently. When a task's status matches the rollback_on setting (default: ["failed"]), that task's rollback method is called immediately after its execution — not retroactively for previously completed tasks.
class PaymentWorkflow < CMDx::Task
include CMDx::Workflow
task ReserveInventory # Succeeds → no rollback
task ChargeCard # Fails → ChargeCard.rollback called
task SendConfirmation # Never runs (workflow halts on failure)
end
class ChargeCard < CMDx::Task
def work
context.charge = PaymentGateway.charge(context.amount)
fail!("Declined") if context.charge.declined?
end
def rollback
PaymentGateway.void(context.charge.id)
end
end
Important
CMDx does not automatically rollback previously successful tasks when a later task fails. If you need to undo ReserveInventory when ChargeCard fails, handle it in ChargeCard's rollback or use a callback on the workflow itself.
class PaymentWorkflow < CMDx::Task
include CMDx::Workflow
on_failed :compensate!
task ReserveInventory
task ChargeCard
private
def compensate!
ReleaseInventory.execute(context) if context.reservation_id
end
end
Nested Workflows¶
Build hierarchical workflows by composing workflows within workflows:
class EmailPreparationWorkflow < CMDx::Task
include CMDx::Workflow
task ValidateRecipients
task CompileTemplate
end
class EmailDeliveryWorkflow < CMDx::Task
include CMDx::Workflow
tasks SendEmails, TrackDeliveries
end
class CompleteEmailWorkflow < CMDx::Task
include CMDx::Workflow
task EmailPreparationWorkflow
task EmailDeliveryWorkflow, if: proc { context.preparation_successful? }
task GenerateDeliveryReport
end
Parallel Execution¶
Run tasks concurrently using native Ruby threads for maximum throughput. No external dependencies required.
class SendWelcomeNotifications < CMDx::Task
include CMDx::Workflow
# One thread per task (default)
tasks SendWelcomeEmail, SendWelcomeSms, SendWelcomePush, strategy: :parallel
# Fixed thread pool size
tasks SendWelcomeEmail, SendWelcomeSms, SendWelcomePush, strategy: :parallel, pool_size: 2
end
Warning
Each parallel task receives its own context copy, which is merged back after execution. If multiple tasks write to the same key, the last merge wins non-deterministically. Use distinct keys per task to avoid conflicts.
Task Generator¶
Generate new CMDx workflow tasks quickly using the built-in generator:
This creates a new workflow task file with the basic structure:
# app/tasks/send_notifications.rb
class SendNotifications < CMDx::Task
include CMDx::Workflow
tasks Task1, Task2
end
Tip
Use present tense verbs + pluralized noun for workflow task names, eg: SendNotifications, DownloadFiles, ValidateDocuments