Power Automate expression cookbook: dates, strings, arrays
The 18 expressions you actually reach for, with the syntax that trips people up called out honestly.
Before you start
Power Automate expressions are written in the Workflow Definition Language — the same function syntax Logic Apps uses. You type them into the Expression tab of the dynamic content picker, not into a regular text field. They are not Power Fx, and they are not the same as the formula language in canvas apps. Different engine, different functions.
This is a cookbook, not a reference. It covers the expressions you actually reach for when wrangling dates, strings, and arrays, plus the two or three behaviors that quietly produce wrong output instead of an error. If you only remember one thing, remember this: in date format strings, MM is month and mm is minute. That single distinction is responsible for more broken flows than any other expression mistake.
Every expression below is real and runs as written. Swap utcNow() or the literal strings for your own dynamic content.
Dates and times
Get "now" and format it
utcNow() returns the current time as an ISO 8601 string in UTC, like 2026-05-31T14:30:00.0000000Z. You can pass a format string directly:
utcNow('yyyy-MM-dd')
formatDateTime() does the same to any timestamp you already have:
formatDateTime('2026-05-31T14:30:00Z', 'dd/MM/yyyy HH:mm')
That returns 31/05/2026 14:30. The format tokens are standard .NET custom date strings: yyyy four-digit year, MM two-digit month, dd day, HH 24-hour hour, hh 12-hour, mm minute, ss second, tt AM/PM. To put literal text inside a format, wrap it in single quotes: 'yyyy-MM-dd''T''HH:mm:ss'.
MM is month. mm is minute. If your dates come out with the wrong number in the middle, you almost certainly wrote yyyy-mm-dd and got the minute where the month should be. There is no error — the flow runs happily and produces garbage. This is the first thing to check when a date looks off.
Add and subtract time
addDays() takes a timestamp, a number of days, and an optional format. Use a negative number to go backward:
addDays(utcNow(), -7, 'yyyy-MM-dd')
That gives you the date a week ago. There are matching addHours(), addMinutes(), and addSeconds() functions. For anything coarser than a day — weeks, months, years — use addToTime() and subtractFromTime(), which take the unit as a string:
addToTime(utcNow(), 3, 'Month', 'yyyy-MM-dd')
Valid units are Second, Minute, Hour, Day, Week, Month, and Year. So "first of next month" is two steps: jump a month, then snap to the start.
startOfMonth(addToTime(utcNow(), 1, 'Month'), 'yyyy-MM-dd')
startOfMonth(), startOfDay(), and startOfHour() zero out everything below their unit, which is what you want for "did this happen today" comparisons.
Parse an ISO string
You rarely need a parsing function. formatDateTime() accepts any valid ISO 8601 input and reformats it, so parsing and reformatting are the same operation:
formatDateTime('2026-05-31T14:30:00.0000000Z', 'dddd, MMMM d')
That returns Sunday, May 31. If your source string is not ISO 8601 — for example 31/05/2026 from a European export — Power Automate cannot parse it directly. Split it and rebuild it as ISO before formatting.
Time zones
This is the other place people lose an afternoon. utcNow() is UTC, and most connectors return UTC. To show a local time, convert it:
convertFromUtc(utcNow(), 'Eastern Standard Time', 'yyyy-MM-dd HH:mm')
The catch is the time zone name. Power Automate uses Windows time zone IDs, not IANA names. It is 'Eastern Standard Time', not 'America/New_York'. It is 'GMT Standard Time' for UK time, not 'Europe/London'. Get the ID wrong and you get an error, which at least is better than a silent one. convertTimeZone() does the same between two named zones when neither side is UTC.
Date math with ticks
To compare two dates or measure a span, convert both to ticks() — the number of 100-nanosecond intervals since year 0001 — and do plain arithmetic. One day is 864,000,000,000 ticks:
div(sub(ticks(utcNow()), ticks('2026-01-01T00:00:00Z')), 864000000000)
That returns whole days elapsed since the start of the year. ticks() is also the cleanest way to ask "is date A after date B" — compare the two tick values as numbers and skip string comparison entirely.
Strings
Cut a piece out
substring() takes the text, a start index (zero-based), and a length:
substring('Power Automate', 0, 5)
That returns Power. The trap: if start plus length runs past the end of the string, substring() throws an error rather than clamping. When the length is variable, that error is waiting for you.
slice() is the safer cousin. It takes a start and an optional end index, accepts negative numbers to count from the end, and clamps instead of throwing:
slice('Power Automate', 6)
Returns Automate. slice('Power Automate', -8) returns the same thing by counting back from the end. Reach for slice() over substring() whenever the boundaries are not fixed.
Find a position
indexOf() returns the zero-based position of the first match, or -1 if there is no match. lastIndexOf() finds the final occurrence:
indexOf('orders/2026/invoice.pdf', '/')
Combine them to pull a value out of a known shape — for example, the filename after the last slash:
slice('orders/2026/invoice.pdf', add(lastIndexOf('orders/2026/invoice.pdf', '/'), 1))
indexOf() and lastIndexOf() are case-insensitive — searching for world finds World. But contains(), startsWith(), endsWith(), and equals() are case-sensitive. So indexOf('Hello', 'hello') returns 0, while contains('Hello', 'hello') returns false. This asymmetry is documented behavior, and it is the kind of thing Microsoft Learn does not put in a warning box. When case matters in a comparison, normalize both sides with toLower() first.
Replace, split, and the rest
replace() swaps every occurrence of a substring. It is case-sensitive:
replace('555-867-5309', '-', '')
split() breaks a string into an array on a delimiter:
split('alice@contoso.com,bob@contoso.com', ',')
The rest of the everyday kit: concat('Hi, ', 'there') joins strings, toLower() and toUpper() change case, trim() strips leading and trailing whitespace, and length('hello') returns 5. length() works on both strings and arrays, which is convenient and occasionally confusing when you forget which one you are holding.
Arrays
Read from an array
length(), first(), and last() cover most needs. first() and last() return the element itself, not a single-item array:
first(body('Get_items')?['value'])
The ?[...] is null-safe property access — if value is missing, you get null instead of an error. Use it whenever you reach into a connector's output.
Dedupe with union
union() merges two arrays and removes duplicates. Pass the same array twice and you get its distinct values — the standard dedupe trick:
union(variables('tags'), variables('tags'))
Find common items with intersection
intersection() returns only the elements present in every array you pass:
intersection(variables('requestedSkills'), variables('candidateSkills'))
That gives you the skills a candidate actually has from the requested list. This is the closest thing expressions offer to filtering one list against another.
There is no expression that filters an array by a condition, and there is no set-difference function. People build elaborate union/intersection chains to fake "items in A but not in B," and it works, but it is hard to read and easy to break. For anything beyond a clean intersection or dedupe, use the Filter array action or a Select with a filter — they are clearer and your future self will thank you.
Reshape and combine
join() turns an array into a string with a separator, which is how you build a comma-separated list for an email or a SQL IN clause:
join(variables('emails'), '; ')
take(array, n) keeps the first n elements and skip(array, n) drops them — useful together for paging through a batch. contains(array, value) checks membership, and empty(array) returns true for an empty array, an empty string, or an empty object, so it is a handy "is there anything here" guard:
if(empty(body('Get_items')?['value']), 'No records', 'Found records')
What to remember
MMis month,mmis minute. When a date looks wrong, check the format string first.- Everything is UTC until you
convertFromUtc(), and time zones are Windows IDs like'Eastern Standard Time', not IANA names. slice()oversubstring()when boundaries are variable — it clamps instead of throwing.- Case rules are inconsistent:
indexOfignores case,contains/startsWith/endsWith/equalsdo not. Normalize withtoLower()when it matters. uniondedupes,intersectionfinds common items. For real filtering, use the Filter array action.
Keep these in a scratch flow you can copy from. Most expression work is recombining the same dozen functions, and once the format-string and time-zone traps stop catching you, the rest is mechanical.