Skip to content

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

  1. Two distinct approver pools — e.g. a team lead (first level) and a department head or group:finance-approvers (second level).
  2. The workflow:create, workflow:update, workflow:publish, and workflow:read permissions on your admin account. Starting a verification run also requires workflow:start.

Security model — read this before publishing

  • Rejection at either level stops the chain. The first-level approval step's error transition 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 condition step'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 a user-typed selfService variable) 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 the Expression field (amount >= 1000) so only someone with workflow:update can change it. (Separately: a bare, unbraced name like amount > threshold would 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. amount is a catalog Number field the requester types in. amount >= 1000 therefore evaluates the declared value, not a server-derived total. A requester who submits 999 for a request that should be 5000 skips 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, populate amount from a connector or transform step before the condition and do not declare it as a workflow variable, so it never appears on the catalog form.
  • default isn't used here on purpose. This workflow uses a condition step (binary true/false), not a case step, because there are exactly two outcomes with no meaningful third arm — use case instead 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:

  1. Start → First-Level Approval
  2. First-Level Approvalsuccess → Check Escalation Threshold
  3. First-Level Approvalerror → Notify Requester — Denied
  4. Check Escalation Thresholdtrue → Second-Level Approval
  5. Check Escalation Thresholdfalse → Apply Approved Request
  6. Second-Level Approvalsuccess → Apply Approved Request
  7. Second-Level Approvalerror → Notify Requester — Denied
  8. Apply Approved Requestsuccess → Notify Requester — Approved
  9. Notify Requester — Approved → End
  10. 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

  1. Click Save. Fix any validation errors — a missing true/false transition on the condition step, or an empty Approvers list on either approval step, will block save.
  2. Click Test Run with amount: 500. Confirm the run completes after First-Level Approval alone approves, skipping Second-Level Approval entirely.
  3. Repeat with amount: 1000. Confirm the run pauses at Second-Level Approval after the first approver approves — amount >= 1000 is inclusive, so the threshold value itself escalates.
  4. 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.
  5. 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.
  6. 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.