Build a complete expense approval workflow from scratch: a SharePoint list captures submissions, Power Automate routes them to the right manager, and approval outcomes are written back automatically. By the end, you'll have a fully auditable, hands-off process replacing email chains and manual spreadsheet updates.

Here's a scenario that plays out in organizations every week: an employee fills out an expense report, emails it to their manager, the manager replies with "approved" (or forgets to reply entirely), and then someone in Finance manually updates a spreadsheet with the outcome. When auditors ask about the approval chain for a $3,200 hotel stay from six months ago, the answer is a forwarded email chain buried in someone's inbox. That's not a process — it's a liability.
Power Automate, working alongside SharePoint and the built-in Approvals connector, gives you the infrastructure to replace that chaos with something auditable, consistent, and largely self-running. By the end of this lesson, you'll have built a complete expense submission workflow: a SharePoint list captures the submission, a flow triggers automatically when a new item is added, an approval request routes to the submitter's manager, and the outcome — approved or rejected, with comments — is written back to that same list item. The entire audit trail lives in SharePoint, queryable and reportable.
What you'll learn:
This lesson assumes you're comfortable with the Power Automate designer interface and have built at least one working flow before. You should understand how triggers work in Power Automate and have a working knowledge of conditions and branching logic. You'll need:
Office 365 Users connector can resolve a manager from an email address)Note
If your organization's Azure AD manager fields aren't populated, the "Get manager" step will fail silently or throw an error. Check this early by opening the Microsoft 365 People card for a test user and confirming their manager is listed. HR or IT can bulk-populate this if needed.
Before writing a single flow action, you need a well-designed list. The list schema is the foundation everything else depends on, and fixing it after your flow is in production is painful.
Create a new SharePoint list called ExpenseSubmissions in your site. Here are the columns you'll need, along with their types and reasoning:
| Column Name | Type | Notes |
|---|---|---|
| Title | Single line of text | Use this as the expense description ("Q3 Client Dinner - Chicago") |
| SubmitterEmail | Single line of text | Captures who submitted — don't rely on the Created By field alone |
| ExpenseAmount | Currency | Enables threshold-based routing later |
| ExpenseDate | Date and Time | The date the expense was incurred |
| ExpenseCategory | Choice | Travel, Meals, Software, Equipment, Other |
| BusinessJustification | Multiple lines of text | Required context for the approver |
| ReceiptAttached | Yes/No | Whether a receipt was uploaded |
| ApprovalStatus | Choice | Pending, Approved, Rejected — default to Pending |
| ApproverEmail | Single line of text | Populated by the flow after resolution |
| ApprovalComments | Multiple lines of text | Populated by the flow with the approver's response |
| ApprovedDate | Date and Time | Populated by the flow when a decision is made |
Take a moment to set the default value of ApprovalStatus to Pending in the column settings. This way, any new item is immediately recognizable as awaiting action, and your flow doesn't need to set it.
Tip
Add a list view filtered to ApprovalStatus equals Pending and share it with the Finance team. They'll get a live dashboard of everything awaiting approval without touching the flow at all.
For the ExpenseCategory column, define at least these choices: Travel, Meals & Entertainment, Software & Subscriptions, Office Equipment, Professional Development, Other. These categories will matter if you later extend this flow to route different categories to different approvers or cost centers.
Once your list is built, test it by manually creating one or two sample items. Make sure the currency field accepts decimal values and that the choice columns display their dropdowns correctly. Catching schema problems before the flow references the columns saves significant debugging time.
Before opening the designer, map out what this flow needs to do in plain language:
This is a standard approval workflow pattern, but expense submissions have specific nuances worth getting right: you need to surface the dollar amount prominently, include the business justification, and make it easy for the approver to respond without opening SharePoint.
Key insight
The Approvals connector's "Start and wait for an approval" action is synchronous from the flow's perspective — the flow literally pauses at that step until a response arrives. This means your flow run can sit open for hours or days. Power Automate handles this gracefully (flows are durable), but it does mean your run history will show flows in a "Running" state for the duration of the approval window.
Create a new Automated cloud flow. When asked to choose a trigger, search for SharePoint and select When an item is created. Configure it with:
This trigger fires every time a new list item is created, giving you access to all the column values immediately through dynamic content.
Add a new action and search for Office 365 Users, then select Get manager (V2). In the User (UPN) field, use the dynamic content SubmitterEmail from the trigger output. This action returns the manager's full profile, including their email address, display name, and job title.
After this action runs, you'll have access to outputs like:
outputs('Get_manager_(V2)')?['body/mail'] — the manager's email addressoutputs('Get_manager_(V2)')?['body/displayName'] — their display nameWarning
Don't use userPrincipalName for sending approval emails — it usually matches the email address, but in some organizations with federated identities, the UPN and mail address differ. Always use mail for the approval recipient.
It's worth storing the manager's email in a variable at this point. Add an Initialize variable action:
varManagerEmailmail output from the Get manager actionThis lets you reference the manager email in multiple places (the approval action and later in the SharePoint update) without chaining deeply nested expressions everywhere. For more on this pattern, see the guidance on working with conditions, loops, and variables.
Add a new action from the Approvals connector: Start and wait for an approval. This is the action that sends the actual request and pauses the flow.
Configure it as follows:
Approval type: Approve/Reject - First to respond
Title: Use an expression that builds a meaningful subject line:
Expense Approval Required: @{triggerOutputs()?['body/Title']} — $@{triggerOutputs()?['body/ExpenseAmount']}
This produces something like: Expense Approval Required: Q3 Client Dinner - Chicago — $342.50
Assigned To: Use your varManagerEmail variable
Details: This field is your opportunity to give the manager real context. Craft it carefully:
Expense Submission Details
--------------------------
Submitted by: @{triggerOutputs()?['body/SubmitterEmail']}
Expense Date: @{triggerOutputs()?['body/ExpenseDate']}
Category: @{triggerOutputs()?['body/ExpenseCategory/Value']}
Amount: $@{triggerOutputs()?['body/ExpenseAmount']}
Receipt Attached: @{triggerOutputs()?['body/ReceiptAttached']}
Business Justification:
@{triggerOutputs()?['body/BusinessJustification']}
Please review and approve or reject below. You may add comments explaining your decision.
Tip
Notice the ExpenseCategory/Value syntax. Choice columns in SharePoint return an object, not a plain string. If you try to reference ExpenseCategory directly without /Value, you'll get [object Object] in your approval email. This trips up almost everyone the first time.
Item link: You can optionally add a URL to the SharePoint list item so the approver can open it directly. Build this as:
@{triggerOutputs()?['body/{Link}']}
Or construct it manually using your site URL and the item ID:
https://yourcompany.sharepoint.com/sites/Finance/Lists/ExpenseSubmissions/DispForm.aspx?ID=@{triggerOutputs()?['body/ID']}
Requestor: Use the SubmitterEmail dynamic content so the approval system tracks who originally submitted.
Once configured, the manager receives an email from approvals@microsoft.com containing the details you've written, plus Approve and Reject buttons they can click directly from the email without signing into SharePoint or Power Automate. That frictionless experience is critical for adoption — if approving requires three logins, managers will just ignore the emails.
After the "Start and wait for an approval" action completes (meaning the manager responded), you have access to the approval outcome through dynamic content. The key output is Outcome, which will be either Approve or Reject.
Add a Condition action. Configure it as:
Outcome dynamic content from the approval actionis equal toApproveThis gives you a Yes branch (approved) and a No branch (rejected). You'll build the SharePoint update and email notification inside each branch.
Note
Some flows return Approved with a capital A and trailing 'd' — this varies by connector version. Check the actual run output from a test run before hardcoding the string comparison. You can also use contains(outputs('Start_and_wait_for_an_approval')?['body/outcome'], 'Approve') as your condition expression to match both Approve and Approved.
Inside both branches of your condition, you'll add a SharePoint: Update item action. This is where the approval result becomes a permanent record.
Add Update item with:
ID dynamic content from the trigger (the item that was created)varManagerEmailComments output from the approval action — this is what the manager typed when they respondedutcNow() as an expressionThe Comments field deserves attention. The approval action returns comments as an array (since multiple approvers could each leave comments in parallel flows). For a single-approver flow like this one, use this expression to extract the first comment:
@{outputs('Start_and_wait_for_an_approval')?['body/responses'][0]['comments']}
If the approver left no comment, this returns an empty string — that's fine. The SharePoint column will just be blank.
The structure is identical, with two differences:
This symmetry is intentional. Whether approved or rejected, you want the same metadata captured: who made the decision, when, and what they said.
For comprehensive patterns around keeping SharePoint list items updated through their lifecycle, the SharePoint list lifecycle management guide covers archival and business-rule-based updates worth reviewing once you outgrow this basic pattern.
Still inside each branch, add a Send an email (V2) action from the Office 365 Outlook connector.
To: SubmitterEmail from the trigger
Subject:
✅ Your Expense Has Been Approved — @{triggerOutputs()?['body/Title']}
Body:
<p>Hi @{triggerOutputs()?['body/SubmitterEmail']},</p>
<p>Your expense submission has been <strong>approved</strong> by @{outputs('Get_manager_(V2)')?['body/displayName']}.</p>
<p><strong>Expense:</strong> @{triggerOutputs()?['body/Title']}<br>
<strong>Amount:</strong> $@{triggerOutputs()?['body/ExpenseAmount']}<br>
<strong>Approved on:</strong> @{utcNow()}</p>
<p><strong>Manager comments:</strong><br>
@{outputs('Start_and_wait_for_an_approval')?['body/responses'][0]['comments']}</p>
<p>Finance has been notified and your reimbursement will be processed according to the standard schedule.</p>
To: SubmitterEmail from the trigger
Subject:
❌ Expense Submission Requires Revision — @{triggerOutputs()?['body/Title']}
Body:
<p>Hi @{triggerOutputs()?['body/SubmitterEmail']},</p>
<p>Your expense submission was <strong>not approved</strong> by @{outputs('Get_manager_(V2)')?['body/displayName']}.</p>
<p><strong>Expense:</strong> @{triggerOutputs()?['body/Title']}<br>
<strong>Amount:</strong> $@{triggerOutputs()?['body/ExpenseAmount']}</p>
<p><strong>Reason given:</strong><br>
@{outputs('Start_and_wait_for_an_approval')?['body/responses'][0]['comments']}</p>
<p>Please review the feedback above and resubmit with the requested changes, or contact your manager directly if you have questions.</p>
The HTML body formatting is worth the extra keystrokes. Plain text approval notifications get ignored. A clean, structured email with bolded key facts and a clear next step dramatically improves the submitter experience — and reduces the "did my expense get approved?" follow-up messages to Finance.
For deeper patterns on crafting effective notification emails, the guide on automating email notifications covers templating and formatting techniques in detail.
There's one step many people forget: Finance needs to know about approved expenses too. After the SharePoint update in the approved branch, add a second Send an email action addressed to your Finance team's shared mailbox or distribution list.
This email should be more concise and data-focused than the submitter notification:
To: finance-approvals@yourcompany.com
Subject:
[APPROVED] Expense for Processing — @{triggerOutputs()?['body/Title']} — $@{triggerOutputs()?['body/ExpenseAmount']}
Body:
Submitter: @{triggerOutputs()?['body/SubmitterEmail']}
Approver: @{outputs('Get_manager_(V2)')?['body/displayName']}
Category: @{triggerOutputs()?['body/ExpenseCategory/Value']}
Amount: $@{triggerOutputs()?['body/ExpenseAmount']}
Expense Date: @{triggerOutputs()?['body/ExpenseDate']}
Receipt on File: @{triggerOutputs()?['body/ReceiptAttached']}
SharePoint Record: https://yourcompany.sharepoint.com/sites/Finance/Lists/ExpenseSubmissions/DispForm.aspx?ID=@{triggerOutputs()?['body/ID']}
That SharePoint link at the bottom means Finance can click through to the full record, see the attached receipt, and confirm the data — all from one email.
Save your flow and do a real test, not just a dry run. Create a new list item in ExpenseSubmissions using an email address you can send approval requests to (temporarily use your own as the submitter email, and make sure your manager is set in Azure AD, or use a colleague who's volunteered to test).
What to verify at each stage:
Trigger fires: Check the flow run history within 60 seconds of creating the item. You should see the flow move to Running.
Manager resolution: Expand the "Get manager" action in the run. Confirm the returned email and display name match the expected manager.
Approval email received: The manager (or your test account) should receive an email from approvals@microsoft.com within a few minutes. Check that the dollar amount, category, and justification all appear correctly. Verify the Approve and Reject buttons are clickable.
Approve the request: Click Approve in the email (you can add an optional comment). Within seconds, the flow should resume.
SharePoint item updated: Refresh the list item. ApprovalStatus should show Approved, ApprovedDate should be populated, and ApprovalComments should show whatever you typed.
Submitter notification received: Check the inbox for the submitter email address.
Warning
If you're testing with your own email as both submitter and approver, the approval request will arrive in your inbox from yourself. This can be confusing to debug — the flow is working correctly, it just looks odd. Use a second test account as the submitter if possible.
If anything looks wrong, the Power Automate Run History and Flow Checker guide walks through the diagnostic tools in detail. The most common issue at this stage is an expression error in the approval details field — open the failed action in run history and read the error message carefully before changing anything.
Build the complete flow described in this lesson in your own environment. Then extend it with the following enhancements:
Exercise 1 — Add a threshold condition
Modify the flow so that expenses under $100 are auto-approved without manager involvement: the SharePoint item gets updated to Approved, the submitter gets notified, and Finance gets their email — but no approval request is sent. Add a Condition action after the trigger and before the "Get manager" step. If ExpenseAmount is less than 100, branch directly to the update/notify steps. Only expenses $100 or above route to the manager.
This kind of threshold-based routing is a natural evolution of single-approver flows — the dynamic multi-condition approval flow guide covers more sophisticated routing rules when you're ready.
Exercise 2 — Add a timeout escalation
The "Start and wait for an approval" action has a Wait for duration setting. Set it to 48 hours. Then, after the action, add a Condition that checks whether the outcome is empty (the approval timed out rather than receiving a response). If timed out, update the status to "Escalated" and send a reminder email to the manager with a sharper subject line.
Exercise 3 — Capture the form via Microsoft Forms
Instead of users creating SharePoint list items directly, build a Microsoft Forms survey that collects the same expense data. Create a second flow triggered by the Forms response that creates the SharePoint list item, then let your existing approval flow take over from there. This is a common real-world pattern — Forms gives you a cleaner submission UX than a SharePoint list new item form. The Microsoft Forms automation guide covers exactly this pattern.
You're referencing a Choice column without the /Value suffix. Every Choice column in SharePoint returns a JSON object. Use triggerOutputs()?['body/ExpenseCategory/Value'] instead of triggerOutputs()?['body/ExpenseCategory']. The same applies to Person columns (use /Email or /DisplayName).
This usually means the email address passed in the User (UPN) field doesn't exist in Azure AD, or the account hasn't had a manager assigned. Verify by opening the Office 365 People search and looking up the user. If the manager field is blank in AD, the action will fail. Add an error handler (a parallel branch set to run on failure) that updates the ApprovalStatus to "Error - No Manager Found" and notifies the submitter.
This happens if something is editing the list item at creation (a Power Apps form, another flow, a list validation rule). Consider switching the trigger to "When an item is created or modified" and adding a Condition at the top that checks ApprovalStatus equals Pending before proceeding — this prevents re-triggering on items that already have an outcome.
Check the approval action version. In older versions, comments live at body/responses/0/comments. In newer versions, the path may differ slightly. Always verify the actual response structure in your run history by expanding the approval action's raw output. Use first(outputs('Start_and_wait_for_an_approval')?['body/responses'])?['comments'] as a safer expression — first() handles the array gracefully.
Double-check that the Id field in the Update Item action is using the integer ID from the trigger, not the GUID or any other identifier. SharePoint's Update item action requires the numeric item ID. Also confirm that the account running the flow has Edit permissions on the list (not just Read).
Tip
If you can't figure out why an expression is producing unexpected output, add a Compose action immediately after the problem step, put the expression in the Compose inputs, and check the run history. You'll see exactly what the expression evaluates to without breaking the rest of the flow.
Check the manager's mailbox junk folder first — approval emails from approvals@microsoft.com occasionally get filtered. If that's not the issue, verify the email address in varManagerEmail by checking the run history for that Initialize variable step. Also confirm your tenant's Exchange policies don't block external senders (approval emails come from Microsoft's infrastructure, not your domain).
You've built a complete, production-ready expense approval workflow. Let's recap what it does: it triggers the moment a new expense is submitted to SharePoint, resolves the submitter's manager from Azure AD, sends a rich approval request the manager can action from their email, waits durably for the response, writes the outcome and comments back to the SharePoint record, and notifies both the submitter and Finance team. That entire paper trail — who submitted, who approved, when, and why — lives in your SharePoint list.
This pattern is genuinely reusable. The same architecture handles purchase request approvals, PTO requests, vendor onboarding forms, and any other process where someone submits something and a human must decide on it.
Where to go from here:
Multi-stage approvals: If your organization requires both a direct manager approval and a VP approval above a certain dollar threshold, the multi-stage approval chains guide covers sequential and parallel sign-off patterns built on top of this foundation.
Adaptive Card approvals in Teams: If your managers live in Teams more than email, implementing Adaptive Card approvals lets them approve directly from a Teams message with a richer interactive interface than email.
SLA enforcement and escalation: If you need approvals completed within a business-day window, time-sensitive approval escalations with Do Until loops gives you the timeout and escalation mechanics to enforce SLAs automatically.
Chaining connectors across M365: Once you're comfortable with this flow, the natural next step is enriching it — creating a Planner task for Finance when approved, posting a summary to a Teams channel, or adding a calendar event. Chaining Microsoft 365 connectors covers exactly that multi-connector choreography.
The expense approval flow you built today isn't just an automation exercise — it's the kind of concrete, working artifact you can show stakeholders to demonstrate that Power Automate is production-ready for real business processes.