The suggested answer has the right shape, but it skips the two things that actually make this flow fail in practice:
Apply to each concurrency and
time-zone handling. Here's a concrete build that fills those gaps.
The two gotchas to get right first
Concurrency must be 1. The whole approach relies on a running "previous end" variable updated meeting-by-meeting. Apply to each runs in parallel by default, which scrambles that. In the Apply to each → Settings, turn on Concurrency Control and set degree to 1 (sequential). Without this, your gaps will be wrong on any day with more than one meeting.
Work in UTC, think in Eastern. The Outlook connector wants UTC for the calendar-view window, but your business rule is 8:30–17:30 Eastern. Build the window like this:
windowStartUtc = convertToUtc(
concat(formatDateTime(convertFromUtc(utcNow(),'Eastern Standard Time'),'yyyy-MM-dd'),'T08:30:00'),
'Eastern Standard Time')
windowEndUtc = convertToUtc(
concat(formatDateTime(convertFromUtc(utcNow(),'Eastern Standard Time'),'yyyy-MM-dd'),'T17:30:00'),
'Eastern Standard Time')
Eastern Standard Time (the Windows zone id) auto-handles EST/EDT, so you don't hardcode the offset.
The build
- Recurrence trigger — set it to run on weekdays only (the recurrence action lets you pick Mon–Fri), so you don't need separate weekday-filtering logic.
- Initialize variables: windowStartUtc, windowEndUtc (as above), and varPrevEnd (String) initialized to windowStartUtc.
- Get calendar view of events — use this, not Get events, because it expands recurring series into individual instances inside your window. Start Time = windowStartUtc, End Time = windowEndUtc.
- Filter array to drop all-day events: keep items where isAllDay is equal to false. Calendar view normally returns events ordered by start time; if you want to be safe, this is also where an Office Script sort would slot in, but in practice the view comes back start-ordered.
- Apply to each over the filtered events, concurrency = 1:
Condition — gap exists?
ticks(items('Apply_to_each')?['start']?['dateTime']) > ticks(variables('varPrevEnd'))
- If yes → Create event (V4): Start = varPrevEnd, End = current meeting's start/dateTime, Show as = Tentative, Subject = Tentative Hold - Auto Created. Set time zone on the action to UTC since those values are UTC.
- Then set varPrevEnd to the later of the two ends (handles overlapping meetings):
if(greater(ticks(items('Apply_to_each')?['end']?['dateTime']), ticks(variables('varPrevEnd'))),
items('Apply_to_each')?['end']?['dateTime'],
variables('varPrevEnd'))
Referencing the variable inside its own Set is fine — it reads then writes.
6. After the loop — trailing gap to end of day:
- Condition: ticks(variables('windowEndUtc')) > ticks(variables('varPrevEnd'))
- If yes → Create event from varPrevEnd to windowEndUtc, Tentative.
Avoiding duplicate holds on re-runs
Cleanest pattern: at the top of the flow (right after step 3's window is known), run a second Get calendar view of events → Filter where subject equals Tentative Hold - Auto Created → Apply to each → Delete event. Then build fresh. That makes the flow idempotent — every run wipes its own prior holds and recomputes, so calendar changes are always reflected and you never stack duplicates.
(If you don't delete first, the previously-created holds will be returned by the main calendar view as existing events, which actually suppresses duplicates naturally — but it also means a deleted meeting won't free up its slot, so the delete-and-rebuild approach is safer.)
One thing worth deciding before you build: do you want one hold spanning each entire gap, or back-to-back 30-minute holds within each gap? The logic above creates one block per gap; if you want fixed slots, you'd add an inner loop that steps through the gap in 30-minute increments.