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: trueRun 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: trueKey points:
- Each state has exactly one
nextstate - 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: trueKey points:
- Conditions evaluated left-to-right
- First match wins
defaultis 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: trueCompletion 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 4Performance 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: trueRetry 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 timeRetry 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: trueKey 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: truePattern: 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: trueBest Practices
✅ DO:
- Use meaningful state names -
ProcessPaymentnotstate_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_MANAGERState 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