🚀 New batches open: Advanced Excel • Power BI • SQL • AI for Analytics — Book a free demo

Day 8 — Dates, times and time zones

Power Automate runs on UTC and your users do not. Almost every date bug comes from that one sentence.

Now

@utcNow()
@utcNow('dd-MM-yyyy')
@utcNow('dddd')
The clock here is frozen This playground pretends it is always 18 March 2026, 09:30 UTC, so every example gives the same answer each time you run it and the lesson text stays true. In a real flow utcNow() is the actual moment the action ran.

Format strings

CodeGivesOn 18 Mar 2026, 09:30
yyyyFour-digit year2026
MMMonth number, padded03
MMMShort monthMar
MMMMFull monthMarch
ddDay, padded18
ddddDay nameWednesday
HHHour, 24-hour09
mmMinute30
Capital MM is month, small mm is minute This is the mistake everybody makes. dd-MM-yyyy is a date. dd-mm-yyyy puts the minutes where the month should be, and because the answer still looks like a date nobody notices until the report is out.
@formatDateTime(utcNow(), 'dd-MM-yyyy')
@formatDateTime(utcNow(), 'dd-mm-yyyy')
@formatDateTime(utcNow(), 'dd MMM yyyy')
@formatDateTime(utcNow(), 'dddd dd MMMM yyyy')
@formatDateTime(utcNow(), 'yyyy-MM-dd HH:mm')

Line two is the wrong one. It reads 18-30-2026, because 30 is the minutes.

Time zones

@utcNow()
@convertTimeZone(utcNow(), 'UTC', 'India Standard Time')
@convertTimeZone(utcNow(), 'UTC', 'India Standard Time', 'dd MMM yyyy HH:mm')
@convertTimeZone(utcNow(), 'UTC', 'India Standard Time', 'HH:mm')

09:30 UTC is 15:00 in India - five and a half hours ahead.

Convert once, at the edge Do the arithmetic in UTC and convert only when you show a human the answer - in an email body, a file name, a report title. Converting early and then adding days to a converted value is how you end up an hour out twice a year in countries that use daylight saving.
India has no daylight saving, so the offset is always +5:30. That makes it easier than most, but the habit is still worth having.

Date arithmetic

@addDays(utcNow(), 7, 'dd MMM yyyy')
@addDays(utcNow(), -1, 'dd MMM yyyy')
@addHours(utcNow(), 48, 'dd MMM HH:mm')
@addToTime(utcNow(), 3, 'Month', 'dd MMM yyyy')
@addToTime(utcNow(), 1, 'Year', 'dd MMM yyyy')
@startOfDay(utcNow())
@startOfMonth(utcNow(), 'dd MMM yyyy')
Negative numbers go backwards There is no subtractDays(). addDays(utcNow(), -1) is yesterday, and that is the accepted way to write it.

Comparing dates

ISO dates sort correctly as text, which means the ordinary comparison functions work on them without any conversion.

@less(triggerBody()?['DueDate'], utcNow())
@greater(triggerBody()?['StartDate'], utcNow())
@formatDateTime(triggerBody()?['DueDate'], 'dd MMM yyyy')
@if(less(triggerBody()?['DueDate'], utcNow()), 'Overdue', 'Still open')
Compare full ISO strings, not formatted ones less('15-03-2026', '18-03-2026') compares text character by character and gives the wrong answer as soon as the months differ. Always compare the raw ISO value - 2026-03-15T00:00:00Z - and format only for display.

A report window

A scheduled flow that reports on "everything since yesterday" is the most common date job there is.

{
  "trigger": { "name": "Every weekday at 9am IST", "type": "Recurrence", "body": {} },
  "actions": [
    { "name": "Window start", "type": "Compose", "input": "@startOfDay(addDays(utcNow(), -1))" },
    { "name": "Window end",   "type": "Compose", "input": "@startOfDay(utcNow())" },
    { "name": "Report title", "type": "Compose",
      "input": "MIS report for @{formatDateTime(addDays(utcNow(), -1), 'dd MMM yyyy')}" },
    { "name": "File name", "type": "Compose",
      "input": "MIS_@{formatDateTime(addDays(utcNow(), -1), 'yyyy-MM-dd')}.xlsx" },
    { "name": "Rows", "type": "Compose", "input": [
        { "Id": 1, "Created": "2026-03-17T11:20:00Z", "Amount": 52000 },
        { "Id": 2, "Created": "2026-03-17T18:45:00Z", "Amount": 9500  },
        { "Id": 3, "Created": "2026-03-18T08:05:00Z", "Amount": 14500 }
      ] },
    { "name": "Yesterday only", "type": "FilterArray",
      "from": "@outputs('Rows')",
      "where": "@and(greaterOrEquals(item()?['Created'], outputs('Window start')), less(item()?['Created'], outputs('Window end')))" },
    { "name": "Count", "type": "Compose", "input": "@length(outputs('Yesterday only'))" }
  ]
}

Two of the three rows are from 17 March. The third is from this morning, so it belongs in tomorrow’s report - which is exactly why the window has a start and an end.

File names want yyyy-MM-dd Name a file with dd-MM-yyyy and a folder full of them sorts by day-of-month, so January and December interleave. yyyy-MM-dd sorts chronologically on its own. Same for anything you might later sort as text.

Try these yourself

  1. Print today in three different formats, including the day name.
  2. Work out what 09:30 UTC is in Indian time, and write the expression for it.
  3. Build a file name for last month’s report, in a form that sorts correctly.
  4. Write a condition that is true when a due date has passed.
  5. Explain the difference between MM and mm, and what breaks when you confuse them.