Most approval workflows fail not on the happy path but when approvers are unavailable, stages need to run in parallel, or decisions need to be delegated. This lesson teaches you every pattern you need to build production-grade, multi-stage approval chains in Power Automate — sequential, parallel, hybrid, and fully delegated.

Your contract is sitting in a SharePoint library, waiting. Legal reviewed it three days ago. Finance hasn't seen it yet, and their manager is out of office. The VP who needs to give final sign-off doesn't even know the document exists. Meanwhile, the vendor is emailing your sales team asking why the deal isn't moving.
This is what approval workflows look like when they're managed through email threads and calendar reminders — fragmented, invisible, and brittle. Power Automate's built-in approval engine gives you a proper solution: a structured, auditable chain of sign-offs that routes work to the right people at the right time, handles delegation automatically, escalates when deadlines slip, and keeps every stakeholder informed without a single manual follow-up email.
By the end of this lesson, you'll be able to design and build production-grade multi-stage approval chains in Power Automate — the kind that handle real organizational complexity, not just the happy path. You'll know when to run approvals in sequence, when to fan them out in parallel, and how to wire in delegation so out-of-office situations don't freeze your workflows.
What you'll learn:
You should be comfortable building cloud flows in Power Automate, working with conditions and expressions, and using SharePoint or Teams as a data/communication layer. If you're newer to the approval actions specifically, Building Approval Workflows with Power Automate covers the foundational approval action setup and is worth reviewing first. You should also be comfortable with working with conditions, loops, and variables in Power Automate, since multi-stage chains depend heavily on branching logic.
Before you start stacking approval stages, you need a clear mental model of what happens inside Power Automate when an approval action fires. Most tutorials skip this and it creates confusion when flows behave unexpectedly.
When you add a "Start and wait for an approval" action, Power Automate does three things simultaneously:
approvals@microsoft.com and an Adaptive Card in the Teams Approvals app if the approver has the app installed.When any approver responds, the approval record is updated, and Power Automate resumes your flow with the response details in the action's output. The key output properties you'll use constantly are:
outputs('Start_and_wait_for_an_approval')?['body/outcome'] — either "Approve" or "Reject"outputs('Start_and_wait_for_an_approval')?['body/responses'] — an array of individual approver responses, each with approverName, approverEmail, requestDate, responseDate, comments, and responseThe distinction between "Start and wait for an approval" and "Start an approval" (without the wait) matters enormously for parallel patterns. The former blocks execution; the latter fires the request and immediately continues, letting you start multiple approvals before waiting on any of them.
Key insight
The Approvals connector stores requests in a Microsoft-managed backend, not in your tenant's Dataverse or SharePoint. This is convenient, but it means you have limited control over the retention and querying of historical approval data. If your organization needs a full audit trail queryable by date range, approver, or document, you should mirror response data to SharePoint or Dataverse after each approval stage completes.
The sequential pattern is the most common in practice: Stage 1 must complete before Stage 2 starts. A contract might need legal review before finance review before executive sign-off. Each subsequent stage only fires if the previous one approved. A rejection at any stage terminates the chain.
Think of a sequential chain as a series of condition nodes where the entry condition for each stage is "did the previous stage approve?" Your flow structure looks like:
Trigger (SharePoint item created)
└─ Stage 1: Legal Review
├─ [Approved] → Stage 2: Finance Review
│ ├─ [Approved] → Stage 3: VP Sign-Off
│ │ ├─ [Approved] → Mark Approved, Notify all parties
│ │ └─ [Rejected] → Mark Rejected, Notify requester
│ └─ [Rejected] → Mark Rejected, Notify requester
└─ [Rejected] → Mark Rejected, Notify requester
Notice the rejection handler repeats at each level. This is unavoidable in pure nested conditions, but there's a cleaner pattern using variables that we'll cover shortly.
Start with a SharePoint "When an item is created" trigger on your Contracts library. Add "Start and wait for an approval" with these settings:
Legal Review Required: [Title from SharePoint]Link to item dynamic value from the SharePoint trigger)After the action, add a Condition checking outcome is equal to Approve. Place Stage 2 in the Yes branch.
Nesting five levels of conditions to handle rejections creates a flow that's genuinely difficult to maintain. A cleaner approach uses a string variable to carry the approval chain's verdict:
Initialize a string variable ApprovalChainStatus to "Pending" at the top of your flow. After each approval action, instead of deeply nesting conditions, use this pattern:
If outcome == "Reject":
Set ApprovalChainStatus = "Rejected - Legal: [response comments]"
Terminate (Cancelled)
If outcome == "Approve":
[continue to next stage]
Using Terminate with status "Cancelled" (not "Failed") after a rejection signals to the flow run history that this was a deliberate business outcome, not an error. You can then handle the rejection notification in a scope block before the Terminate, keeping your cleanup logic in one place.
Tip
Capture the rejecting approver's comments in your variable: concat('Rejected at Legal stage by ', outputs('Legal_Approval')?['body/responses'][0]?['approverName'], ': ', outputs('Legal_Approval')?['body/responses'][0]?['comments']). This gives your requester a meaningful rejection reason rather than a bare "rejected" notification.
A common mistake in sequential chains is treating each stage as isolated. Stage 2 reviewers often need to know what Stage 1 reviewers said — particularly their comments. Pass this forward explicitly.
After Stage 1 completes, compose a "Stage 1 Review Summary" variable:
Legal Reviewer: [approverName]
Legal Decision: Approved
Legal Comments: [comments]
Reviewed on: [responseDate]
[Original contract details...]
Inject this summary into Stage 2's Details field. Finance reviewers shouldn't have to ask legal what they thought — it's right there in the approval notification.
What happens when Stage 2's approver goes on vacation for two weeks? Your flow hangs indefinitely. Power Automate's approval action has a built-in timeout parameter: set Due date to an expression like addDays(utcNow(), 3) for a 3-day deadline.
When an approval times out (no response by due date), the outcome is "No response" — not Reject. Handle this as a third branch in your condition: escalate to the approver's manager using the Office 365 Users connector's "Get manager" action, then fire a new approval to the manager directly.
Get manager (V2) — User UPN: [approver's email]
Start and wait for an approval — Assigned to: [manager's email]
Title: "[ESCALATED] " + original title
Details: "The original assignee did not respond within 3 days. ..."
This escalation pattern is the foundation of delegated sign-off, which we'll explore in depth after covering parallel patterns.
Some approvals don't have a natural order. A vendor onboarding request might need sign-off from IT (for system access), Procurement (for budget), and Legal (for compliance) — but there's no reason IT needs to wait for Procurement to finish before they start reviewing. Running these in parallel cuts the total approval time dramatically.
The secret to parallel approvals is using "Start an approval" (without the wait) for every branch, then "Wait for an approval" as a separate action after all branches have started. This fires all approval requests simultaneously before any blocking occurs.
Here's the pattern in pseudostructure:
[Parallel Branch 1] Start an approval → IT Security Review
[Parallel Branch 2] Start an approval → Procurement Review
[Parallel Branch 3] Start an approval → Legal Compliance Review
Wait for approval → IT Security Review (uses approval ID from branch 1)
Wait for approval → Procurement Review (uses approval ID from branch 2)
Wait for approval → Legal Compliance Review (uses approval ID from branch 3)
[Evaluate all three outcomes]
In the Power Automate designer, add a "Parallel branch" from the plus (+) icon below your trigger. Each branch gets its own "Start an approval" action. After the parallel branches converge, add three sequential "Wait for an approval" actions, each referencing the approval ID output from its corresponding start action.
Warning
The "Wait for an approval" action requires the approval ID (outputs('Start_an_approval')?['body/id']). Store each approval ID in a separate variable before the parallel branches converge, or use a Compose action in each branch. If you reference the output directly across branches, the dynamic content picker will show it, but it's easy to wire the wrong ID to the wrong wait action — double-check these connections carefully.
To add the parallel structure:
Each "Start an approval" action produces an id in its output body. Reference these as:
outputs('Start_an_approval_IT')?['body/id']outputs('Start_an_approval_Procurement')?['body/id']outputs('Start_an_approval_Legal')?['body/id']Pass each respective ID to the corresponding "Wait for an approval" action.
After all three approvals resolve, you need logic to determine whether the request passes. The two common policies are:
Unanimous approval required: All three must approve. Implement with:
And(
equals(outputs('Wait_for_IT')?['body/outcome'], 'Approve'),
equals(outputs('Wait_for_Procurement')?['body/outcome'], 'Approve'),
equals(outputs('Wait_for_Legal')?['body/outcome'], 'Approve')
)
Majority approval: At least two of three must approve. This requires counting approvals. Use a Compose action to build an array of outcomes, then count with length(filter(...)):
Compose Outcomes Array:
[
outputs('Wait_for_IT')?['body/outcome'],
outputs('Wait_for_Procurement')?['body/outcome'],
outputs('Wait_for_Legal')?['body/outcome']
]
Filter Array (ApproveCount):
From: outputs('Compose_Outcomes_Array')
Where: item() is equal to 'Approve'
Condition: length(body('ApproveCount')) >= 2
If you need to understand the filter array action mechanics in more depth, the transforming and mapping data with Power Automate's Compose, Select, and Filter Array actions article covers this thoroughly.
When any reviewer in a parallel chain rejects, you face an architectural choice: do you wait for all approvers to respond before acting on a rejection, or do you short-circuit as soon as you see the first reject?
Wait for all (recommended for auditing): Let all three waits complete, then evaluate. Even if IT rejected on day 1, Procurement and Legal still get to provide their review. This gives you a complete picture and avoids scenarios where you later ask "would Procurement have approved?"
Short-circuit on first rejection: This requires a more complex architecture using "Start an approval" across all three branches, then checking each outcome in sequence — if the first is rejected, skip the others. This saves reviewers time on requests that will clearly fail, but creates incomplete audit records.
For most organizational use cases, waiting for all responses and surfacing the complete set of decisions is better governance practice.
Key insight
When surfacing results for a parallel rejection, compile a summary that lists which stages approved and which rejected, with comments. A rejection notification that says "Your vendor onboarding request was not approved" tells the requester nothing. A summary that says "IT: Approved, Procurement: Rejected (budget not allocated for Q3), Legal: Approved" tells them exactly what to fix.
Delegation is where most approval workflows fail in production. Your carefully designed three-stage chain works perfectly until the VP goes on a two-week vacation, or the approver changes jobs, or someone's email bounces. You need explicit patterns for each of these scenarios.
The Office 365 Users connector includes a "Get mail tips for a mailbox" action that can check whether someone has automatic replies (out of office) enabled. Use this before firing an approval to decide whether to send the request to the primary approver or their backup.
Get mail tips for a mailbox
Mailbox address: finance_director@contoso.com
Condition: body('Get_mail_tips')?['automaticReplies']?['status'] is not equal to 'disabled'
Yes (OOO detected):
Get manager (V2) — User: finance_director@contoso.com
Start and wait for an approval — Assigned to: [manager email]
Title: "[DELEGATED - OOO] " + original title
No:
Start and wait for an approval — Assigned to: finance_director@contoso.com
The automaticReplies status field returns "alwaysEnabled", "scheduledEnabled", or "disabled". Both of the non-disabled states indicate OOO is active.
Tip
Log the delegation decision to your audit trail. Update a SharePoint column like "Approver Used" with the actual email address that received the request, plus a note if delegation occurred. "Approved by Sarah Chen (delegated from Marcus Rodriguez who was OOO)" is far more useful audit information than just "Approved."
Some organizations want requesters to nominate their own backup approver when submitting a request. This is common in regulated industries where the specific approver might need to be a named individual, not just whoever happens to be available.
Wire this up through your intake form. If you're using Microsoft Forms as your intake mechanism, automating Microsoft Forms responses covers how to pull form responses into your flow. Include a field like "If your primary approver is unavailable, who should receive this request?" and store that value in SharePoint alongside the request record.
In your flow, before each approval stage, check whether the designated delegate field is populated:
Condition: empty(triggerBody()?['DelegateApproverEmail']) is equal to false
Yes (delegate specified):
Condition: [Run OOO check on primary approver]
Primary OOO? → Send to delegate
Primary available? → Send to primary
No (no delegate specified):
[Run manager escalation path]
Power Automate doesn't natively support an approver reassigning a request to someone else through the approval interface — that's a Teams/Outlook experience limitation, not a Power Automate limitation. But you can build a reassignment mechanism by combining the Approvals connector with a separate "reassignment" flow.
The design: create a secondary Instant flow (triggered manually or via a Teams message) that accepts a request ID and a new assignee. This flow calls the HTTP action against the Approvals API to cancel the in-flight approval and reissue it to the new person.
The Approvals API endpoint for canceling an approval:
DELETE https://flow.microsoft.com/api/approvals/2016-06-01/environments/{environment-id}/approvalRequests/{approval-id}
This requires using the HTTP action with the First Party or managed identity authentication, which is an advanced pattern. A simpler approach for most teams: configure a "Comment" response option that signals reassignment is needed, then have a monitoring flow react to that comment by issuing a new approval to the indicated person. It's a workaround, but it's maintainable.
The most robust delegation pattern is a pre-defined escalation ladder: if the Level 1 approver doesn't respond in 3 days, escalate to Level 2. If Level 2 doesn't respond in 2 days, escalate to Level 3. Each level gets a progressively shorter window, reflecting the urgency of a delayed decision.
Build this as a "Do Until" loop around your approval action:
Initialize variable: EscalationLevel = 0
Initialize variable: ApprovalResolved = false
Do Until: ApprovalResolved equals true OR EscalationLevel > 3
Switch on EscalationLevel:
Case 0: Approver = primary_approver@contoso.com, Due = +3 days
Case 1: Approver = [manager of primary], Due = +2 days
Case 2: Approver = vp_operations@contoso.com, Due = +1 day
Case 3: Approver = ceo@contoso.com, Due = +1 day
Start and wait for an approval
Condition: outcome is "No response"
Yes: Increment EscalationLevel
No: Set ApprovalResolved = true
Set ApprovalChainStatus = outcome
Warning
"Do Until" loops in Power Automate have a default limit of 60 iterations and a default timeout of 60 minutes of execution time. For approval escalation, the execution time isn't a concern (the flow sleeps during waits), but you should set the loop count limit to match your maximum escalation levels to prevent runaway loops. Configure this in the loop's settings panel under "Change limits."
An approval chain that lives only in email is half-built. Your most effective workflows weave together multiple M365 services to create a cohesive experience. Here are the integration patterns that matter most.
For document-centric approvals like contracts, policies, or design documents, SharePoint should be your authoritative store. Power Automate with SharePoint for document approvals covers the baseline SharePoint integration, but multi-stage chains add some nuances.
Use SharePoint columns to track stage-by-stage state:
Update these columns at each stage transition using "Update item." This makes the SharePoint list a live dashboard of where every request stands — something your team members can check without interrogating anyone.
Update item
List: Contracts
ID: triggerBody()?['ID']
ApprovalStage: "Finance Review"
CurrentApprover: "finance_director@contoso.com"
LastDecision: "Legal: Approved — Contract terms are standard (John Smith, 2024-03-15)"
If your approvers are regular Teams users, the Teams Approvals app is significantly more convenient than email for reviewing and acting on requests. Power Automate's approval notifications go to Teams automatically when the approver has the Approvals app, but you can augment this with a proactive Teams message using the Teams connector.
After firing each approval action, send the approver a Teams chat message as a heads-up:
Post message in a chat or channel (Teams)
Post as: Flow bot
Post in: Chat with Flow bot
Recipient: [approver email]
Message: "Hi [approver name] — a new approval request is waiting for you in the Approvals tab:
**[Contract Title]**
Due: [due date]
You can respond directly in Teams or click the link in your email."
This nudge reduces response times significantly, because people see it in their regular Teams context rather than hunting through approval emails.
For more on building rich Teams automation patterns including adaptive cards for approvals, using Power Automate with Microsoft Teams covers the full connector surface area.
While approvers respond through Teams or the approval email, the requester deserves a richer notification experience than the default system messages. Use the Office 365 Outlook connector to send styled HTML emails at each stage transition.
Send a "Your request has moved to the next stage" email when Stage 1 completes:
<body style="font-family: Segoe UI, sans-serif; padding: 20px;">
<h2 style="color: #0078d4;">Contract Approval Update</h2>
<p>Your contract <strong>[Contract Title]</strong> has completed the Legal Review stage.</p>
<table style="border-collapse: collapse; width: 100%;">
<tr><td style="padding: 8px; font-weight: bold;">Stage</td><td style="padding: 8px;">Legal Review</td></tr>
<tr><td style="padding: 8px; font-weight: bold;">Decision</td><td style="padding: 8px; color: green;">Approved ✓</td></tr>
<tr><td style="padding: 8px; font-weight: bold;">Reviewer</td><td style="padding: 8px;">[approver name]</td></tr>
<tr><td style="padding: 8px; font-weight: bold;">Comments</td><td style="padding: 8px;">[comments]</td></tr>
<tr><td style="padding: 8px; font-weight: bold;">Next Stage</td><td style="padding: 8px;">Finance Review (est. 3 business days)</td></tr>
</table>
</body>
See automate email notifications with Power Automate for patterns on building dynamic HTML email content at scale.
For multi-stage chains involving more than three approvers, consider creating a Planner task for each stage. The task represents the review assignment; when the approver completes their review in Power Automate, the flow marks the Planner task complete.
This gives project managers visibility into the approval pipeline through Planner boards — they can see at a glance which stage is in progress, which are complete, and which are blocked. The automating Planner task creation, assignment, and status updates article covers the Planner connector in detail.
Real-world approval chains rarely fit cleanly into "all sequential" or "all parallel." The most common enterprise pattern is a hybrid: some stages must happen in order, but within a stage, multiple reviewers might need to respond simultaneously.
Consider a capital expenditure (CapEx) approval for a $500,000 server purchase:
The architecture nests a parallel block inside a sequential chain:
Stage 1: IT Director Sequential Approval
└─ [Approved] → Stage 2 Parallel Block
├─ Start approval: CFO
├─ Start approval: CTO
├─ Wait for CFO
├─ Wait for CTO
└─ [Both Approved] → Stage 3: CEO Sequential Approval
└─ [Approved] → Mark Final Approved
The key design decision here: what happens if one of the Stage 2 parallel approvers rejects? Do you cancel the other parallel approval that's still pending, or let it run to completion?
If you want to cancel an in-flight approval when a sibling rejects, you need the HTTP action against the Approvals API to cancel the specific approval by ID — this is the same pattern mentioned in the reassignment section. If you're comfortable with the added complexity, it's a better user experience (why ask the CTO to review something the CFO already killed?). If you need to keep things maintainable, letting both run to completion is the pragmatic choice.
Note
The hybrid sequential/parallel pattern can create deeply nested condition blocks if you're not deliberate about structure. Use Scope actions to group each stage's logic into a named block — "Stage 1: IT Director Review," "Stage 2: CFO + CTO Review," etc. Scope actions are collapsible in the designer, making the overall flow readable even when each stage has 10-15 actions inside it. If you want to go further and modularize each stage as a reusable child flow, orchestrating child flows and scoped execution in Power Automate covers that architecture.
Build a three-stage vendor onboarding approval chain. The scenario: your company is onboarding a new SaaS vendor that will have access to customer data. The chain requires sequential approval from IT Security, then Procurement, with a parallel sign-off from Legal and Privacy as a combined Stage 2.
Your intake: A SharePoint list called "Vendor Requests" with columns: VendorName, ContractValue, DataAccessRequired (Yes/No), RequestedBy, PrimaryContact.
Stage 1: IT Security Review (Sequential)
OverallStatus = "Pending", AuditTrail = "".concat('IT Security Review: ', triggerBody()?['VendorName'])addDays(utcNow(), 3)outcome equals "Approve" → continue. outcome equals "Reject" → set OverallStatus = "Rejected at IT Security", terminate. outcome equals "No response" → escalate to IT Security Manager.Stage 2: Parallel Legal and Privacy Review
In the Yes branch of Stage 1's condition:
Stage 3: Procurement Final Sign-Off (Sequential)
In the Yes branch of Stage 2's unanimous check:
Verification: Test with two scenarios — a clean run where all three stages approve, and a rejection at Stage 2 (Privacy rejects). Verify that your SharePoint columns update at each transition, your AuditTrail variable captures all decisions and comments, and the requester notification emails contain meaningful content.
If your flow runs under a specific user's connection, approval notifications sent "from" that connection appear to come from that user in some contexts. This isn't usually a functional problem, but it can confuse approvers who see "John (IT Admin) is requesting your approval" when the actual request came from Maria in Finance. Use a service account or the flow's own system identity where possible, and make sure the approval Title and Details make the actual requester's identity obvious.
When parallel branches converge, dynamic content from inside those branches isn't always accessible to subsequent actions. Specifically, outputs from "Start an approval" actions inside parallel branches can be tricky to reference after the branches merge. The fix: at the end of each parallel branch, use Compose actions to explicitly capture the values you need (approval ID, title, etc.) and store them in flow-level variables initialized before the parallel block. Variables are globally scoped; outputs from branch-local actions are not reliably so.
Developers test their flows with "Approve" and "Reject," then discover in production that approvers ignore requests and the flow is in an unknown state. Always handle the "No response" outcome explicitly. At minimum, send an escalation notification. At best, run the full escalation ladder described in the delegation patterns section.
When you have 50 pending approvals in the Approvals center, an approval called "Contract Review" is useless. Make titles specific and actionable: concat('LEGAL REVIEW [', triggerBody()?['VendorName'], '] - ', triggerBody()?['ContractValue'], ' contract - Due ', addDays(utcNow(), 3)). Approvers can scan and prioritize at a glance.
The comments field in the approval response is one of the most valuable pieces of data you collect — it's where approvers explain conditional approvals, flag concerns, or note exceptions. Many flows capture the binary outcome (Approve/Reject) but discard the comments. Always append comments to your AuditTrail variable and include them in downstream notifications.
If your flow shows "Running" but the approver claims they responded, check:
For systematic debugging approaches, using Power Automate Run History and Flow Checker to debug and fix failing flows covers the debugging toolset in detail.
Approval notifications can fail to arrive for several reasons:
approvals@microsoft.com sender might be flagged by the organization's spam filter. IT needs to whitelist this address.Warning
If you're building approval chains for a multi-national organization, be careful about whose Microsoft 365 account credentials the approval connection uses. The Approvals backend is region-aware, and approvals created under a US-based account's connection may not be accessible in the EU Approvals center for European approvers. Test cross-region scenarios explicitly if your organization spans multiple M365 regions.
You now have a complete architecture toolkit for multi-stage approval chains in Power Automate. The sequential pattern gives you ordered sign-off with clear stage gates and rejection handling. The parallel pattern lets multiple reviewers work simultaneously, with flexible unanimous or majority outcome logic. The delegation patterns — OOO detection, requester-defined delegates, escalation ladders — keep your chains moving even when the expected approver isn't available.
The M365 connector integrations tie it all together: SharePoint as the auditable system of record, Teams for the approval experience people actually use, Outlook for rich requester notifications, and Planner for project-level visibility.
The single most important habit to build: always design for the non-happy path first. Who handles it if the approver doesn't respond? What if they reject? What if the approval action itself fails? The operational reliability of your approval chain is determined by how well you've answered those questions, not by how smooth the approval path is.
Where to go from here: