Your First Real Workflow: Leave Request
Build a real-world Leave Request workflow with human tasks, business logic, and notifications.
What You’ll Build
A complete workflow that:
- Collects leave request details from the user
- Checks vacation balance
- Routes based on number of days
- Sends approval/denial emails
- Updates HR system
State types you’ll learn:
HumanTask- Wait for user inputChoice- Conditional logicEvaluatePolicy- Business rulesTask- Execute activitiesParallel- Send multiple notificationsWait- Time-based delays (bonus)Receive- External events (bonus)
Complete Leave Request Workflow
Create leave-request-app.yaml:
apiVersion: cascade.io/v1
kind: Application
metadata:
name: hr-leave-request
spec:
workflows:
- name: leave_request_flow
description: "HR Leave Request Process"
start: RequestDetails
states:
# ==========================================
# 1. HUMANTASK STATE - Collect user input
# ==========================================
- name: RequestDetails
type: HumanTask
description: "Employee submits leave request"
ui:
schema: urn:cascade:schema:leave_request_form
target: appsmith
next: CheckVacationBalance
# ==========================================
# 2. CHOICE STATE - Conditional branching
# ==========================================
- name: CheckVacationBalance
type: Choice
description: "Check if employee has enough vacation days"
choices:
- variable: $.state.days_requested
numericLessThanEquals: $.context.vacation_balance
next: SelectApprovalPath
- default: InsufficientBalance
# ==========================================
# 3. EVALUATE POLICY STATE - Business rules
# ==========================================
- name: SelectApprovalPath
type: EvaluatePolicy
description: "Determine approval workflow based on days"
policy: urn:cascade:policy:leave_approval_routing
engine: opa # Options: opa, dmn, wasm
parameters:
days_requested: $.state.days_requested
department: $.context.department
resultPath: $.policy_decision
next: DecideRoute
# ==========================================
# 4. CHOICE STATE - Route based on policy
# ==========================================
- name: DecideRoute
type: Choice
choices:
- variable: $.policy_decision.approval_type
stringEquals: "auto_approve"
next: AutoApprove
- variable: $.policy_decision.approval_type
stringEquals: "manager_approval"
next: ManagerApprovalTask
- default: ExecutiveApproval
# ==========================================
# Path A: Auto-approve short leaves
# ==========================================
- name: AutoApprove
type: Task
resource: urn:cascade:activity:approve_leave
parameters:
instance_id: $.id
approval_type: "automatic"
resultPath: $.approval_result
next: SendApprovalNotifications
# ==========================================
# Path B: Manager approval task
# ==========================================
- name: ManagerApprovalTask
type: HumanTask
description: "Manager reviews and approves/denies"
ui:
schema: urn:cascade:schema:approval_form
target: appsmith
next: EvaluateManagerDecision
- name: EvaluateManagerDecision
type: Choice
choices:
- variable: $.state.manager_approved
booleanEquals: true
next: SendApprovalNotifications
- default: Denied
# ==========================================
# Path C: Executive approval (for long leaves)
# ==========================================
- name: ExecutiveApproval
type: HumanTask
description: "Executive reviews extended leave"
ui:
schema: urn:cascade:schema:executive_approval_form
target: appsmith
next: EvaluateExecutiveDecision
- name: EvaluateExecutiveDecision
type: Choice
choices:
- variable: $.state.executive_approved
booleanEquals: true
next: SendApprovalNotifications
- default: Denied
# ==========================================
# 5. PARALLEL STATE - Send notifications
# ==========================================
- name: SendApprovalNotifications
type: Parallel
description: "Notify employee and update systems"
branches:
- name: SendEmployeeEmail
type: Task
resource: urn:cascade:activity:send_email
parameters:
recipient: $.context.employee_email
subject: "Your leave request has been approved"
template: "leave_approved"
- name: NotifyHR
type: Task
resource: urn:cascade:activity:notify_hr
parameters:
employee_id: $.context.employee_id
leave_dates: $.state.leave_dates
days: $.state.days_requested
- name: UpdateCalendar
type: Task
resource: urn:cascade:activity:update_calendar
parameters:
employee_id: $.context.employee_id
leave_dates: $.state.leave_dates
next: Complete
# ==========================================
# Denial path
# ==========================================
- name: Denied
type: Task
resource: urn:cascade:activity:deny_leave
parameters:
instance_id: $.id
reason: $.state.denial_reason
next: SendDenialNotification
- name: SendDenialNotification
type: Task
resource: urn:cascade:activity:send_email
parameters:
recipient: $.context.employee_email
subject: "Your leave request has been denied"
template: "leave_denied"
next: Complete
# ==========================================
# Insufficient balance path
# ==========================================
- name: InsufficientBalance
type: Task
resource: urn:cascade:activity:insufficient_balance_email
parameters:
recipient: $.context.employee_email
available_balance: $.context.vacation_balance
requested_days: $.state.days_requested
next: Complete
# ==========================================
# Final state
# ==========================================
- name: Complete
type: Task
end: trueUnderstanding Each Part
1. HumanTask State: RequestDetails
- name: RequestDetails
type: HumanTask
ui:
schema: urn:cascade:schema:leave_request_form
target: appsmith
next: CheckVacationBalanceWhat it does:
- Waits for user to interact with UI form
- Collects: employee name, dates, reason
- Stores input in
$.state - Moves to
CheckVacationBalancewhen user submits
Output example:
{
"state": {
"employee_name": "John Doe",
"leave_dates": ["2025-11-15", "2025-11-16"],
"days_requested": 2,
"reason": "Vacation"
}
}2. Choice State: CheckVacationBalance
- name: CheckVacationBalance
type: Choice
choices:
- variable: $.state.days_requested
numericLessThanEquals: $.context.vacation_balance
next: SelectApprovalPath
- default: InsufficientBalanceWhat it does:
- Compares
days_requested(from form) withvacation_balance(from context) - If balance is sufficient → routes to
SelectApprovalPath - Otherwise → routes to
InsufficientBalance - Execution time:
<0.1ms (in-processlogic)
Available operators:
stringEquals,stringLessThan,stringContainsnumericEquals,numericLessThan,numericGreaterThanbooleanEquals- Combinations:
and,or,not
3. EvaluatePolicy State: SelectApprovalPath
- name: SelectApprovalPath
type: EvaluatePolicy
policy: urn:cascade:policy:leave_approval_routing
engine: opa # or dmn, wasm
parameters:
days_requested: $.state.days_requested
department: $.context.department
resultPath: $.policy_decision
next: DecideRouteWhat it does:
- Evaluates complex business rules (OPA Rego policy)
- Passes employee info and request to policy engine
- Gets decision back:
auto_approve,manager_approval, orexecutive_approval - Typical execution time:
<5ms (OPA) or 10-50ms(DMN)
Example OPA policy (urn:cascade:policy:leave_approval_routing):
package leave_approval
default approval_type = "manager_approval"
approval_type = "auto_approve" {
input.days_requested <= 2
input.department == "Engineering"
}
approval_type = "executive_approval" {
input.days_requested > 10
}4. Choice State: DecideRoute
- name: DecideRoute
type: Choice
choices:
- variable: $.policy_decision.approval_type
stringEquals: "auto_approve"
next: AutoApprove
- variable: $.policy_decision.approval_type
stringEquals: "manager_approval"
next: ManagerApprovalTask
- default: ExecutiveApprovalWhat it does:
- Routes workflow to different approval paths based on policy decision
- Shows how Choice and EvaluatePolicy work together
5. Parallel State: SendApprovalNotifications
- name: SendApprovalNotifications
type: Parallel
branches:
- name: SendEmployeeEmail
type: Task
resource: urn:cascade:activity:send_email
parameters:
recipient: $.context.employee_email
subject: "Your leave request has been approved"
template: "leave_approved"
- name: NotifyHR
type: Task
resource: urn:cascade:activity:notify_hr
parameters:
employee_id: $.context.employee_id
leave_dates: $.state.leave_dates
- name: UpdateCalendar
type: Task
resource: urn:cascade:activity:update_calendar
parameters:
employee_id: $.context.employee_id
leave_dates: $.state.leave_dates
next: CompleteWhat it does:
- Executes 3 activities simultaneously (not sequentially)
- All 3 complete before moving to
next - Ideal for: sending notifications, updating multiple systems
- Execution time: ~max(SendEmail, NotifyHR, UpdateCalendar), not sum
Deploy and Test
1. Deploy the application
cascade app apply --file leave-request-app.yaml2. Start a workflow instance
cascade process start leave_request_flow \
--app=hr-leave-request \
--input='{"employee_id": "emp-123"}'3. Monitor execution
cascade process inspect <instance-id> --app=hr-leave-request4. Check logs
cascade logs --app=hr-leave-request --instance=<instance-id>Next Steps
You now understand:
- ✅ HumanTask (user input)
- ✅ Choice (conditional routing)
- ✅ EvaluatePolicy (business rules)
- ✅ Task (execute activities)
- ✅ Parallel (concurrent execution)
Learn More
- Core Concepts - Understand CDL, activities, policies
- Architecture Guide - How the system works
- Policy Authoring - Write OPA/DMN policies
- Workflow Development - Advanced patterns
- Examples - More real-world workflows
Ready to dive deeper? Continue with Core Concepts 🚀
Last updated on