Skip to Content
GuidesWorkflow Development

Workflow Development Guide

For: Developers building CDL workflows
Level: Intermediate → Advanced
Time to read: 30 minutes
Complexity: Patterns for every workflow type

This guide shows you how to design and build production-grade workflows using CDL, with proven patterns for common scenarios.


Getting Started with CDL

Minimal Workflow

apiVersion: cascade.io/v1 kind: Application metadata: name: hello-workflow namespace: default spec: workflows: - name: HelloWorld description: "Minimal workflow example" start: Greeting states: - name: Greeting type: Task resource: urn:cascade:activity:greet parameters: name: "{{ workflow.input.name | default: 'World' }}" result: $.message end: true

Run it:

cascade process start \ --app hello-workflow \ --workflow HelloWorld \ --input '{"name": "Alice"}'

Workflow Design Patterns

Pattern 1: Sequential Workflow

Use: Step-by-step processes with clear ordering

Example: Order Processing

workflows: - name: ProcessOrder start: ValidateOrder states: - name: ValidateOrder type: Task resource: urn:cascade:activity:validate_order parameters: order_id: "{{ workflow.input.order_id }}" result: $.validation next: ReserveInventory - name: ReserveInventory type: Task resource: urn:cascade:activity:reserve_inventory parameters: items: "{{ $.validation.items }}" result: $.inventory_reservation next: ProcessPayment - name: ProcessPayment type: Task resource: urn:cascade:activity:charge_card parameters: amount: "{{ $.validation.total }}" card_token: "{{ workflow.input.card_token }}" result: $.payment next: ShipOrder - name: ShipOrder type: Task resource: urn:cascade:activity:create_shipment parameters: order_id: "{{ workflow.input.order_id }}" items: "{{ $.validation.items }}" result: $.shipment end: true

Key points:

  • Each state has exactly one next state
  • Results accumulate in context
  • Clear linear flow
  • Easy to trace execution

Latency: Sum of all activities (if any can parallelize, use Pattern 4)


Pattern 2: Choice-Based Routing

Use: Different paths based on data

Example: Approval Routing

workflows: - name: RouteApproval start: ReceiveRequest states: - name: ReceiveRequest type: HumanTask description: "Submit for approval" ui: schema: urn:cascade:schema:approval_request target: appsmith timeout: 24h next: DetermineRoute - name: DetermineRoute type: Choice choices: - condition: "{{ $.request.amount <= 1000 && $.request.category == 'office' }}" next: OfficeManagerApproval description: "Small office expense" - condition: "{{ $.request.amount <= 10000 }}" next: DepartmentHeadApproval description: "Department-level review" - condition: "{{ $.request.amount > 10000 }}" next: ExecutiveApproval description: "Executive approval required" - condition: "{{ $.request.category == 'emergency' }}" next: FastTrackApproval description: "Emergency fast-track" default: ManagerApproval # Parallel approval paths - name: OfficeManagerApproval type: HumanTask ui: schema: urn:cascade:schema:approval_form target: appsmith assignee: role: office_manager timeout: 2h next: SendResult - name: DepartmentHeadApproval type: HumanTask ui: schema: urn:cascade:schema:approval_form target: appsmith assignee: role: department_head tags: - department: "{{ $.request.department }}" timeout: 24h next: SendResult - name: ExecutiveApproval type: HumanTask ui: schema: urn:cascade:schema:approval_form target: appsmith assignee: role: executive timeout: 48h next: SendResult - name: FastTrackApproval type: Task resource: urn:cascade:activity:fast_track_approve parameters: request_id: "{{ workflow.input.request_id }}" result: $.approval next: SendResult - name: ManagerApproval type: HumanTask ui: schema: urn:cascade:schema:approval_form target: appsmith assignee: role: manager timeout: 24h next: SendResult - name: SendResult type: Task resource: urn:cascade:activity:send_result parameters: request_id: "{{ workflow.input.request_id }}" approved: "{{ $.approval.approved }}" end: true

Key points:

  • Conditions evaluated left-to-right
  • First match wins
  • default is fallback
  • All branches converge at common state
  • Supports complex boolean logic

Best practices:

  • Order conditions from most specific to least
  • Keep conditions simple (use OPA for complex logic)
  • Always have a default path

Pattern 3: Parallel Execution

Use: Speed up workflow by running independent tasks simultaneously

Example: Application Processing

workflows: - name: ProcessApplication start: ReceiveApplication states: - name: ReceiveApplication type: HumanTask ui: schema: urn:cascade:schema:application_form target: appsmith next: ParallelVerification - name: ParallelVerification type: Parallel branches: - name: VerifyIdentity type: Task resource: urn:cascade:activity:verify_identity parameters: applicant_id: "{{ $.applicant_id }}" document_id: "{{ $.document_id }}" result: $.identity_check - name: CheckCreditScore type: Task resource: urn:cascade:activity:check_credit parameters: applicant_id: "{{ $.applicant_id }}" result: $.credit_check - name: VerifyEmployment type: Task resource: urn:cascade:activity:verify_employment parameters: employer: "{{ $.employer }}" employee_id: "{{ $.applicant_id }}" result: $.employment_check - name: CheckCriminalRecord type: Task resource: urn:cascade:activity:criminal_check parameters: name: "{{ $.applicant_name }}" ssn: "{{ $.ssn }}" result: $.criminal_check completion_strategy: ALL # Wait for all to complete next: CombineVerifications - name: CombineVerifications type: Task resource: urn:cascade:activity:combine_checks parameters: identity: "{{ $.identity_check }}" credit: "{{ $.credit_check }}" employment: "{{ $.employment_check }}" criminal: "{{ $.criminal_check }}" result: $.combined_result next: EvaluateResults - name: EvaluateResults type: Choice choices: - condition: "{{ $.combined_result.all_passed }}" next: Approve - condition: "{{ $.combined_result.needs_review }}" next: ManagerReview default: Reject - name: Approve type: Task resource: urn:cascade:activity:approve_application end: true - name: ManagerReview type: HumanTask ui: schema: urn:cascade:schema:review_form pre_populate: results: "{{ $.combined_result }}" assignee: role: application_manager timeout: 24h next: ApprovalDecision - name: ApprovalDecision type: Choice choices: - condition: "{{ $.manager_decision == 'approve' }}" next: Approve default: Reject - name: Reject type: Task resource: urn:cascade:activity:reject_application end: true

Completion strategies:

# ALL: Wait for all branches (default) completion_strategy: ALL # ANY: Proceed when first branch completes completion_strategy: ANY # N_OF_M: Proceed when N branches complete completion_strategy: N_OF_M n: 2 # Wait for 2 of 4

Performance impact:

  • Sequential: T1 + T2 + T3 + T4 = 40ms (4×10ms)
  • Parallel: max(T1, T2, T3, T4) = 10ms (same duration as longest)
  • 3x speedup!

Pattern 4: Error Recovery & Retry

Use: Handle failures gracefully with automatic recovery

Example: Unreliable API Integration

workflows: - name: OrderWithRetry start: CallPaymentAPI states: - name: CallPaymentAPI type: Task resource: urn:cascade:activity:charge_payment parameters: amount: "{{ workflow.input.amount }}" card: "{{ workflow.input.card_token }}" result: $.payment retries: max_attempts: 5 backoff: initial_interval: 1s max_interval: 60s multiplier: 2.0 timeout: 30s next: CheckPayment - name: CheckPayment type: Choice choices: - condition: "{{ $.payment.status == 'success' }}" next: ProcessOrder - condition: "{{ $.payment.status == 'pending' }}" next: WaitForConfirmation default: PaymentFailed - name: WaitForConfirmation type: Wait duration: 5m next: CheckPaymentStatus - name: CheckPaymentStatus type: Task resource: urn:cascade:activity:check_payment_status parameters: payment_id: "{{ $.payment.id }}" result: $.payment_status next: ProcessIfSuccessful - name: ProcessIfSuccessful type: Choice choices: - condition: "{{ $.payment_status.confirmed }}" next: ProcessOrder default: PaymentFailed - name: ProcessOrder type: Task resource: urn:cascade:activity:create_order parameters: items: "{{ workflow.input.items }}" payment_id: "{{ $.payment.id }}" result: $.order end: true - name: PaymentFailed type: Task resource: urn:cascade:activity:notify_payment_failed parameters: reason: "{{ $.payment.error }}" end: true

Retry configuration:

retries: max_attempts: 5 # Try up to 5 times backoff: initial_interval: 1s # Start at 1 second max_interval: 60s # Cap at 60 seconds multiplier: 2.0 # Double each time

Retry timeline: 0s → 1s → 3s → 7s → 15s (total 26s before failure)


Pattern 5: Long-Running Workflow

Use: Workflows lasting hours/days with human interaction

Example: Contract Review Process

