Most approval flows assume every approver is always available — and they break the moment someone goes on vacation. This lesson teaches you to build multi-approver chains that automatically detect out-of-office status, route to configured delegates, escalate to managers, and preserve complete request context across every hop.

Picture this: your organization runs a capital expenditure approval process that requires sign-off from a department manager, a finance controller, and the CFO — in sequence. A $47,000 server refresh request gets submitted on a Tuesday. The department manager approves it Wednesday morning. By Thursday, the finance controller is three days into a two-week vacation with their out-of-office turned on. The request sits. Fourteen days later, the requestor sends a frustrated email asking what happened, and someone has to manually dig through the approval chain to figure out where it got stuck. The server refresh deadline passes. The whole process starts over.
This is not a hypothetical. It plays out in organizations every week, and it happens not because people are irresponsible but because most approval flows are built as if everyone will always be available. The approvers are hardcoded. There's no fallback. There's no proxy. When someone is out, the flow just sits there like a patient golden retriever waiting at the door for someone who isn't coming home.
By the end of this lesson, you'll know how to build approval flows that actually account for the messy reality of human availability. You'll handle out-of-office detection, proxy approver lookups, dynamic reassignment, and fallback routing — all while preserving the full request context so that whoever steps in knows exactly what they're approving and why.
What you'll learn:
Before working through this lesson, you should be comfortable with:
You'll also need a Power Automate environment with a premium license (to use HTTP actions for Graph API calls), a SharePoint site where you can create lists, and an Azure AD app registration for Graph API access — or willingness to use the "Send an HTTP request to SharePoint" connector as an alternative for OOF detection.
Before writing a single action, let's establish what we're actually building. A resilient multi-approver flow has three concerns that a naive implementation ignores entirely.
Approver availability is the first. You cannot assume the next approver in the chain will be reachable when the request lands at their stage. Out-of-office is the obvious case, but there's also no-reply situations where the approver receives the notification but has inbox rules routing approval emails to a folder they haven't checked in a week.
Delegation registry is the second. Your flow needs a source of truth for who covers whom. This isn't something you hardcode into the flow — it's data, and data belongs in a list. We'll use a SharePoint list as a lightweight delegation registry that HR or team leads can update without touching the flow at all.
Context persistence is the third and most underestimated. Every time you reassign or route to a fallback, you risk losing context. The proxy approver needs to know who originally submitted the request, what stage is this in the chain, what did the prior approvers say, and what is the actual thing being approved. If they have to go hunting for this information, they either delay or approve blindly — neither is acceptable.
The architecture looks like this: a SharePoint list stores the request. A separate SharePoint list stores the delegation registry. When the flow reaches an approver step, it first checks OOF status, then checks for a proxy, then sends to either the original approver or their proxy with the full context embedded. If neither is available, it escalates to the approver's manager via a Graph API lookup. All of this happens automatically, and the original request record in SharePoint is updated at every step.
The delegation registry is your foundation. Without it, every OOF detection leads to the same blunt fallback — send it to the manager. The registry gives you precision: it lets individuals configure their own coverage in advance.
Create a SharePoint list called ApprovalDelegates with these columns:
The reason you include both explicit date ranges and an IsActive toggle is that humans don't always set end dates accurately. Someone sets a delegation for a two-week vacation, returns early, and forgets to cancel it. The IsActive field lets them flip it off immediately. Your flow will check IsActive first, then validate that today falls within the date range.
Tip
Add a SharePoint view to this list called "Active Delegations" that filters for IsActive equals Yes and DelegateEndDate greater than today. This makes it easy for people to audit who currently has a delegate configured without building a separate tool for it.
Now build a Child Flow — a separate, instant-cloud flow that other flows can call — called GetDelegateForApprover. This keeps your lookup logic centralized and reusable across multiple approval flows in your environment.
The child flow accepts a single input called ApproverEmail (string). It does the following:
Title eq 'ApproverEmail_value' and IsActive eq 1empty(body('Get_items')?['value']) equals false)greaterOrEquals(utcNow(), item()?['DelegateStartDate']) and lessOrEquals(utcNow(), item()?['DelegateEndDate'])DelegateEmail if valid, or an empty string if no active delegation existsThe return schema should include two properties: HasDelegate (boolean) and DelegateEmail (string). This makes the consuming flow's conditional logic clean and readable.
This is where many tutorials stop, and where the real complexity begins. The Microsoft Graph API exposes automatic reply settings for users via the /users/{userPrincipalName}/mailboxSettings endpoint. The response includes an automaticRepliesSetting object with a status field that can be disabled, alwaysEnabled, or scheduled.
Warning
This Graph API call requires the MailboxSettings.Read permission (delegated or application). If you're using an Azure AD app registration with client credentials (the recommended approach for flows), request this as an application permission and have your tenant admin consent to it. Without the right permission, you'll get a 403 and your OOF check will silently fail.
In your flow, use an HTTP action with the following configuration:
Method: GET
URI: https://graph.microsoft.com/v1.0/users/@{variables('CurrentApproverEmail')}/mailboxSettings
Headers:
Authorization: Bearer @{body('Get_Graph_Token')?['access_token']}
Content-Type: application/json
You'll need to get the token first with a separate HTTP POST to https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token with your client credentials. If you haven't worked with this pattern before, the Connecting Power Automate to External APIs and Services lesson covers the OAuth2 client credentials flow in detail.
Parse the response with a Parse JSON action using this schema for the relevant portion:
{
"type": "object",
"properties": {
"automaticRepliesSetting": {
"type": "object",
"properties": {
"status": { "type": "string" },
"scheduledStartDateTime": {
"type": "object",
"properties": {
"dateTime": { "type": "string" }
}
},
"scheduledEndDateTime": {
"type": "object",
"properties": {
"dateTime": { "type": "string" }
}
}
}
}
}
}
Now build your OOF detection condition. The approver is considered out-of-office if:
status equals alwaysEnabled, ORstatus equals scheduled AND the current UTC time is between scheduledStartDateTime and scheduledEndDateTimeExpress this in your condition using or() and and() expressions. The scheduled case requires two comparisons:
@and(
equals(body('Parse_Mailbox_Settings')?['automaticRepliesSetting']?['status'], 'scheduled'),
greaterOrEquals(utcNow(), body('Parse_Mailbox_Settings')?['automaticRepliesSetting']?['scheduledStartDateTime']?['dateTime']),
lessOrEquals(utcNow(), body('Parse_Mailbox_Settings')?['automaticRepliesSetting']?['scheduledEndDateTime']?['dateTime'])
)
Store the result in a boolean variable called ApproverIsOOF. This separates the detection logic from the routing logic, which matters when you're debugging a run that took an unexpected path.
Note
The Graph API's mailboxSettings endpoint reflects what the user has configured in Outlook. It does not know whether someone simply hasn't checked their email in four days. OOF detection handles the declared absence case. For handling non-responsive approvers more broadly, you'll want to combine this with a timeout and escalation pattern — covered in the Automating Time-Sensitive Approval Escalations in Power Automate: Using Do Until Loops, Timeout Conditions, and Teams Notifications to Enforce SLA Deadlines lesson.
You now have two signals: whether the approver is OOF, and whether they have a configured delegate. Combine them into a clean resolution function — another child flow called ResolveEffectiveApprover — that outputs the email address who should actually receive the approval task.
The resolution logic works as a priority chain:
approvals-fallback@yourorg.com)The manager lookup uses the Graph API endpoint:
GET https://graph.microsoft.com/v1.0/users/{approverEmail}/manager
This returns the manager's profile. Extract the mail property for the manager's email. Wrap this call in a try-catch using the "Configure run after" settings on a subsequent action — if the HTTP action fails (maybe the user has no manager configured in AAD), set the effective approver to your fallback mailbox.
Key insight
The priority chain above is a design choice, not a law. Some organizations want delegates to always receive requests even when the original approver is in office (true proxy model). Others want OOF to be the only trigger. Talk to your stakeholders before building. The right time to have this conversation is before writing a single action in the designer, not after you've deployed to production.
Store the resolved approver email in a string variable called EffectiveApproverEmail. This is what gets passed to your approval action. The original approver's email is kept separately in OriginalApproverEmail because you'll need it for context and audit trail.
Here's where most delegation implementations fall apart. They route the request to the right person but give that person a stripped-down approval notification with no idea what they're looking at or why they received it instead of the original approver.
Your approval request needs to carry several categories of information:
Request identity: Who submitted it, when, what is the unique ID of the request (your SharePoint item ID), and what is the request title/description.
Financial and business context: For a CapEx request, this means the dollar amount, the cost center, the vendor, the business justification, and any attachments (link to the SharePoint document, not the file itself — embedding files in approval notifications hits size limits fast).
Chain state: Which approval stage is this (Stage 1 of 3, Stage 2 of 3), what did prior approvers decide, and did any of them add comments.
Delegation context: If this was routed to a proxy or fallback, say so explicitly. "You are receiving this because Sarah Chen (sarah.chen@org.com) is currently out of office" is far more useful than a request that appears to have come from nowhere.
Build a Compose action called BuildApprovalContext that assembles this as a formatted string. For the "Details" field of a Start and wait for an approval action, you can use markdown-style formatting:
**Request:** @{triggerOutputs()?['body/Title']}
**Requested by:** @{triggerOutputs()?['body/RequestorEmail']}
**Submitted:** @{formatDateTime(triggerOutputs()?['body/Created'], 'dddd, MMMM d yyyy')}
**Amount:** $@{formatNumber(triggerOutputs()?['body/RequestAmount'], 'N2')}
**Cost Center:** @{triggerOutputs()?['body/CostCenter']}
**Business Justification:** @{triggerOutputs()?['body/Justification']}
---
**Approval Stage:** @{variables('CurrentStage')} of @{variables('TotalStages')}
**Original Approver:** @{variables('OriginalApproverEmail')}
@{if(not(equals(variables('EffectiveApproverEmail'), variables('OriginalApproverEmail'))), concat('**⚠️ Delegated:** You are acting as proxy for ', variables('OriginalApproverEmail'), ' (', variables('DelegationReason'), ')'), '')}
---
**Prior Approval History:**
@{variables('ApprovalHistoryLog')}
**Supporting Documents:** @{variables('DocumentLink')}
The ApprovalHistoryLog variable is a string that you append to at each stage. After each approver responds, do something like:
@{concat(variables('ApprovalHistoryLog'),
'• Stage ', variables('CurrentStageNumber'),
' – ', variables('EffectiveApproverEmail'),
' (', formatDateTime(utcNow(), 'MMM d, h:mm tt'), '): ',
outputs('Start_and_wait_for_an_approval')?['body/outcome'],
' – "', outputs('Start_and_wait_for_an_approval')?['body/responses'][0]?['comments'], '"',
'\n')}
This running log means that by the time the CFO sees the request at Stage 3, they can read the exact words the department manager and finance controller wrote when they approved it at Stages 1 and 2. That context is often the deciding factor.
Now let's wire everything together. The main flow iterates through an array of approvers using an Apply to each loop. This is better than building sequential hardcoded stages because it's dynamic — the approver array can be built from SharePoint column lookups, form inputs, or dollar-threshold routing logic.
Initialize these variables at the start of your flow:
CurrentApproverEmail (String) – ""
OriginalApproverEmail (String) – ""
EffectiveApproverEmail (String) – ""
DelegationReason (String) – ""
ApproverIsOOF (Boolean) – false
ApprovalHistoryLog (String) – ""
CurrentStageNumber (Integer) – 0
FlowShouldContinue (Boolean) – true
FinalOutcome (String) – ""
Build an array variable called ApproverChain that holds the ordered list of approvers. For a CapEx flow, this might be populated by a Switch condition based on the request amount:
["dept.manager@org.com", "finance.controller@org.com"]["dept.manager@org.com", "finance.controller@org.com", "cfo@org.com"]["dept.manager@org.com", "vp.operations@org.com", "finance.controller@org.com", "cfo@org.com"]The routing logic that builds this array is a great candidate for the dynamic expression patterns covered in Mastering Dynamic Expressions and the Power Automate Formula Language: String, Date, and Array Functions for Real-World Data Manipulation.
Warning
Apply to each loops in Power Automate run sequentially by default, which is what you want for an approval chain. However, if you ever turn on concurrency settings on an Apply to each, you'll break the sequential nature and multiple approval stages will fire simultaneously. Leave concurrency off for approval chain loops.
Inside the Apply to each, the action sequence is:
Step 1 – Skip if chain should stop:
A condition checking FlowShouldContinue equals true. If false (previous approver rejected), the loop body does nothing and the iteration completes without firing an approval. This is how you short-circuit a rejection without using Terminate — which kills the flow and prevents cleanup actions from running.
Step 2 – Increment stage counter and set approver variables:
Set CurrentStageNumber: add(variables('CurrentStageNumber'), 1)
Set OriginalApproverEmail: items('Apply_to_each')
Set CurrentApproverEmail: items('Apply_to_each')
Set DelegationReason: ""
Step 3 – Call ResolveEffectiveApprover child flow:
Pass CurrentApproverEmail as input. Get back EffectiveApproverEmail, HasDelegate, DelegateEmail, ApproverIsOOF.
Step 4 – Update DelegationReason:
If ApproverIsOOF is true and HasDelegate is true: set DelegationReason to "out-of-office, delegate configured"
If ApproverIsOOF is true and HasDelegate is false: set DelegationReason to "out-of-office, escalated to manager"
Step 5 – Build approval context: Run the Compose action that assembles the full context payload described in the previous section.
Step 6 – Start and wait for an approval:
Approval type: Approve/Reject - First to respond
Title: [Request Title] – Stage @{variables('CurrentStageNumber')} of @{length(variables('ApproverChain'))}
Assigned to: @{variables('EffectiveApproverEmail')}
Details: @{outputs('BuildApprovalContext')}
Item link: @{variables('SharePointItemLink')}
Item link description: View Request in SharePoint
Step 7 – Append to history log:
Update ApprovalHistoryLog with the stage outcome and comments.
Step 8 – Update SharePoint item: Update the request's SharePoint list item with the current stage, current effective approver, and the outcome so far. This is critical — if your flow runs for hours or days across multiple stages and something fails, the SharePoint record reflects exactly where you are.
Step 9 – Check outcome:
If the outcome is "Reject", set FlowShouldContinue to false and set FinalOutcome to "Rejected". The loop continues to iterate (you can't break out of Apply to each) but the condition at Step 1 prevents any further approval actions from firing.
There's a scenario the loop above doesn't cover cleanly: what if an approver wants to manually reassign the request to someone else — not a pre-configured delegate, but an ad-hoc "send this to Janet instead"?
The approval task in Power Automate doesn't natively support reassignment within the same running flow. What you can build instead is a "Reassign" response option. When you use the "Custom Responses" approval type, you can add a "Reassign" button alongside "Approve" and "Reject."
Approval type: Custom Responses - Wait for all responses
Title: [Request Title] – Stage @{variables('CurrentStageNumber')} Approval
Assigned to: @{variables('EffectiveApproverEmail')}
Response options:
- Approve
- Reject
- Reassign
When the response comes back as "Reassign," you need a way to capture who the approver wants to reassign to. This is where the approval Comments field becomes a protocol field. Document in the approval instructions that to reassign, the approver should type "REASSIGN: recipient@email.com" in the comments.
Then after the approval action, check the outcome:
If outcome equals 'Reassign':
Extract email from comments using:
last(split(outputs('Start_and_wait_for_an_approval')?['body/responses'][0]?['comments'], 'REASSIGN: '))
Set EffectiveApproverEmail to extracted email
Set DelegationReason to "manually reassigned by [OriginalApproverEmail]"
Re-run the approval action for this stage with the new approver
Re-running means you need a Do Until loop wrapping the approval action for each stage — not just the Apply to each. The Do Until exits when the outcome is either "Approve" or "Reject." This is an important architectural choice: the Apply to each handles progression through stages, while a Do Until inside each stage iteration handles reassignment within a stage.
Do Until (outcome equals 'Approve' OR outcome equals 'Reject' OR reassignment count > 3):
→ Send approval to EffectiveApproverEmail
→ If Reassign: update EffectiveApproverEmail, increment reassignment count, log to history
Capping reassignment at 3 prevents an infinite loop where someone keeps reassigning. After 3 reassignments within a single stage, escalate to the fallback approver automatically.
Tip
For the reassignment email parsing, trim whitespace from the extracted email using trim(last(split(...))) — approvers will sometimes type "REASSIGN: recipient@email.com" with extra spaces, and that extra space will cause the approval to fail silently by sending to an invalid address.
Multi-stage approval chains can run for days. A $50K CapEx request that requires four approvers, with each potentially being OOF or needing reassignment, could realistically be in-flight for a week. During that time, your flow is in a suspended state between approval stages.
Power Automate flows can run for up to 30 days, so duration isn't the problem. The problem is that any context you stored only in variables is preserved in the flow run's memory — but if the flow is explicitly cancelled and restarted for any reason, that context is gone. This is why you write context to SharePoint at every stage.
Design your SharePoint list columns to be a complete audit trail:
Update these with "Update item" actions after every approval action completes. Yes, this adds latency to each stage, but for an approval workflow spanning hours or days, a few seconds of additional overhead is irrelevant compared to the value of a complete, queryable audit trail.
If you're using a SharePoint-based approach, you can also use the Send an HTTP request to SharePoint action to update specific columns using REST, which is faster than the full "Update item" action for large lists. This is part of a broader pattern for Automating SharePoint List Item Lifecycle Management with Power Automate.
Every participant in the approval chain deserves to know what's happening: the original requestor, the approvers, and the people who got delegated to unexpectedly.
When delegation occurs, send an additional email to the original approver (even though they're OOF):
To: @{variables('OriginalApproverEmail')}
Subject: Your approval task has been routed to @{variables('EffectiveApproverEmail')}
Body:
A request requiring your approval has been routed to [EffectiveApproverEmail]
because you currently have an out-of-office configured.
Request: [Title]
Stage: [CurrentStage] of [TotalStages]
This is informational — no action required from you.
If this is incorrect, please contact [EffectiveApproverEmail] directly.
When the final outcome is reached, send a comprehensive summary email to the requestor. This is covered in depth in the Automate Email Notifications with Power Automate lesson, but the key here is that your summary email should include the full ApprovalHistoryLog variable — every stage, every approver (original and effective), every comment. Give the requestor the complete picture, not just "Approved" or "Rejected."
Key insight
Notify the proxy approver when they receive a delegated task with a clear subject line prefix like "[DELEGATED]" so they recognize it's not their usual workflow. Many proxy approvers will see the task in their inbox and assume it was sent to them in error if there's no delegation context in the notification itself. Clarity here prevents tasks from being ignored.
For approvals happening through Teams, the Using Power Automate with Microsoft Teams: Automate Notifications, Approvals, and Channel Messages lesson walks through sending adaptive card notifications alongside the email, which gives approvers two channels to respond from.
A delegation flow has more failure points than a simple sequential approval because each routing step introduces an external call that can fail: the Graph API call for OOF status, the child flow call for delegate lookup, the manager lookup, the fallback email send.
Wrap each of these in proper error handling using "Configure run after" settings. For the OOF check HTTP action, configure the next action to run "has failed" as well as "is successful," and set a variable OOFCheckFailed to true in that case. Your routing logic should treat a failed OOF check the same as "not OOF" — send to the original approver and don't block the chain.
The philosophy: a failed status check should never stop an approval from being sent. Default to the safest behavior (send to original approver) rather than halting. Halting means nothing gets approved, which is almost always worse than an imperfect routing decision.
For the manager lookup fallback, always have the hardcoded fallback email as a last resort. Your error handling chain should look like:
OOF check succeeds → proceed with result
OOF check fails → assume not OOF, send to original approver, log warning
Delegate lookup succeeds with delegate → send to delegate
Delegate lookup succeeds with no delegate → try manager lookup
Manager lookup succeeds → send to manager
Manager lookup fails → send to hardcoded fallback, send alert to flow owner
Log the fallback email as a trigger for a manual review. Send a message to a Teams channel or a monitoring email address when the ultimate fallback fires — someone should know the automatic resolution chain couldn't find a better option. This ties into the broader pattern from Master Error Handling and Retry Patterns in Power Automate for Bulletproof Flows.
Build the following flow from scratch in your development environment. Use the CapEx scenario as your template.
Setup:
Create a SharePoint list called CapExRequests with columns: Title, RequestorEmail, RequestAmount (number), CostCenter (text), Justification (multiline text), Status (choice: Draft/Pending/Approved/Rejected), CurrentStage (number), FullApprovalLog (multiline text).
Create the ApprovalDelegates SharePoint list as described earlier in this lesson.
Create an Azure AD app registration with MailboxSettings.Read and User.Read.All application permissions, consented by a tenant admin. Note the client ID, client secret, and tenant ID.
Build the flow:
Trigger: When an item is created in CapExRequests (SharePoint trigger, filter Status equals Draft).
Initialize all variables as described in the implementation section.
Build a Switch on RequestAmount to populate the ApproverChain variable:
Build the OOF detection child flow using the Graph API. Test it against your own account: turn on OOF in Outlook, trigger the flow, verify the detection works, then turn it off.
Build the ApprovalDelegates lookup child flow. Add yourself as a delegate for one of your test approvers. Verify the child flow returns your email correctly.
Implement the main Apply to each loop with the Do Until inside for reassignment support (cap at 2 reassignments for testing).
Add the context payload assembly and verify the full approval history log appears correctly in Stage 2 and 3 notifications.
Test three scenarios:
Check your SharePoint list after each run. Verify the FullApprovalLog column has the complete history including delegation reasons.
Graph API returns 403 on mailboxSettings
This almost always means missing permissions. Check that your Azure AD app has MailboxSettings.Read as an application permission (not delegated) and that an admin has granted consent. In the Azure portal, go to your app registration, API Permissions, and look for a green checkmark next to MailboxSettings.Read. If it's yellow or absent, request admin consent.
OOF detection fires for "scheduled" but the user isn't actually away
The Graph API reports "scheduled" status even if the scheduled window is in the future. Your condition must validate the current time is within the scheduledStartDateTime and scheduledEndDateTime window — not just check that the status is "scheduled." Double-check your greaterOrEquals(utcNow(), ...) expression is comparing UTC to UTC. If the scheduled times come back in a non-UTC timezone, you'll get false positives.
Apply to each processes approvers in wrong order
Check that you're not accidentally using the array initialization expression createArray(...) in a way that evaluates alphabetically rather than by your intended priority. Log each items('Apply_to_each') value at the start of each iteration to confirm the order matches your ApproverChain array.
Reassignment email parsing fails silently
The last(split(...)) expression will return the original comment string (with no "REASSIGN:" prefix) if the comment doesn't contain that keyword. Add a contains(comments, 'REASSIGN:') condition before attempting the parse. If the condition fails, treat the response as an error and send a message back to the approver explaining the correct format.
Child flow returns empty results despite a valid delegation existing
Check the OData filter case sensitivity. SharePoint OData filters for text fields are case-insensitive, but email address capitalization can sometimes cause mismatches if you stored addresses with mixed case and are querying with a different case. Normalize emails to lowercase using toLower() before storing them in the list and before passing them to the child flow query.
Approval history log grows too large
Multi-line text columns in SharePoint have a 63,999 character limit. For a 10-stage approval with detailed comments, you can hit this. Consider trimming comments to 200 characters in the log string (use substring(comments, 0, min(200, length(comments)))), or move the full log to a separate list with one row per approval event.
Flow times out while waiting for an approval Power Automate approval actions time out after 28 days by default. If your SLA requires responses within a specific window, implement a parallel branch using a Scope action — one branch waits for the approval, the other uses a Delay Until action with your deadline, then sends a cancellation. The first branch to complete terminates the other. This pattern is covered in depth in the Automating Recurring Approval Reminders in Power Automate: Escalating Overdue Requests, Notifying Delegates, and Closing Stale Approvals Automatically lesson.
You've built something that most organizations don't have: an approval chain that survives contact with reality. When approvers are out of office, the request doesn't stall — it routes to a configured delegate, or escalates to the approver's manager, or falls back to a monitored mailbox. When someone needs to manually reassign, there's a protocol for that too. And at every step, the person receiving the request knows exactly what it is, where it came from, what stage they're at, and what the previous approvers said.
The key architectural decisions that make this work:
Where to go from here: if your approval chain needs to handle dollar-threshold-based routing decisions more sophisticatedly — routing to different approver chains based on combinations of department, amount, and request type — the Building a Dynamic Multi-Condition Approval Flow in Power Automate: Routing Requests to Different Approvers Based on Form Input, Dollar Thresholds, and Department Rules lesson goes deep on that routing logic.
If you want richer, more interactive approval notifications with inline data displays and conditional response options, explore Implementing Adaptive Card-Based Human-in-the-Loop Approvals in Power Automate: Dynamic Forms, Contextual Data Injection, and Response Handling, which replaces the plain-text approval detail string with fully formatted cards that render beautifully in both email and Teams.
The approval flows you build from this point forward should never again be the reason a $47,000 server refresh gets delayed because someone went on vacation.