LowCodeStacks™
Sign in

Power Automate formatDateTime and time zones that come out right

Use Power Automate formatDateTime, convertTimeZone and addDays to show dates in your time zone, filter SharePoint and Dataverse by day, and stop off-by-one dates.

Power AutomateFix a problem11 min read

Checked against Microsoft Learn · 12 sourcesPosted by the Maker DeskHow we write guides

A date in a flow can be right in SharePoint and a day out in your email. The cause is nearly always the same: the value is in UTC, and it was formatted before anyone converted it. This guide shows the Power Automate formatDateTime function with the right format letters, how to convert time zones with an expression or with no code at all, and how to compare dates so "today" means your today.

A due date of 21 October, midnight, in India is stored in UTC as 2026-10-20T18:30:00Z. Formatting it straight away with formatDateTime and dd/MM/yyyy prints 20/10/2026, a day early; converting first with convertFromUtc and India Standard Time prints 21/10/2026. A table shows that MM is the month (10), mm is minutes (30), HH is the 24-hour clock (18), and hh with tt is the 12-hour clock with AM or PM (06 PM).
Convert from UTC to your time zone first, then format.

Why your dates arrive in UTC

Power Automate uses Coordinated Universal Time (UTC) by default. Three things catch people out:

  • utcNow() is always UTC. It returns the current date and time, by default in the round-trip format yyyy-MM-ddTHH:mm:ss.fffffffK, such as 2025-09-25T05:18:31.3384797Z. A Z at the end means UTC.
  • SharePoint sends UTC. SharePoint's back end uses UTC. The SharePoint web page shows your local time only because the browser converts it for you, so treat a SharePoint date in a flow as UTC until you convert it.
  • Dataverse stores UTC. Dataverse stores all date and time values in UTC. A column with the default User local behaviour comes back from a retrieve as UTC. Date only columns are the exception: they store a date with no time and no time zone conversion.
Where a date goes wrong, and where it comes right
  1. Stored in UTC
  2. Read by the flow as UTC
  3. Converted to your time zone
  4. Formatted for people or compared by day
  5. Right day, right hour

Convert to your time zone first

With an expression

Three functions do the work. Each takes an optional format string as its last parameter.

FunctionWhat it doesExample
convertTimeZone(timestamp, source, destination, format?)Any zone to any zoneconvertTimeZone(utcNow(), 'UTC', 'Eastern Standard Time')
convertFromUtc(timestamp, destination, format?)UTC to your zone; the result has no UTC offsetconvertFromUtc(utcNow(), 'India Standard Time')
convertToUtc(timestamp, source, format?)Your zone back to UTCconvertToUtc('01/01/2018 00:00:00', 'Pacific Standard Time') returns 2018-01-01T08:00:00.0000000Z

The time zone names are Windows time zone names, not offsets or city names:

WhereName to use
UK and Ireland (Dublin, Edinburgh, Lisbon, London)GMT Standard Time
India (Chennai, Kolkata, Mumbai, New Delhi)India Standard Time
Eastern Time (US and Canada)Eastern Standard Time
Amsterdam, Berlin, Bern, Rome, Stockholm, ViennaW. Europe Standard Time
Pacific Time (US and Canada)Pacific Standard Time

Greenwich Standard Time is a different zone (Monrovia, Reykjavik), so don't pick it for the UK. If a name with punctuation, such as W. Europe Standard Time, is rejected, Microsoft notes you might have to remove the punctuation.

Warning

Don't fake a time zone with addHours(utcNow(), 5). addHours takes a whole number of hours, so it can't even express India's UTC+05:30, and a fixed offset is wrong for half the year anywhere the clocks change. Use a time zone name, and test with dates either side of a clock change.

With the Convert time zone action

If you'd rather not write expressions, use the built-in Convert time zone action. In the designer, search for convert time zone and choose the built-in operation. Fill in:

  • Base time: the date and time to convert, usually dynamic content.
  • Source time zone: the zone it's in now, usually UTC.
  • Destination time zone: the zone you want.
  • Format string (optional): how the result should look.

