Approval Workflows
Configure multi-step approval chains
Surflet supports flexible multi-step approval chains that can handle everything from simple approvals to complex enterprise processes.
Approval Chain Modes
Sequential
Steps proceed one at a time — the next step begins only after the previous one completes. Ideal for hierarchical approval scenarios.
approval_chain = {
"mode": "sequential",
"steps": [
{
"step_id": "step_manager",
"name": "Direct Manager Approval",
"assignees": [
{"type": "email", "value": "[email protected]"},
],
"policy": {"type": "any"},
"timeout_hours": 24,
"on_timeout": "escalate",
},
{
"step_id": "step_director",
"name": "Director Approval",
"assignees": [
{"type": "email", "value": "[email protected]"},
],
"policy": {"type": "any"},
"timeout_hours": 48,
},
{
"step_id": "step_finance",
"name": "Finance Confirmation",
"assignees": [
{"type": "email", "value": "[email protected]"},
{"type": "email", "value": "[email protected]"},
],
"policy": {"type": "threshold", "min_approvals": 1},
},
],
"on_reject_at_any_step": "halt",
}
Flow: Manager → Director → Finance (any one approver). A rejection at any step terminates the entire chain.
Parallel
All steps run simultaneously and the chain completes when all steps finish. Ideal for scenarios requiring simultaneous sign-off from multiple independent departments.
approval_chain = {
"mode": "parallel",
"steps": [
{
"step_id": "step_legal",
"name": "Legal Review",
"assignees": [
{"type": "email", "value": "[email protected]"},
],
"policy": {"type": "any"},
},
{
"step_id": "step_compliance",
"name": "Compliance Review",
"assignees": [
{"type": "email", "value": "[email protected]"},
],
"policy": {"type": "any"},
},
{
"step_id": "step_security",
"name": "Security Review",
"assignees": [
{"type": "email", "value": "[email protected]"},
],
"policy": {"type": "any"},
},
],
"on_reject_at_any_step": "halt",
}
Legal, compliance, and security reviews proceed simultaneously. All three must pass for the chain to complete.
Conditional
Routes to different approval steps based on a field value. Ideal for tiered approval by amount or other criteria.
approval_chain = {
"mode": "conditional",
"condition_field": "amount",
"routes": [
{
"condition": {"operator": "lt", "value": 1000},
"steps": [
{
"step_id": "step_manager",
"name": "Manager Approval",
"assignees": [
{"type": "email", "value": "[email protected]"},
],
"policy": {"type": "any"},
},
],
},
{
"condition": {"operator": "gte", "value": 1000},
"steps": [
{
"step_id": "step_manager",
"name": "Manager Approval",
"assignees": [
{"type": "email", "value": "[email protected]"},
],
"policy": {"type": "any"},
},
{
"step_id": "step_cfo",
"name": "CFO Approval",
"assignees": [
{"type": "email", "value": "[email protected]"},
],
"policy": {"type": "any"},
},
],
},
],
}
Amounts under $1,000 require only manager approval. Amounts of $1,000 or more require both manager and CFO approval.
Decision Policy
Each approval step can be configured with a different decision policy:
any — First Responder
"policy": {"type": "any"}
Any one approver's decision passes the step. The most commonly used policy.
unanimous — All Must Agree
"policy": {"type": "unanimous"}
All approvers must agree for the step to pass. A single rejection rejects the step.
threshold — Minimum Approvals
"policy": {"type": "threshold", "min_approvals": 2}
Passes when the specified minimum number of approvals is reached.
weighted — Weighted Voting
"policy": {
"type": "weighted",
"weights": {
"[email protected]": 2,
"[email protected]": 1,
"[email protected]": 1,
},
"threshold": 3,
}
Each approver has a different weight. The step passes when the weighted sum reaches the threshold.
Timeout Handling
Set a timeout and timeout behavior for each step:
{
"step_id": "step_manager",
"name": "Manager Approval",
"assignees": [
{"type": "email", "value": "[email protected]"},
],
"policy": {"type": "any"},
"timeout_hours": 24,
"on_timeout": "escalate", # escalate / auto_approve / auto_reject / skip
"escalation_target": { # required when on_timeout is "escalate"
"type": "email",
"value": "[email protected]",
},
}
| on_timeout | Behavior |
|---|---|
escalate | Escalate to the specified person |
auto_approve | Automatically approve |
auto_reject | Automatically reject |
skip | Skip this step |
Delegation
Approvers can delegate their approval to someone else:
# API call
client.delegate("pg_xxx", {
"step_id": "step_manager",
"from": "[email protected]",
"to": "[email protected]",
"reason": "On vacation, delegating to deputy",
})
Delegations are recorded in the audit log, preserving the full delegation chain.
Rejection Handling
approval_chain = {
"mode": "sequential",
"steps": [...],
"on_reject_at_any_step": "halt", # halt / continue / restart
}
| Behavior | Description |
|---|---|
halt | Immediately terminate the approval chain; page is marked as rejected |
continue | Continue subsequent steps (rejection is recorded but doesn't terminate) |
restart | Restart the entire approval chain from the beginning |
Query Approval Status
approval = client.get_approval("pg_xxx")
print(approval)
Response:
{
"chain_status": "in_progress",
"current_step": "step_finance",
"steps": [
{
"step_id": "step_manager",
"status": "completed",
"decision": "approved",
"decided_by": "[email protected]",
"decided_at": "2026-03-23T10:30:00Z"
},
{
"step_id": "step_finance",
"status": "pending",
"assignees": ["[email protected]", "[email protected]"],
"decisions": []
}
]
}