workflows: - name: ContractReviewProcess start: UploadContract states: - name: UploadContract type: HumanTask description: "Upload contract for review" ui: schema: urn:cascade:schema:contract_upload target: appsmith timeout: 7d next: InitialReview - name: InitialReview type: HumanTask description: "Legal team initial review" ui: schema: urn:cascade:schema:contract_review pre_populate: document: "{{ $.document_url }}" assignee: role: legal_specialist timeout: 3d next: CheckReviewComments - name: CheckReviewComments type: Choice choices: - condition: "{{ $.review.issues_found }}" next: RequestRevisions default: ManagerReview - name: RequestRevisions type: HumanTask description: "Request contract revisions" ui: schema: urn:cascade:schema:revision_request pre_populate: issues: "{{ $.review.issues }}" assignee: role: contract_originator timeout: 5d next: WaitForResubmission - name: WaitForResubmission type: Wait duration: 24h # Give them a day to respond next: CheckIfResubmitted - name: CheckIfResubmitted type: Task resource: urn:cascade:activity:check_resubmission parameters: contract_id: "{{ workflow.input.contract_id }}" result: $.resubmission next: HandleResubmission - name: HandleResubmission type: Choice choices: - condition: "{{ $.resubmission.received }}" next: InitialReview # Review again default: FollowUp - name: FollowUp type: HumanTask description: "Follow up on missing revisions" ui: schema: urn:cascade:schema:followup_form assignee: role: legal_manager timeout: 3d next: ProcessFollowUp - name: ProcessFollowUp type: Choice choices: - condition: "{{ $.followup.proceed_without_revisions }}" next: ManagerReview - condition: "{{ $.followup.cancel }}" next: Cancelled default: FollowUp - name: ManagerReview type: HumanTask description: "Manager final approval" ui: schema: urn:cascade:schema:final_approval pre_populate: contract_summary: "{{ $.contract_summary }}" reviews: "{{ $.review }}" assignee: role: manager timeout: 2d next: FinalApprovalDecision - name: FinalApprovalDecision type: Choice choices: - condition: "{{ $.final_decision == 'approved' }}" next: ExecuteContract - condition: "{{ $.final_decision == 'rejected' }}" next: Rejected default: RequireAdditionalReview - name: RequireAdditionalReview type: HumanTask description: "Address manager concerns" ui: schema: urn:cascade:schema:additional_review pre_populate: concerns: "{{ $.final_decision.concerns }}" assignee: role: legal_specialist timeout: 3d next: ManagerReview - name: ExecuteContract type: Task resource: urn:cascade:activity:execute_contract parameters: contract_id: "{{ workflow.input.contract_id }}" result: $.execution end: true - name: Rejected type: Task resource: urn:cascade:activity:send_rejection parameters: reason: "{{ $.final_decision.reason }}" end: true - name: Cancelled type: Task resource: urn:cascade:activity:cancel_review end: true

Key points:

  • Multiple HumanTask states with long timeouts
  • Wait states for delays
  • Multiple revision cycles possible
  • Clear path to completion or rejection
  • Typical duration: 2-4 weeks

Pattern 6: Workflow Composition

Use: Reuse workflows by calling them from other workflows

Example: Multi-Level Approval

workflows: - name: ComplexApprovalWorkflow start: ReceiveRequest states: - name: ReceiveRequest type: HumanTask ui: schema: urn:cascade:schema:request_form target: appsmith next: InitialValidation - name: InitialValidation type: Task resource: urn:cascade:activity:validate_request parameters: request: "{{ workflow.input }}" result: $.validation next: DetermineApprovalLevel - name: DetermineApprovalLevel type: Choice choices: - condition: "{{ $.validation.amount <= 5000 }}" next: StandardApproval - condition: "{{ $.validation.amount <= 50000 }}" next: ManagerApproval default: ExecutiveApproval # Standard approval (inline) - name: StandardApproval type: HumanTask ui: schema: urn:cascade:schema:approval_form assignee: role: supervisor timeout: 24h next: ProcessApproval # Manager approval (could be sub-workflow) - name: ManagerApproval type: Task resource: urn:cascade:activity:invoke_manager_approval parameters: request_id: "{{ workflow.input.request_id }}" amount: "{{ $.validation.amount }}" result: $.manager_decision timeout: 48h next: ProcessApproval # Executive approval (could be sub-workflow) - name: ExecutiveApproval type: Task resource: urn:cascade:activity:invoke_executive_approval parameters: request_id: "{{ workflow.input.request_id }}" amount: "{{ $.validation.amount }}" result: $.executive_decision timeout: 72h next: ProcessApproval - name: ProcessApproval type: Choice choices: - condition: "{{ $.manager_decision.approved || $.executive_decision.approved || $.approval.approved }}" next: NotifyApproved default: NotifyRejected - name: NotifyApproved type: Task resource: urn:cascade:activity:send_approval_notification end: true - name: NotifyRejected type: Task resource: urn:cascade:activity:send_rejection_notification end: true

