Multi-Step Approval Chain Workflow¶
A step-by-step guide to a user-triggered workflow that routes a request through a first-level approval, then conditionally escalates to a second-level approval when the requested amount exceeds a threshold. Below the threshold, one approval is enough; at or above it, both approvers must sign off before the request is applied.
| Amount | Approvals required |
|---|---|
| Below threshold | First-level approver only |
| At or above threshold | First-level approver and second-level approver |
Prerequisites¶
- Two distinct approver pools — e.g. a team lead (first level) and a
department head or
group:finance-approvers(second level). - The
workflow:create,workflow:update,workflow:publish, andworkflow:readpermissions on your admin account. Starting a verification run also requiresworkflow:start.
Security model — read this before publishing¶
- Rejection at either level stops the chain. The first-level approval
step's
errortransition goes straight to the denial notification — a request never reaches the second-level approver unless the first level approved it. This is a strict AND-chain, not two independent votes. - The escalation threshold is a literal on purpose, not a workflow
variable. The
conditionstep's expression is interpolated before evaluation, so a{{thresholdVar}}token would technically substitute a declared variable's value — but any variable you declare (other than auser-typedselfServicevariable) is rendered as an editable field on the requester's own submission form, with no generic "hidden from requester" flag available. Wiring the escalation cutoff to a workflow variable would let a requester set their own threshold on the way in and dodge the second approval entirely. Keep the number a literal in theExpressionfield (amount >= 1000) so only someone withworkflow:updatecan change it. (Separately: a bare, unbraced name likeamount > thresholdwould silently compare against the literal string"threshold"rather than resolving anything — another reason to stick to a plain numeric literal here rather than any variable-reference syntax.) - The compared amount is requester-supplied.
amountis a catalog Number field the requester types in.amount >= 1000therefore evaluates the declared value, not a server-derived total. A requester who submits999for a request that should be5000skips second-level approval if the first-level approver rubber-stamps it. Treat first-level review as the check that the declared amount matches the request; this pattern does not look up or compute a trusted amount. If you need that, populateamountfrom a connector ortransformstep before the condition and do not declare it as a workflow variable, so it never appears on the catalog form. defaultisn't used here on purpose. This workflow uses aconditionstep (binary true/false), not acasestep, because there are exactly two outcomes with no meaningful third arm — usecaseinstead if you later add a third tier (see Extending this pattern).
Overview¶
flowchart TD
Submit[Portal Submit]
Start[Start]
L1["First-Level Approval (approval)"]
Check{"Check Escalation Threshold (condition)"}
L2["Second-Level Approval (approval)"]
Apply["Apply Approved Request (action)"]
Approved[Notify Requester — Approved]
Denied[Notify Requester — Denied]
Done[End]
Submit --> Start --> L1
L1 -->|success| Check
L1 -->|error| Denied
Check -->|true| L2
Check -->|false| Apply
L2 -->|success| Apply
L2 -->|error| Denied
Apply --> Approved
Approved --> Done
Denied --> Done
Step 1 — Create the workflow¶
Navigate to Design in the sidebar (opens /workflows), open the
Workflows tab, and click New Workflow:
| Field | Value |
|---|---|
| Name | Multi-Step Approval Chain |
| Description | Route a request through first-level approval, escalating to second-level approval above a threshold |
| Category | user |
| Subject Variable | requestor |
| Error Strategy | stop |
| Trigger | manual |
Step 2 — Define variables¶
Open the Variables tab and add:
| Name | Type | Required | Secret | Default | Description |
|---|---|---|---|---|---|
requestor |
Requestor | yes | no | — | The submitting employee (auto-filled from session, hidden in form) |
amount |
Number |
yes | no | — | Dollar (or point/quantity) value the escalation threshold is checked against |
requestDetail |
String |
no | no | — | Free-text description of the request, shown to both approvers |
Like the Document Submission workflow,
the Requestor preset binds requestor to the authenticated caller
server-side — a requester cannot submit on someone else's behalf by editing
the form.
Step 3 — Add the workflow steps¶
3.1 — Start¶
Type: start. Entry point.
3.2 — First-Level Approval¶
Type: approval.
| Config Field | Value |
|---|---|
| Approvers | your first-level approver (user id, manager:{{requestor.id}}, or group:team-leads) |
| Minimum approvals needed | leave blank |
| Output Key | level1_approval |
Leaving Minimum approvals needed blank does not mean "any one
approver" — it means every listed approver token must individually
approve. A single user-id token needs just that person; a single
group:team-leads token needs any one member of that group (the group
itself is the token, not each member individually). If you list more than
one token, each one is a separate requirement (AND of tokens — the same
shape document_submission uses when multiple approvers are listed).
Transitions: success → Check Escalation Threshold; error → Notify Requester — Denied.
3.3 — Check Escalation Threshold¶
Type: condition.
| Config Field | Value |
|---|---|
| Expression | Custom expression... → amount >= 1000 |
Transitions: true → Second-Level Approval; false → Apply Approved Request.
1000 here is a literal threshold baked into the step, chosen deliberately
per the security note above —
adjust the number for your use case by editing the expression directly, but
keep it a literal rather than a workflow variable.
3.4 — Second-Level Approval¶
Type: approval. Only reached when the amount is at or above the threshold.
| Config Field | Value |
|---|---|
| Approvers | your second-level approver (e.g. group:finance-approvers) |
| Minimum approvals needed | leave blank (any one member of the group approves) |
| Output Key | level2_approval |
Transitions: success → Apply Approved Request; error → Notify Requester — Denied.
3.5 — Apply Approved Request¶
Type: action. A generic placeholder for whatever your request actually
does once fully approved — e.g. call a downstream connector step, a
transform step that computes a final result, or leave it as a no-op
marker if approval alone is the deliverable. Only the Output Key and
routing are configured here; type-specific logic (if you swap in a
connector step instead) is edited on the legacy Steps tab.
| Config Field | Value |
|---|---|
| Output Key | apply_request |
Transitions: success → Notify Requester — Approved.
3.6 — Notify Requester — Approved¶
Type: notification.
| Config Field | Value |
|---|---|
| Recipient Type | Internal User |
| Recipient User | {{requestor.id}} |
| Subject Override | Your request was approved |
| Custom Body | Your request for {{amount}} ({{requestDetail}}) has been approved. |
Transitions: success → End.
3.7 — Notify Requester — Denied¶
Type: notification.
| Config Field | Value |
|---|---|
| Recipient Type | Internal User |
| Recipient User | {{requestor.id}} |
| Subject Override | Your request was not approved |
| Custom Body | Your request for {{amount}} ({{requestDetail}}) was not approved. See your task inbox for the reviewer's comments. |
Transitions: success → End.
3.8 — End¶
Type: end. Exit point.
Step 4 — Wire the graph¶
In the Graph tab, draw these transitions:
- Start → First-Level Approval
- First-Level Approval →
success→ Check Escalation Threshold - First-Level Approval →
error→ Notify Requester — Denied - Check Escalation Threshold →
true→ Second-Level Approval - Check Escalation Threshold →
false→ Apply Approved Request - Second-Level Approval →
success→ Apply Approved Request - Second-Level Approval →
error→ Notify Requester — Denied - Apply Approved Request →
success→ Notify Requester — Approved - Notify Requester — Approved → End
- Notify Requester — Denied → End
Steps 5 and 6 both point at Apply Approved Request — that's the chain converging back to one path once every required approval has happened, regardless of which route got there.
Step 5 — Save, test, publish¶
- Click Save. Fix any validation errors — a missing
true/falsetransition on theconditionstep, or an emptyApproverslist on either approval step, will block save. - Click Test Run with
amount: 500. Confirm the run completes after First-Level Approval alone approves, skipping Second-Level Approval entirely. - Repeat with
amount: 1000. Confirm the run pauses at Second-Level Approval after the first approver approves —amount >= 1000is inclusive, so the threshold value itself escalates. - Repeat with
amount: 5000. Confirm the run pauses at Second-Level Approval after the first approver approves, and does not reach Apply Approved Request until the second approver also approves. - Test the rejection path at each level: reject at First-Level Approval and confirm the run ends via Notify Requester — Denied without ever reaching the threshold check; then repeat rejecting at Second-Level Approval for a high-amount run.
- Click Publish.
Extending this pattern¶
The condition step is binary (true / false). If you later need a
third approval tier — for example amounts below 1,000 skip escalation,
1,000–10,000 go to second-level, and above 10,000 go to a third
approver — replace the condition step with a case step. Give the
case one arm per band, keep every arm's expression a numeric literal
(same reason the threshold here is a literal: a requester-editable
variable would let them dodge a tier), wire each arm to its approval
step, and keep every approval's error transition on the shared
Notify Requester — Denied step so a rejection at any tier still
stops the chain.