Stop losing leave requests in email threads. This lesson walks you through building a complete Power Automate flow that captures requests in SharePoint, routes them to managers for approval, updates the record on every outcome, and automatically creates Outlook calendar events for approved leave — all without manual intervention.

Every HR team has a version of this problem. An employee fires off an email to their manager asking for two weeks off in August. The manager replies "approved" and promptly forgets. Nobody updates a shared calendar. Nobody logs it anywhere. When August arrives, the team is missing two people simultaneously because HR never knew the second person was also approved. The employee's absence isn't tracked, payroll has incomplete records, and the manager is fielding panicked questions on their first day back from their own vacation.
Leave management is exactly the kind of structured, repetitive, multi-step process that automation was designed for. Every request follows the same path: someone submits details, someone else approves or rejects, and if approved, the absence needs to land on calendars and be recorded. There's no ambiguity in the business logic — which makes it a perfect candidate for a Power Automate flow backed by SharePoint.
By the end of this lesson, you'll have built a complete, production-ready leave request automation that covers every step of that journey. We'll design the SharePoint list, build the trigger and approval logic, handle both approval and rejection branches, write calendar events programmatically, and notify all parties through email. This is a real workflow you could deploy to your organization tomorrow.
What you'll learn:
You should already be comfortable with the Power Automate designer, understand how triggers work, and have built at least one flow before. If you need a foundation, Power Automate Triggers: When to Start a Flow covers the mechanics of trigger types thoroughly. You'll also want a working knowledge of conditions and branching — Working with Conditions, Loops, and Variables in Power Automate is excellent background reading.
On the infrastructure side, you need:
Before writing a single action in Power Automate, the data structure matters enormously. Your SharePoint list is doing double duty here: it's the submission mechanism and the record of truth. Design it thoughtfully and the flow practically writes itself. Design it poorly and you'll spend hours working around data type mismatches and missing fields.
Create a new list on your SharePoint site. Call it Leave Requests. Here are the columns you need and — crucially — why each one exists:
Title (Single line of text — built-in, rename it to "Employee Name") This is the requestor's display name. You'll use it in email subjects and approval notifications.
EmployeeEmail (Single line of text) Don't rely solely on the "Created By" field for the email address. Managers sometimes submit requests on behalf of employees, so capturing the email explicitly keeps the notification logic clean.
LeaveType (Choice) Options: Annual Leave, Sick Leave, Parental Leave, Unpaid Leave, Other. This lets you branch later if certain leave types need different approval chains (HR approval for parental leave, for instance).
StartDate (Date and Time — Date Only) The first day of leave. Set "Date Only" in column settings to avoid timezone headaches in calendar events.
EndDate (Date and Time — Date Only) The last day of leave.
NumberOfDays (Number) Yes, this is technically derivable from StartDate and EndDate, but calculating working days in SharePoint formulas is messy and Power Automate will handle it better. Ask the employee to enter it explicitly. You can always validate it in the flow.
Reason (Multiple lines of text) Optional but important. Approvers need context. A one-line reason box significantly reduces back-and-forth emails.
Status (Choice) Options: Pending, Approved, Rejected. Default to Pending. Your flow will update this column based on the approval outcome — it becomes your audit trail.
ManagerEmail (Single line of text) The approver's email. Some organizations have this in a lookup against an employee directory; for this lesson, we'll have employees enter it directly. If your organization has a Dataverse employee table or an Azure AD attribute for manager, you can look it up programmatically — see Integrating Power Automate with Microsoft Dataverse for that pattern.
ApprovalComments (Multiple lines of text) Left blank on submission. Your flow will populate this with the approver's response comments when they act on the request.
Once your columns are created, test the list by submitting one dummy entry. Confirm all columns appear correctly and the default Status value is "Pending." You're ready to build the flow.
Tip
Give your SharePoint list a sensible internal name and avoid spaces — SharePoint replaces spaces with %20 in the internal column name, which can make expressions in Power Automate harder to read. Name your columns with no spaces (e.g., "ManagerEmail" not "Manager Email") to save yourself debugging time later.
Navigate to Power Automate and create a new Automated cloud flow. When prompted for a trigger, search for "SharePoint" and select "When an item is created." This trigger fires every time a new row is added to your list — which happens the moment an employee submits their leave request.
Configure the trigger:
Name your flow something descriptive like Leave Requests - Submission, Approval, Calendar. Naming matters more than it seems when you have ten flows in production — Power Automate Best Practices: Master Flow Naming, Testing, and Monitoring has an excellent naming convention framework worth adopting from the start.
You'll reference the date range repeatedly throughout this flow — in approval notifications, email bodies, and calendar event descriptions. Rather than constructing it from scratch each time, create it once with a Compose action right after the trigger.
Add a Compose action and name it Compose - Date Range Label. In the Inputs field, enter:
formatDateTime(triggerOutputs()?['body/StartDate'], 'dddd, MMMM d yyyy')
concat(' to ', formatDateTime(triggerOutputs()?['body/EndDate'], 'dddd, MMMM d yyyy'))
Actually, in the Power Automate expression box, you'd combine these as:
concat(
formatDateTime(triggerOutputs()?['body/StartDate'], 'dddd, MMMM d yyyy'),
' to ',
formatDateTime(triggerOutputs()?['body/EndDate'], 'dddd, MMMM d yyyy')
)
This produces something like Monday, August 4 2025 to Friday, August 15 2025 — a human-readable string you'll drop into notification messages throughout the flow. For a deeper dive on expression syntax like formatDateTime and concat, Mastering Dynamic Expressions and the Power Automate Formula Language is essential reading.
Add a new action and search for "Approvals". Select "Start and wait for an approval." This is the synchronous variant — the flow pauses at this step until the approver responds. For leave requests, this is exactly what you want: nothing downstream should happen until a decision is made.
Note
Power Automate also has "Start an approval" (fire and forget) and "Wait for an approval" as separate actions. The combined "Start and wait" action is cleaner for linear workflows like this one. If you need to do other work while waiting for approval (like sending a notification to HR), use the split pattern — but for this flow, the simple synchronous approach is best.
Configure the approval action:
Approval type: Approve/Reject - First to respond
Title:
Leave Request: [Employee Name] - [Leave Type] ([Number of Days] days)
Use the dynamic content picker to insert the actual field values:
concat('Leave Request: ', triggerOutputs()?['body/Title'], ' - ', triggerOutputs()?['body/LeaveType'], ' (', string(triggerOutputs()?['body/NumberOfDays']), ' days)')
Assigned to: Insert the ManagerEmail field from the trigger dynamic content
Details:
concat(
'Employee: ', triggerOutputs()?['body/Title'], '\n',
'Email: ', triggerOutputs()?['body/EmployeeEmail'], '\n',
'Leave Type: ', triggerOutputs()?['body/LeaveType'], '\n',
'Dates: ', outputs('Compose_-_Date_Range_Label'), '\n',
'Number of Days: ', string(triggerOutputs()?['body/NumberOfDays']), '\n',
'Reason: ', triggerOutputs()?['body/Reason']
)
Item link: Set this to the URL of the specific SharePoint list item. Use:
concat(triggerOutputs()?['body/{Link}'])
Or construct it manually:
concat('https://yourorg.sharepoint.com/sites/YourSite/Lists/LeaveRequests/DispForm.aspx?ID=', string(triggerOutputs()?['body/ID']))
This gives the approver a direct link to the original request — they can review it in SharePoint without hunting through the list.
The approver will receive an email and an Adaptive Card in the Approvals hub (and in Teams if they have the Teams app). They can approve or reject directly from that notification with an optional comment. See Building Approval Workflows with Power Automate for a thorough explanation of how the approval action behaves from the approver's perspective.
After the approval action completes, you have a decision point. Add a Condition action. You're going to check whether the approval was approved or rejected.
In the condition, use the dynamic content from the approval action:
outputs('Start_and_wait_for_an_approval')?['body/outcome']ApproveWarning
The outcome value is case-sensitive and must be Approve (not Approved, not approve). Power Automate's Approvals connector returns exactly the string Approve for a positive decision. Getting this wrong means your condition never matches and all requests fall into the "No" branch.
You now have two branches: Yes (Approved) and No (Rejected). Let's build each one.
The first thing to do after approval is write the outcome back to SharePoint. Add a "Update item" action from the SharePoint connector.
ID field from the trigger dynamic contentSet the field values:
outputs('Start_and_wait_for_an_approval')?['body/responses'][0]?['comments']The responses[0] expression pulls the first (and in this case only) approver's comment. This gets written back to the SharePoint list as a permanent record.
Tip
Always update the SharePoint record as the first step in each branch, before sending notifications. If a downstream action fails (like the calendar event creation), you still have an accurate record in SharePoint. The SharePoint item becomes your source of truth for auditing — don't let transient failures leave it in an inconsistent state.
Add the "Create an event (V4)" action from the Office 365 Outlook connector. This is where the real value of automation becomes obvious — the calendar event appears without anyone needing to remember to create it.
Configure it:
Calendar Id: Calendar (this creates the event on the flow owner's calendar — more on this in a moment)
Subject:
concat('[LEAVE] ', triggerOutputs()?['body/Title'], ' - ', triggerOutputs()?['body/LeaveType'])
Start time:
triggerOutputs()?['body/StartDate']
End time:
addDays(triggerOutputs()?['body/EndDate'], 1)
Note the addDays(..., 1) on the end date. Outlook treats all-day event end dates as exclusive — the event ends at the start of that day rather than the end of it. Adding one day makes the event span correctly through the last day of leave.
Is All Day Event: Yes
Body:
concat(
'<b>Employee:</b> ', triggerOutputs()?['body/Title'], '<br>',
'<b>Leave Type:</b> ', triggerOutputs()?['body/LeaveType'], '<br>',
'<b>Duration:</b> ', string(triggerOutputs()?['body/NumberOfDays']), ' days<br>',
'<b>Reason:</b> ', triggerOutputs()?['body/Reason'], '<br>',
'<b>Approved by:</b> ', outputs('Start_and_wait_for_an_approval')?['body/responses'][0]?['responder/displayName']
)
Attendees: Add the EmployeeEmail field here. This sends a calendar invitation to the employee, which they can accept to block the time on their own calendar.
For a comprehensive guide to the calendar connector's options and quirks, Automating Outlook Calendar Events and Meeting Scheduling with Power Automate covers the full action configuration in detail.
Note
The calendar event will be created on the calendar of the account whose connection is used in the flow. For a shared team or HR calendar, you'd configure a service account connection, or use a Microsoft 365 Group calendar by setting the Calendar Id to the group's calendar ID. For a personal manager calendar, the manager would need their own flow trigger — which is a more complex pattern covered in multi-stage approval designs.
Add a "Send an email (V2)" action from the Office 365 Outlook connector.
To: triggerOutputs()?['body/EmployeeEmail']
Subject:
concat('Your Leave Request Has Been Approved: ', outputs('Compose_-_Date_Range_Label'))
Body:
<p>Hi @{triggerOutputs()?['body/Title']},</p>
<p>Great news! Your leave request has been approved.</p>
<table>
<tr><td><b>Leave Type:</b></td><td>@{triggerOutputs()?['body/LeaveType']}</td></tr>
<tr><td><b>Dates:</b></td><td>@{outputs('Compose_-_Date_Range_Label')}</td></tr>
<tr><td><b>Duration:</b></td><td>@{triggerOutputs()?['body/NumberOfDays']} days</td></tr>
</table>
<p><b>Manager's Comments:</b><br>
@{outputs('Start_and_wait_for_an_approval')?['body/responses'][0]?['comments']}</p>
<p>A calendar invitation has been sent to your email. Please accept it to block the time.</p>
<p>HR Team</p>
For more email formatting patterns and best practices, Automate Email Notifications with Power Automate covers HTML body construction and dynamic content injection thoroughly.
The rejection branch is simpler — no calendar event, but the record still needs updating and the employee needs a clear, professional notification.
Add another "Update item" action:
outputs('Start_and_wait_for_an_approval')?['body/responses'][0]?['comments']To: triggerOutputs()?['body/EmployeeEmail']
Subject:
concat('Update on Your Leave Request: ', outputs('Compose_-_Date_Range_Label'))
Body:
<p>Hi @{triggerOutputs()?['body/Title']},</p>
<p>Thank you for submitting your leave request. Unfortunately, your request for the following period was not approved at this time:</p>
<table>
<tr><td><b>Leave Type:</b></td><td>@{triggerOutputs()?['body/LeaveType']}</td></tr>
<tr><td><b>Dates:</b></td><td>@{outputs('Compose_-_Date_Range_Label')}</td></tr>
<tr><td><b>Duration:</b></td><td>@{triggerOutputs()?['body/NumberOfDays']} days</td></tr>
</table>
<p><b>Manager's Comments:</b><br>
@{outputs('Start_and_wait_for_an_approval')?['body/responses'][0]?['comments']}</p>
<p>If you have questions about this decision, please speak with your manager directly.</p>
<p>HR Team</p>
Key insight
Always include the manager's comments in rejection emails, even if that field might be empty. If the manager approved or rejected without leaving a comment, the expression will return an empty string — which is fine and renders cleanly. Never leave an employee with a rejection and no explanation of how to get more information.
One overlooked piece: after the manager acts on the approval, they should receive a confirmation that their decision was recorded and the employee was notified. This prevents the manager from wondering if their approval "went through."
Add this after the condition block (outside both branches, so it runs regardless of the outcome). Add a "Send an email (V2)" action:
To: triggerOutputs()?['body/ManagerEmail']
Subject:
concat('Confirmation: Leave Request Decision Recorded for ', triggerOutputs()?['body/Title'])
Body:
<p>Hi,</p>
<p>This confirms that your decision on the following leave request has been recorded:</p>
<table>
<tr><td><b>Employee:</b></td><td>@{triggerOutputs()?['body/Title']}</td></tr>
<tr><td><b>Dates:</b></td><td>@{outputs('Compose_-_Date_Range_Label')}</td></tr>
<tr><td><b>Decision:</b></td><td>@{outputs('Start_and_wait_for_an_approval')?['body/outcome']}</td></tr>
</table>
<p>The employee has been notified of your decision.</p>
This small addition builds trust in the system. Managers who feel confident their decisions are recorded become advocates for the automation rather than resistors.
Real employees will test your assumptions. Here are the edge cases worth handling:
Before sending the approval request, you might want to check whether the employee already has an approved leave request overlapping those dates. Add a "Get items" action from SharePoint before the approval step:
EmployeeEmail eq 'EMAIL_VALUE' and Status eq 'Approved' and StartDate le 'ENDDATE_VALUE' and EndDate ge 'STARTDATE_VALUE'
In Power Automate, you'd use OData filter syntax with dynamic content:
EmployeeEmail eq '@{triggerOutputs()?['body/EmployeeEmail']}' and Status eq 'Approved' and StartDate le '@{triggerOutputs()?['body/EndDate']}' and EndDate ge '@{triggerOutputs()?['body/StartDate']}'
Then add a condition: if the returned list has more than zero items (length(body('Get_items')?['value']) is greater than 0), you can either alert the manager in the approval details or block the request outright. For this flow, flagging it in the approval details is the safer choice — let the manager decide with full information.
Check whether StartDate is in the past. Add a Compose to evaluate:
less(triggerOutputs()?['body/StartDate'], utcNow())
If this returns true, you might want to email the employee to resubmit with correct dates and end the flow early using the "Terminate" action with status "Cancelled."
If the ManagerEmail field contains a typo, the approval action will fail because it can't find a valid recipient. Add error handling around the approval action using the "Configure run after" settings — specifically, handle the case where the approval action fails by sending an email to a generic HR mailbox. Master Error Handling and Retry Patterns in Power Automate for Bulletproof Flows covers the "run after" configuration pattern in depth.
Here's the full flow structure laid out linearly so you can verify your build matches the intended design:
TRIGGER: When an item is created (SharePoint - Leave Requests)
│
├── Compose: Date Range Label
│
├── Approvals: Start and wait for an approval
│ └── [Flow pauses here until manager responds]
│
├── Condition: outcome eq 'Approve'
│ │
│ ├── YES (Approved Branch):
│ │ ├── SharePoint: Update item (Status = Approved, write comments)
│ │ ├── Outlook: Create an event (all-day event for leave period)
│ │ └── Outlook: Send email to employee (approved notification)
│ │
│ └── NO (Rejected Branch):
│ ├── SharePoint: Update item (Status = Rejected, write comments)
│ └── Outlook: Send email to employee (rejection notification)
│
└── Outlook: Send email to manager (confirmation of decision recorded)
Build this flow in your own environment using the following scenario:
Scenario: Priya Sharma is submitting a request for Annual Leave from Monday, September 8, 2025 to Friday, September 12, 2025 (5 days). Her manager is David Chen.
Create the Leave Requests SharePoint list with all columns described above.
Build the complete flow following the structure in this lesson.
Submit a test list item as Priya's request. Use your own email as both the EmployeeEmail and ManagerEmail so you can test the full loop.
When you receive the approval email, approve it with a comment like "Enjoy your break, approved as requested."
Verify that:
Submit a second test item and reject it. Verify:
Extension challenge: Add a condition before the approval action that checks if NumberOfDays is greater than 10. If it is, change the Assigned To field in the approval to route to a second approver (e.g., an HR manager email). This introduces a basic routing rule. For more complex multi-level routing patterns, see Designing Multi-Stage Approval Chains in Power Automate.
Mistake: The flow triggers but the approval never arrives
Check the Assigned To field in the approval action. If you're using a dynamic field for the manager's email and it's empty or malformed (e.g., contains extra spaces), the approval action silently fails or creates a request nobody can see. Test by hardcoding your own email in Assigned To first, then switch to dynamic content once the flow works.
Mistake: Calendar event ends a day early
You forgot the addDays(EndDate, 1) on the end time. Outlook's all-day event model uses exclusive end dates. If an employee's last day is Friday the 12th, the End time must be set to Saturday the 13th for the event to display through Friday.
Mistake: The condition never matches "Approve"
Double-check your condition value. It must be Approve exactly — capital A, no "d" at the end. A common mistake is typing Approved because that's what the UI shows you in run history. The internal string the connector returns is Approve.
Mistake: The flow runs twice for the same request
Check whether you have both "When an item is created" AND "When an item is created or modified" triggers pointing at the same list. When your flow updates the Status field (in the Update item action), it can re-trigger a "created or modified" flow. Use "When an item is created" (not modified) to avoid this loop. If you need a modified trigger elsewhere, add a condition at the start to check that Status equals "Pending" before proceeding.
Mistake: The SharePoint item ID isn't available in expressions
The item ID from the trigger is exposed as triggerOutputs()?['body/ID'] (uppercase ID). Some developers reference Id (mixed case) and get null. When constructing the SharePoint item URL manually, make sure you're using the correct case.
Debugging tool: When a flow run fails, click the failed run in the flow's run history. Each action shows its inputs and outputs expanded — you can see exactly what value was passed to the condition, what the approval outcome string was, and where the expression broke. Using Power Automate Run History and Flow Checker to Debug and Fix Failing Flows walks through this debugging workflow step by step.
Warning
Don't test this flow by repeatedly creating and deleting SharePoint items. Each creation fires a new flow run, and approvals that are still pending from deleted items create orphaned approvals in the system. Use the flow's built-in test feature (the "Test" button in the designer) which lets you use a previous run's trigger data without creating new list items.
Once the base flow is working, several natural extensions become obvious:
Automatic manager lookup from Azure AD Instead of asking employees to type their manager's email, use the Office 365 Users connector to look up the employee's manager automatically. The "Get manager (V2)" action takes a user's UPN (their email address) and returns their manager's profile, including email. This eliminates the most common data quality problem in the current design.
Teams notification to the manager Alongside the approval email, send an Adaptive Card to the manager in Teams using the Teams connector. This dramatically improves response rates since most managers live in Teams. See Using Power Automate with Microsoft Teams: Automate Notifications, Approvals, and Channel Messages for the Teams connector patterns.
Approval reminder escalation If the manager doesn't respond within 48 hours, automatically send a reminder. This requires a parallel branch with a delay and a timeout check — Automating Recurring Approval Reminders in Power Automate covers this exact escalation pattern.
Multi-level approval for long leave If the leave request exceeds 15 days, route it through a two-stage approval: immediate manager first, then HR director. This builds directly on the extension challenge in the hands-on exercise. The full pattern is documented in Building a Dynamic Multi-Condition Approval Flow in Power Automate.
HR shared calendar Instead of (or in addition to) the manager's calendar, create the event on a shared HR calendar. Use a service account connection for the Outlook Calendar action and target the shared mailbox or Microsoft 365 group calendar. This gives HR a consolidated view of all approved leave across the organization.
You've built something genuinely useful here. The flow you've constructed handles the entire lifecycle of a leave request: ingestion through SharePoint, routing to the right approver, branching on the decision, calendar event creation, employee notification, and manager confirmation — all wired together without a single manual step after the initial submission.
More importantly, you've done this in a way that creates an audit trail. Every request has a SharePoint record with its final status and the approver's comments. That's something most email-based approval systems never achieve.
The natural next building blocks from here:
The leave request flow is a genuinely excellent learning vehicle because it touches so many real automation patterns in a single, coherent scenario. What you've built here transfers directly to expense approvals, purchase order workflows, IT access requests, and any other structured decision process your organization runs on email and hope.