Surflet

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_timeoutBehavior
escalateEscalate to the specified person
auto_approveAutomatically approve
auto_rejectAutomatically reject
skipSkip 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
}
BehaviorDescription
haltImmediately terminate the approval chain; page is marked as rejected
continueContinue subsequent steps (rejection is recorded but doesn't terminate)
restartRestart 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": []
    }
  ]
}