The action and the convertTimeZone expression do the same job. The action is easier to read for the next person; the expression fits inside another expression.

Format with formatDateTime

formatDateTime(timestamp, format?, locale?) returns the timestamp as text. The format can be a standard format (one letter) or a custom format (more than one character).

FormatKindResult for 15 June 2009, 13:45:30
'dd/MM/yyyy'Custom15/06/2009
'yyyy-MM-dd'Custom2009-06-15
'dd MMM yyyy HH:mm'Custom15 Jun 2009 13:45
'dddd d MMMM yyyy'CustomMonday 15 June 2009
'hh:mm tt'Custom01:45 PM
'D'StandardMonday, June 15, 2009
's'Standard2009-06-15T13:45:30
'u'Standard2009-06-15 13:45:30Z

The letters that matter:

  • yyyy year; MM month 01–12; MMM and MMMM the short and full month name.
  • dd day 01–31; ddd and dddd the short and full day name.
  • HH hour on a 24-hour clock; hh on a 12-hour clock, with tt for AM or PM.
  • mm minutes; ss seconds.

Standard formats such as 'd' follow the language, so 'd' gives 9/25/2025 in the default en-us. For a fixed layout such as dd/MM/yyyy, write the custom format. The third parameter changes the language of day and month names: formatDateTime('01/31/2016', 'dddd MMMM d', 'fr-FR') returns dimanche janvier 31.

Format last. The result is text, so do the converting and the date maths first, then format once, at the point where a person reads it or you compare days. To turn text such as 25/12/2026 back into a date, use parseDateTime with a locale and the format, so the day and month can't swap: parseDateTime('25/12/2026', 'en-GB', 'dd/MM/yyyy').

Add, subtract and start of day

  • addDays(timestamp, days, format?) adds days; use a negative number to go back.
  • addHours, addMinutes and addSeconds work the same way.
  • addToTime(timestamp, interval, unit, format?) adds weeks, months or years, with 'Week', 'Month' or 'Year' as the unit.
  • startOfDay(timestamp, format?) moves the time to midnight: startOfDay('2018-03-15T13:30:30Z') returns 2018-03-15T00:00:00.0000000Z.
  • dayOfWeek(timestamp) returns 0 for Sunday, 1 for Monday, up to 6 for Saturday.

The order matters. startOfDay(utcNow()) is midnight in UTC, not yours. Convert first, then take the start of the day:

text
startOfDay(convertFromUtc(utcNow(), 'India Standard Time'))

To filter a source that expects UTC, turn that local midnight back into UTC:

text
convertToUtc(startOfDay(convertFromUtc(utcNow(), 'India Standard Time')), 'India Standard Time', 'yyyy-MM-ddTHH:mm:ssZ')

Compare dates in conditions and filters

In a Condition or Filter array

Put both sides in the same time zone and the same format. For "is this item due today in London", compare the two dates as yyyy-MM-dd text, which convertTimeZone can produce in one step:

text
@equals(convertTimeZone(item()?['DueDate'], 'UTC', 'GMT Standard Time', 'yyyy-MM-dd'), convertTimeZone(utcNow(), 'UTC', 'GMT Standard Time', 'yyyy-MM-dd'))

For "before" and "after", compare ticks, which count time from the year 1: greater(ticks(variables('dateA')), ticks(variables('dateB'))). Comparing date text with greater only works for yyyy-MM-dd; '03/18/2026' against '12/01/2025' gives the wrong answer.

An expression typed into a trigger input is worked out once, when you save the flow. utcNow() there becomes a fixed time and never moves on. Do date comparisons in actions instead.

In a SharePoint Get items filter

Dates go in single quotes, and the column is its internal name (spaces become _x0020_). This filter, adapted from Microsoft's own example, asks for items starting after today:

text
Start_x0020_Date gt '@{formatDateTime(utcNow(), 'yyyy-MM-dd')}'

For "due today", don't rely on one exact date: the stored value is UTC and can carry a time. Ask Get items for a window a day either side, then keep the exact local day with the Filter array expression above. The reminder emails guide builds this pattern step by step.

text
DueDate ge '@{formatDateTime(addDays(utcNow(), -1), 'yyyy-MM-dd')}' and DueDate le '@{formatDateTime(addDays(utcNow(), 1), 'yyyy-MM-dd')}'

In a Dataverse List rows filter

In Filter rows, dates have no quotes, and the column is its logical name: createdon ge 2026-01-01T00:00:00Z. To get rows created since midnight in India, use the UTC-midnight expression from the previous section:

text
createdon ge @{convertToUtc(startOfDay(convertFromUtc(utcNow(), 'India Standard Time')), 'India Standard Time', 'yyyy-MM-ddTHH:mm:ssZ')}

A Date only column needs no conversion: it has no time to shift. More filter syntax is in the OData filter query cheat sheet.

The off-by-one-day bug

Someone in India sets a due date of 21 October, midnight. India is UTC+05:30, so the UTC value is 2026-10-20T18:30:00Z. A flow that runs formatDateTime(item()?['DueDate'], 'dd/MM/yyyy') prints 20/10/2026: a day early. In a zone behind UTC, a late-evening time tips into the next day instead.

The fix is always the same: convert, then format.

text
formatDateTime(convertFromUtc(item()?['DueDate'], 'India Standard Time'), 'dd/MM/yyyy')

What are you seeing?

Do

  • Name the zone in every conversion, and keep the name in one Compose or variable so it's changed in one place.
  • Convert, then do the date maths, then format, in that order.
  • Compare days as yyyy-MM-dd text in the same time zone.
  • Test with an item near midnight, and one either side of a clock change.

Don't

  • Don't format a SharePoint or Dataverse date for people without converting it from UTC.
  • Don't use addHours as a time zone.
  • Don't compare dates written as dd/MM/yyyy text: the order is wrong for sorting and comparing.
  • Don't convert a Dataverse Date only value.

Copy-ready expressions

Swap 'GMT Standard Time' for your own zone.

TaskExpression
Now, in UTCutcNow()
Now, in your zoneconvertFromUtc(utcNow(), 'GMT Standard Time')
Today as dd/MM/yyyy, your zoneconvertFromUtc(utcNow(), 'GMT Standard Time', 'dd/MM/yyyy')
Today as yyyy-MM-dd, your zoneconvertTimeZone(utcNow(), 'UTC', 'GMT Standard Time', 'yyyy-MM-dd')
A SharePoint date for an emailformatDateTime(convertFromUtc(item()?['DueDate'], 'GMT Standard Time'), 'dd MMM yyyy HH:mm')
Seven days from nowaddDays(utcNow(), 7)
Thirty days ago, as a dateaddDays(utcNow(), -30, 'yyyy-MM-dd')
Next monthaddToTime(utcNow(), 1, 'Month')
Midnight today, your zonestartOfDay(convertFromUtc(utcNow(), 'GMT Standard Time'))
Your midnight, in UTCconvertToUtc(startOfDay(convertFromUtc(utcNow(), 'GMT Standard Time')), 'GMT Standard Time')
Day of the week (0 is Sunday)dayOfWeek(convertFromUtc(utcNow(), 'GMT Standard Time'))
Is A after B?greater(ticks(variables('dateA')), ticks(variables('dateB')))
Text to a dateparseDateTime('25/12/2026', 'en-GB', 'dd/MM/yyyy')
A file name stampformatDateTime(utcNow(), 'yyyy-MM-dd HH:mm')

Related: Power Automate expressions cheat sheet.

Sources

Did this fix it?

Comments

No comments yet. Share what worked for you.

Sign in to comment. Comments show under a display name you choose, never your email.