
You've built a flow that extracts sales data from Dynamics 365, enriches it with margin calculations, and drops a formatted report into a SharePoint library. It works perfectly when you test it manually. Then you schedule it, go home, and discover the next morning that it ran at 2 AM instead of 6 AM, the timestamp in every row is off by five hours, and the "business hours only" condition you added let a weekend run slip through. Welcome to the time-based flow problem — and it trips up even experienced Power Automate developers.
Time management in Power Automate is one of those topics that looks simple on the surface ("just add a Recurrence trigger") but hides enough nuance to break production workflows in subtle, hard-to-diagnose ways. UTC vs. local time, DST transitions, interval drift, and business-hours logic that almost-but-not-quite works are all common culprits. This lesson takes all of that apart systematically so you can build scheduled flows that behave exactly as intended, every time, across any time zone.
By the end of this lesson you'll be able to configure Recurrence and Sliding Window triggers with precision, handle time zones without guessing, write bulletproof business-hours conditions using Power Automate expressions, and build a real-world daily report dispatcher that only fires on business days within business hours.
What you'll learn:
convertTimeZone, dayOfWeek, and related expressions to enforce business-hours logicYou should already be comfortable with:
utcNow() and formatDateTime()If you haven't worked with expressions at all yet, spend 20 minutes with the "Expression Basics" lesson first — this lesson moves quickly through expression syntax and focuses on applying it.
Before touching a single trigger, you need to internalize one rule: Power Automate stores and processes all times in UTC. Internally, everything — trigger times, timestamps from connectors, the output of utcNow() — is UTC. The platform doesn't know where you are sitting, and it doesn't care. When your flow runs at what you intended to be 8:00 AM Eastern, the engine sees 13:00:00Z (or 12:00:00Z during daylight saving time, which we'll address).
This is the right design. UTC is unambiguous, stable, and globally consistent. The problems arise when developers treat Power Automate as if it knows their local time — configuring a Recurrence start time without thinking about UTC offset, or comparing utcNow() directly against a "9 AM" string and wondering why it fires at odd hours.
The mental model you want is: configure and store in UTC, convert to local only at the display or decision layer. When you need to check whether a flow is running inside business hours for your team in Chicago, you convert UTC to Central Time right before that check. You don't try to make UTC equal to Central Time everywhere else.
The Recurrence trigger is the workhorse for scheduled flows. You'll find it under "Schedule" when creating a new cloud flow, or you can search for "Recurrence" in the trigger picker. Let's walk through every field.
Interval is a number; Frequency is the unit. Together they define the beat. "Every 15 minutes" = Interval: 15, Frequency: Minute. "Every 2 weeks" = Interval: 2, Frequency: Week.
The supported frequency values are Second, Minute, Hour, Day, Week, and Month. There are some important behavioral differences depending on which frequency you choose:
The Start Time field sets two things simultaneously: the first execution time, and the alignment anchor for all future executions. If you set a daily flow to start at 2024-01-15T08:00:00Z and today is January 20th, the flow starts today — but it still anchors its schedule to 8:00 AM UTC.
The critical mistake is entering a start time in your local time without converting it to UTC. If you're in Eastern Standard Time (UTC-5) and you want the flow to start at 8:00 AM EST, you need to enter 2024-01-15T13:00:00Z in the Start Time field. Every scheduling platform that uses UTC does this, and Power Automate is no exception.
The format Power Automate expects is ISO 8601: YYYY-MM-DDTHH:mm:ssZ. The Z suffix means UTC. You can also supply an offset like 2024-01-15T08:00:00-05:00 for Eastern Standard Time, and Power Automate will convert it correctly — but the UTC form is cleaner and unambiguous.
Warning: If you leave Start Time blank, Power Automate picks a start time based on when you saved the flow. This is fine for testing but creates unpredictable anchor times in production. Always set an explicit Start Time for any flow that needs to run at a specific clock time.
When Frequency is set to Day, you can specify At These Hours (a comma-separated list of hours in 24-hour UTC format) and At These Minutes (0–59). This is how you get precise sub-daily scheduling without building a more complex trigger.
For example: a flow that should run at 9:00 AM and 2:00 PM Central Standard Time (UTC-6) every day would use:
15, 200Tip: "At These Hours" and "At These Minutes" work as a Cartesian product. If you specify Hours:
9, 14and Minutes:0, 30, the flow runs at 9:00, 9:30, 14:00, and 14:30 UTC. This is powerful but easy to over-schedule accidentally — double-check your combinations.
When Frequency is Week, the On These Days selector appears. This is the right way to schedule weekday-only runs. Select Monday through Friday, set your UTC hour, and you have a flow that legitimately won't fire on weekends. This is meaningfully different from scheduling every day and then checking the day inside the flow body — we'll compare these approaches directly later.
The Sliding Window trigger looks almost identical to Recurrence at first glance, but it solves a different problem: guaranteed execution with backfill for missed runs.
With a standard Recurrence trigger, if your flow is turned off (or paused, or the service has an outage) for six hours, you simply miss those runs. When the flow comes back, it continues from the next scheduled time. That's fine for reporting flows but catastrophic for an integration flow that processes transaction batches — you could miss six hours of transactions.
The Sliding Window trigger keeps track of every interval it was supposed to fire. When the flow resumes, it fires once for each missed window, working forward sequentially until it's caught up. Each run receives the triggerOutputs() window start and end times so your logic can process exactly the right data range.
triggerOutputs()?['windowStartTime'] // e.g., 2024-03-15T08:00:00.0000000Z
triggerOutputs()?['windowEndTime'] // e.g., 2024-03-15T09:00:00.0000000Z
Use these in your data queries instead of utcNow() to query records that belong to that specific window — not "the last hour" from the current moment.
| Scenario | Use |
|---|---|
| Daily report email | Recurrence |
| Hourly data sync where gaps cause data loss | Sliding Window |
| Weekly stakeholder digest | Recurrence |
| ETL pipeline processing hourly transaction batches | Sliding Window |
| Polling an API that doesn't support webhooks | Recurrence (usually) |
| Financial reconciliation — every run must account for its period | Sliding Window |
Warning: Sliding Window backfill can generate a large burst of concurrent runs. If your flow calls downstream APIs with rate limits, add a Delay action or configure concurrency settings (under the trigger's Settings) to prevent hammering those APIs during catch-up.
Now we get into the expressions layer, which is where real-world scheduling logic actually lives. Even if your trigger fires at exactly the right UTC time, you'll often need to reason about local time inside the flow — for display strings, conditional logic, or timestamps on output records.
This is your primary tool. The signature is:
convertTimeZone(timestamp, sourceTimeZone, destinationTimeZone, format)
A concrete example: converting utcNow() to Eastern Time:
convertTimeZone(utcNow(), 'UTC', 'Eastern Standard Time', 'yyyy-MM-dd HH:mm:ss')
The time zone strings use Windows Time Zone IDs — not IANA/Olson IDs like America/New_York. This catches people who come from Python, Linux, or JavaScript backgrounds. Common ones you'll use:
| Location | Windows Time Zone ID |
|---|---|
| Eastern US | Eastern Standard Time |
| Central US | Central Standard Time |
| Mountain US | Mountain Standard Time |
| Pacific US | Pacific Standard Time |
| UTC | UTC |
| London | GMT Standard Time |
| Central Europe | Central European Standard Time |
| India | India Standard Time |
| Tokyo | Tokyo Standard Time |
Important: Despite their names,
Eastern Standard Timeand similar IDs automatically handle Daylight Saving Time. When DST is active,convertTimeZone(..., 'UTC', 'Eastern Standard Time')will correctly return EDT (UTC-4) without you doing anything special. The "Standard" in the name is a Windows historical artifact, not a promise that DST is ignored.
You'll frequently need to extract pieces of the converted local time for conditional checks. Combine convertTimeZone with formatDateTime:
// Get the current hour in Central Time as an integer
int(formatDateTime(convertTimeZone(utcNow(), 'UTC', 'Central Standard Time'), 'H'))
The 'H' format specifier gives you a 24-hour hour with no leading zero — so 9 AM = 9, 2 PM = 14. Use 'HH' for zero-padded, 'h' for 12-hour without padding.
// Get the day of week as an integer (0 = Sunday, 6 = Saturday)
dayOfWeek(convertTimeZone(utcNow(), 'UTC', 'Central Standard Time'))
dayOfWeek() returns 0 for Sunday through 6 for Saturday. To check for a weekday, you want the result to be between 1 and 5 inclusive.
// Get today's date string in local time
formatDateTime(convertTimeZone(utcNow(), 'UTC', 'Eastern Standard Time'), 'yyyy-MM-dd')
This is especially useful when you need to include "today's date" in a report filename or subject line and you want it to reflect the user's date, not UTC date. At 11 PM Eastern on December 14th, UTC is already December 15th — without this conversion, your "December 14th report" would be labeled December 15th.
Let's build the actual decision logic for a flow that should only execute meaningful work during business hours — say, Monday through Friday, 8:00 AM to 6:00 PM Eastern Time. This kind of check is needed when you have a trigger that fires continuously (like a SharePoint item created trigger) but only want to act during business hours, or when you have a recurrence that you want to gracefully skip over holidays.
Inside a Condition action, we need to check two things: is today a weekday, and is the current hour within the business window?
First, get the local hour and day of week:
// Store these as variables at the top of the flow for reuse
// Variable: LocalHour (Integer)
int(formatDateTime(convertTimeZone(utcNow(), 'UTC', 'Eastern Standard Time'), 'H'))
// Variable: LocalDayOfWeek (Integer)
dayOfWeek(convertTimeZone(utcNow(), 'UTC', 'Eastern Standard Time'))
Then build a condition that checks both:
// Is Weekday: LocalDayOfWeek >= 1 AND LocalDayOfWeek <= 5
// Is Business Hours: LocalHour >= 8 AND LocalHour < 18
In the Power Automate condition editor, you'd chain these with "And" connectors. But for complex multi-condition logic, using a single expression in a Condition with and() is cleaner and more readable:
and(
greaterOrEquals(variables('LocalDayOfWeek'), 1),
lessOrEquals(variables('LocalDayOfWeek'), 5),
greaterOrEquals(variables('LocalHour'), 8),
less(variables('LocalHour'), 18)
)
Place this in a Condition action set to check if the expression evaluates to true. Everything in the "Yes" branch executes; "No" terminates without doing anything, and the flow run is marked as succeeded (not failed — an important distinction for run history cleanliness).
Tip: Use
less(variables('LocalHour'), 18)rather thanlessOrEquals(variables('LocalHour'), 17)because the hour variable is an integer representing the start of that hour. Hour 17 covers 5:00 PM to 5:59 PM. If you want to stop at 6:00 PM exactly, the last valid hour is 17, soless(hour, 18)andlessOrEquals(hour, 17)are equivalent — but thelessversion is more explicit about your intent.
Instead of an empty "No" branch in your condition, consider adding a Terminate action set to "Succeeded" with a status message like "Flow skipped — outside business hours." This makes run history self-documenting and saves you from guessing why a run appeared to do nothing.
Status: Succeeded
Code: SKIPPED
Message: Run at @{utcNow()} is outside business hours (Eastern Time). No action taken.
Weekday and hours checks get you 90% of the way there. Holidays require a data source. The cleanest approach in Power Automate is to maintain a SharePoint list called "BusinessHolidays" with a single Date column. At the start of your flow, query it:
Date eq '@{formatDateTime(convertTimeZone(utcNow(), 'UTC', 'Eastern Standard Time'), 'yyyy-MM-dd')}'length(body('Get_items')?['value'])equals(variables('HolidayCount'), 0) — only proceed if zero holidays found for todayThis keeps holiday management in SharePoint where non-developers can maintain it, and your flow stays clean.
A subtle issue with Recurrence triggers: they can drift. If a flow run takes longer than the interval (say your 5-minute interval flow runs for 7 minutes), Power Automate doesn't stack runs — it waits for the current run to finish and then fires again. Over days or weeks, this can cause the schedule to drift meaningfully.
For most reporting use cases, drift of a few minutes is irrelevant. But if you're doing something time-sensitive (like cutting off data at exactly 9 AM for a reconciliation), you should not rely on utcNow() inside the flow to determine the period boundary. Instead, use the trigger timestamp:
triggerOutputs()?['headers']?['x-ms-workflow-run-id'] // run ID
trigger()?['startTime'] // when the trigger actually fired
Or more precisely, reference the trigger's scheduled time. For a Recurrence trigger:
trigger()?['scheduledTime']
This gives you the time the trigger was scheduled to fire, not when it actually started running. Use this as your "as of" timestamp for data queries, and your results will be consistent regardless of drift.
Tip: For Sliding Window triggers, always use
triggerOutputs()?['windowStartTime']andtriggerOutputs()?['windowEndTime']rather than computing periods fromutcNow(). That's exactly what those properties are for.
By default, Power Automate allows multiple instances of a flow to run at the same time. For a flow triggered by an event, that's usually what you want. For a scheduled flow, it's almost never what you want — if a long-running instance is still executing when the next recurrence fires, you can end up with two instances stepping on each other, writing duplicate records, or hitting API rate limits.
To prevent this, open the trigger's Settings (click the three dots on the Recurrence trigger) and find Concurrency Control. Set it to "On" and limit the degree to 1. Now if a run is in progress when the next recurrence fires, the new run will be queued (if you set a queue depth) or skipped.
The queue depth setting is important: a queue of 1–5 gives you a safety net for brief overruns without letting work pile up indefinitely. If you expect your flow to run in 4 minutes and your interval is 5 minutes, a small queue is fine. If your flow regularly takes longer than the interval, you have a design problem to solve, not a queue depth to increase.
Let's build something real: a daily flow that runs at 7:45 AM Eastern Time (Monday through Friday), checks if it's a business day, pulls a summary of the previous day's sales opportunities from Dataverse, formats them into an HTML table, and sends a digest email to a distribution list. On non-business days or holidays, it terminates cleanly with a log entry.
Create a new Scheduled Cloud Flow. In the trigger:
12 (12:00 UTC = 7:00 AM EST / 8:00 AM EDT — we'll choose 12:45 for 7:45 AM EST)4512:45:00ZThis immediately eliminates Saturday and Sunday at the trigger level, reducing noise in run history.
Add Initialize Variable actions for:
LocalTimeString (String):
formatDateTime(convertTimeZone(utcNow(), 'UTC', 'Eastern Standard Time'), 'yyyy-MM-dd HH:mm')
ReportDate (String):
formatDateTime(addDays(convertTimeZone(utcNow(), 'UTC', 'Eastern Standard Time'), -1), 'yyyy-MM-dd')
HolidayCount (Integer): 0
OpportunityHTML (String): ''
ReportDate uses addDays(..., -1) to get yesterday's date in Eastern Time. Note that we apply addDays to the already-converted local time string, so we're subtracting a calendar day from the local perspective, not from UTC.
Add Get items from your SharePoint BusinessHolidays list with this filter query:
Date eq '@{variables('ReportDate')}'
Then add Set Variable to update HolidayCount:
length(body('Get_items')?['value'])
Add a Condition action:
Expression: equals(variables('HolidayCount'), 0)
In the No branch, add Terminate with Status: Succeeded, Message: Holiday skip — @{variables('ReportDate')}.
In the Yes branch, add a List rows action (Dataverse) or Get items (SharePoint, depending on your setup) to pull opportunities where:
OData filter: createdon ge @{variables('ReportDate')}T00:00:00Z and createdon lt @{addDays(variables('ReportDate'), 1)}T00:00:00Z
Note: We're filtering by UTC timestamps from Dataverse but using the local ReportDate as the day boundary. This works because we've already accounted for the timezone when computing ReportDate. For exact precision at day boundaries (especially for teams that work late), consider using the start of the local day in UTC:
convertTimeZone(concat(variables('ReportDate'), 'T00:00:00'), 'Eastern Standard Time', 'UTC').
Add Set Variable for OpportunityHTML with a starting header:
<table border="1" cellpadding="6" style="border-collapse:collapse;font-family:Arial;font-size:13px;">
<tr style="background:#0078d4;color:white;">
<th>Opportunity Name</th><th>Account</th><th>Est. Revenue</th><th>Stage</th>
</tr>
Then add an Apply to each loop over the Dataverse results, appending a row per record:
<tr>
<td>@{items('Apply_to_each')?['name']}</td>
<td>@{items('Apply_to_each')?['_accountid_value@OData.Community.Display.V1.FormattedValue']}</td>
<td>$@{formatNumber(items('Apply_to_each')?['estimatedvalue'], 'N0')}</td>
<td>@{items('Apply_to_each')?['stepname']}</td>
</tr>
After the loop, append the closing </table> tag to OpportunityHTML.
Add Send an email (V2) with:
Daily Opportunity Digest — @{variables('ReportDate')}<p>Good morning,</p>
<p>Here is the opportunity summary for <strong>@{variables('ReportDate')}</strong>,
generated at @{variables('LocalTimeString')} ET.</p>
@{variables('OpportunityHTML')}
<p style="color:#666;font-size:11px;">This report is auto-generated. Do not reply.</p>
Rather than waiting until tomorrow, test the flow manually by:
ReportDate to a date you know has dataCheck the run history, expand each action, and verify the LocalTimeString and ReportDate values look correct before re-enabling the holiday check.
Check first: Is your Start Time in UTC or local time? Open the trigger and look at the Start Time field. If you entered 2024-01-15T08:00:00Z intending 8 AM local, but you're in UTC+5:30, the flow is actually running at 1:30 PM local time.
Fix: Recalculate your Start Time in UTC. For 8 AM IST (UTC+5:30), enter 2024-01-15T02:30:00Z.
This is the "UTC midnight rollover" problem. At 10 PM Eastern on the 15th, UTC is already 3 AM on the 16th. If your flow computes formatDateTime(utcNow(), 'yyyy-MM-dd'), it returns the 16th. Always convert to local time before extracting a date string.
// Wrong: uses UTC date
formatDateTime(utcNow(), 'yyyy-MM-dd')
// Right: uses Eastern date
formatDateTime(convertTimeZone(utcNow(), 'UTC', 'Eastern Standard Time'), 'yyyy-MM-dd')
Usually caused by dayOfWeek() receiving a UTC timestamp instead of a local one. If it's Saturday at 1 AM Eastern, it's Saturday at 6 AM UTC — both are Saturday, so this specific error would manifest when your local time is Saturday but UTC is still Friday (Friday 11 PM Eastern = Saturday 4 AM UTC). The dayOfWeek check on UTC would return Friday (5), passing the weekday check, while the local day is actually Saturday.
Fix: Always pass the local-time string into dayOfWeek(), never a UTC string.
// Wrong:
dayOfWeek(utcNow())
// Right:
dayOfWeek(convertTimeZone(utcNow(), 'UTC', 'Eastern Standard Time'))
Concurrency control is off. Two instances ran simultaneously — usually visible in run history as two runs starting within seconds of each other at the same scheduled time. Enable concurrency control on the trigger and set the degree to 1.
Your flow ran at 7:00 AM instead of 8:00 AM (or vice versa) on the day clocks changed. This happens when the UTC start time was calibrated for Standard Time and isn't automatically adjusted.
If you use Windows Time Zone IDs in convertTimeZone, DST is handled automatically in your expressions. But the Recurrence trigger's Start Time and At These Hours fields are always UTC — they don't adjust for DST. If you need to run at exactly 8 AM local time year-round through DST transitions, your best option is to schedule two flows: one active October–March (standard offset) and one active March–October (DST offset), or schedule at the UTC time that's correct for your more common state and accept the one-hour shift during the other half of the year.
Alternatively, schedule with the Day frequency at a UTC hour that's correct for standard time, and add an internal check that terminates early if the local hour doesn't match your intent — letting the flow re-fire next cycle.
The Terminate action with Status: Succeeded is working as intended — the flow hit your business-hours or holiday guard and exited cleanly. Check the run history, expand the Terminate action, and read the message you configured. This is a feature, not a bug.
The backfill is working correctly, which means the system had a real outage and is catching up. To prevent API overload, enable concurrency control (degree of 1) and add a Delay action at the end of the flow body. The Delay slows the catch-up cadence. Also review whether all those backfill runs actually need to execute their full logic, or whether you can add a staleness check: if triggerOutputs()?['windowStartTime'] is more than 24 hours old, log it and skip rather than trying to process very stale data.
Time-based flows look deceptively simple until you're in production explaining to a VP why the "daily" report ran on Sunday. The concepts you've worked through in this lesson form a solid foundation for all scheduling work in Power Automate:
convertTimeZone: Your primary tool for any local-time reasoning, with automatic DST handlingdayOfWeek and hour extraction: The two primitives of business-hours logic, always applied to locally-converted timestampsImmediate next steps to reinforce this:
Audit one existing scheduled flow you own. Does it have an explicit Start Time? Is that time in UTC? Does it have concurrency control enabled? Fix anything that's off.
Add a LocalTimeString variable to that same flow and include it in the output (email subject, SharePoint item, whatever makes sense). This makes your flow's perspective on time visible and verifiable.
Explore the Retry Policy settings on individual actions within scheduled flows. Understanding how retries interact with time-based logic is a natural next topic, especially for flows that call external APIs.
Where to go next in this learning path:
Learning Path: Flow Automation Basics