Pattern: Use activities to invoke sub-workflows or external processes, treating them as black boxes.


Real-World Example 1: Order Processing (10 States)

Complete order-to-shipment workflow:

workflows: - name: OrderFulfillment description: "End-to-end order processing" start: ReceiveOrder states: - name: ReceiveOrder type: Task resource: urn:cascade:activity:receive_order parameters: order_id: "{{ workflow.input.order_id }}" result: $.order timeout: 10s next: ValidateOrder - name: ValidateOrder type: Task resource: urn:cascade:activity:validate_order parameters: order: "{{ $.order }}" result: $.validation timeout: 15s next: ValidateDecision - name: ValidateDecision type: Choice choices: - condition: "{{ $.validation.valid }}" next: ReserveInventory default: RejectOrder - name: RejectOrder type: Task resource: urn:cascade:activity:reject_order parameters: reason: "{{ $.validation.error }}" end: true - name: ReserveInventory type: Parallel branches: - name: ReserveItem1 type: Task resource: urn:cascade:activity:reserve_item parameters: sku: "{{ $.order.items[0].sku }}" qty: "{{ $.order.items[0].qty }}" result: $.item1_reserved - name: ReserveItem2 type: Task resource: urn:cascade:activity:reserve_item parameters: sku: "{{ $.order.items[1].sku }}" qty: "{{ $.order.items[1].qty }}" result: $.item2_reserved completion_strategy: ALL next: ProcessPayment - name: ProcessPayment type: Task resource: urn:cascade:activity:process_payment parameters: amount: "{{ $.order.total }}" payment_method: "{{ workflow.input.payment_method }}" result: $.payment retries: max_attempts: 3 backoff: initial_interval: 5s max_interval: 30s multiplier: 2 timeout: 30s next: PaymentDecision - name: PaymentDecision type: Choice choices: - condition: "{{ $.payment.success }}" next: CreateShipment default: PaymentFailed - name: PaymentFailed type: Task resource: urn:cascade:activity:release_inventory parameters: reservations: "{{ $.item1_reserved }},{{ $.item2_reserved }}" end: true - name: CreateShipment type: Task resource: urn:cascade:activity:create_shipment parameters: order_id: "{{ workflow.input.order_id }}" items: "{{ $.order.items }}" result: $.shipment timeout: 20s next: NotifyCustomer - name: NotifyCustomer type: Task resource: urn:cascade:activity:send_shipment_notification parameters: email: "{{ $.order.customer_email }}" shipment_id: "{{ $.shipment.id }}" end: true

Best Practices

✅ DO:

  • Use meaningful state names - ProcessPayment not state_5
  • Set appropriate timeouts - Quick tasks 10-30s, human tasks 24h+
  • Group related work - Use Parallel for independent tasks
  • Fail fast - Validate early, before expensive operations
  • Handle errors explicitly - Don’t assume activities succeed
  • Document complex conditions - Add descriptions to Choice branches
  • Use activities for side effects - Don’t do I/O in Choice states
  • Version your workflows - Track changes in version control

❌ DON’T:

  • Create infinite loops - Always have termination condition
  • Nest workflows too deep - Max 3-4 levels before readability drops
  • Hardcode values - Use parameters and context
  • Forget compensation - Plan rollback for complex workflows
  • Mix concerns - Keep authentication/validation separate
  • Ignore timeouts - Every activity needs a timeout
  • Create very long workflows - Break into sub-workflows after 15 states

Troubleshooting Common Issues

Workflow Stuck in State

Symptom: Process hasn’t progressed in hours

Causes:

  • Missing timeout on HumanTask
  • Activity never returns
  • Condition always false

Solution:

# Inspect workflow cascade process inspect {workflow_id} # Check current state & how long it's been there # Add timeout to HumanTask if missing: timeout: 24h timeoutAction: ESCALATE_TO_MANAGER

State Transition Errors

Symptom: Invalid next state: StateNotFound

Cause: Typo in state name or wrong case

Solution:

  • State names are case-sensitive
  • Check spelling exactly
  • Use IDE autocomplete if available

Next Steps

Ready to build workflows?Getting Started: First Workflow

Need to write activities?Activity Development Guide

Want production patterns?Best Practices: Error Handling


Updated: October 29, 2025
Version: 1.0
Examples: 10+ complete workflows
Patterns: 6 proven designs

Last updated on