Approval Workflows

Human-in-the-loop approval workflows using signals. A workflow pauses at an approval step and resumes when a human sends an approve or reject signal.

Prerequisites: Workflows, Durable Execution. What you'll learn: How to build workflows that wait for human decisions.

The Pattern

  1. A workflow reaches an approval node
  2. The node signals and waits for a response
  3. A human reviews and sends an approve or reject signal
  4. The workflow resumes and branches based on the decision

Example: Expense Approval

workflow approve_expense v1 {
  node validate = validate_expense v1
  node notify = notify_approver v1
  node wait = wait_for_approval v1
  node approved = process_approved v1
  node rejected = process_rejected v1

  edge validate -> notify
  edge notify -> wait
  edge wait -> approved
  edge wait -> rejected
}

activity wait_for_approval v1 {
  data decision {
    signal approval_decision {
      timeout: 7d
    }
  }

  apply(input: ApprovalInput) -> ApprovalDecision {
    return {
      approved: decision.approved,
      reason: decision.reason
    }
  }
}

Sending the Signal

External systems (UI, API, email handler) send the signal to resume the workflow:

# Approve
curl -X POST 'http://localhost:3000/api/signal/approval_decision' \
  -H 'content-type: application/json' \
  -d '{"workflow_id": "exp-123", "approved": true, "reason": "Within budget"}'

# Reject
curl -X POST 'http://localhost:3000/api/signal/approval_decision' \
  -H 'content-type: application/json' \
  -d '{"workflow_id": "exp-123", "approved": false, "reason": "Over budget limit"}'

Timeout Handling

If no signal arrives within the timeout period, the activity fails. Use compensation or a default branch to handle timeouts:

activity wait_for_approval v1 {
  data decision {
    signal approval_decision {
      timeout: 7d
    }
  }

  apply(input: ApprovalInput) -> ApprovalDecision {
    return {
      approved: decision.approved ?? false,
      reason: decision.reason ?? "Timed out - auto-rejected"
    }
  }

  retry_policy {
    max_attempts: 1
  }
}

See Also