Expression Basics: How Power Automate Evaluates Expressions
Power Automate expressions are written in a language that Microsoft calls the workflow definition language — a function-based expression language (similar in spirit to Excel formulas) that evaluates at runtime inside the flow engine. Expressions are enclosed in @{} when embedded in strings, or prefixed with @ when used as standalone field values. Understanding this distinction prevents one of the most common novice mistakes: wrapping an entire expression in quotes when it should be a bare @expression() call.
Every expression in Power Automate returns one of six data types: string, integer, float, boolean, array, or object. Type mismatch errors — where a function receives a string when it expects an integer, or vice versa — are the leading cause of flow failures in production. The expression editor in Power Automate's designer does not enforce types at edit time, so errors only surface at runtime. Developing a mental model of what type each function accepts and returns is the single most valuable skill for Power Automate developers beyond the basics.
Expressions can be nested arbitrarily deep — the output of one function becomes the input of another. A complex expression like formatDateTime(addDays(utcNow(), 30), 'dd/MM/yyyy') chains three functions: utcNow() returns the current UTC timestamp, addDays() adds 30 days to it, and formatDateTime() converts the result to a readable date string. Mastering this nesting pattern is what separates Power Automate developers who can solve complex problems in a single expression from those who need five sequential Compose actions to achieve the same result.
Use a Compose action with your expression during development and check the run history output before wiring it into a consequential action. The Compose action shows you exactly what the expression evaluates to — invaluable for diagnosing unexpected outputs.
String Functions: concat, substring, replace, split, trim
String manipulation is the most frequently needed category of Power Automate expressions. Whether you are constructing dynamic SharePoint URLs, formatting display names from first and last name fields, or sanitising user input before storage, string functions are in nearly every production flow.
concat() joins two or more strings. Unlike Excel's CONCATENATE, Power Automate's concat accepts unlimited arguments: concat('Hello ', triggerBody()?['Author'], ' — your submission has been received.'). For more readable multi-part strings, consider using the string interpolation syntax inside dynamic content instead: @{triggerBody()?['FirstName']} @{triggerBody()?['LastName']}.
// concat — join multiple strings concat('Project-', variables('ProjectCode'), '-', formatDateTime(utcNow(), 'yyyyMM')) // Result: "Project-PRJ001-202603" // substring — extract part of a string substring('INV-2026-00142', 4, 4) // Result: "2026" // replace — swap a substring replace(triggerBody()?['EmailBody'], '[CompanyName]', 'Contoso Ltd') // split — convert delimited string to array split(triggerBody()?['TagList'], ';') // Input: "finance;legal;hr" → Output: ["finance","legal","hr"] // trim — remove leading/trailing whitespace trim(triggerBody()?['SubmittedName']) // toLower / toUpper toLower(triggerBody()?['EmailAddress']) // startsWith / endsWith / contains startsWith(triggerBody()?['InvoiceNumber'], 'INV') endsWith(triggerBody()?['FileName'], '.pdf') contains(triggerBody()?['Description'], 'urgent') // indexOf — find position of a character indexOf(triggerBody()?['Email'], '@') // length — count characters length(triggerBody()?['Comment'])
Date and Time Functions: utcNow, addDays, formatDateTime, convertTimeZone
Date and time manipulation is the second most common expression need, and also the most error-prone area for developers new to Power Automate. The key mental model to internalise: Power Automate works internally in UTC ISO 8601 format (2026-03-30T09:00:00.0000000Z). All date/time functions that accept a datetime value expect this format. The formatDateTime() function converts to human-readable display; convertTimeZone() shifts to a local timezone.
// utcNow — current UTC timestamp utcNow() // Result: "2026-03-30T09:00:00.0000000Z" // formatDateTime — format for display formatDateTime(utcNow(), 'dd MMMM yyyy') // Result: "30 March 2026" formatDateTime(utcNow(), 'yyyy-MM-dd') // Result: "2026-03-30" // addDays — add or subtract days addDays(utcNow(), 30) addDays(utcNow(), -7) // 7 days ago // addHours, addMinutes, addSeconds, addMonths addHours(utcNow(), 2) addMonths(utcNow(), 3) // convertTimeZone — localise for display convertTimeZone(utcNow(), 'UTC', 'India Standard Time', 'dd/MM/yyyy HH:mm') // Result: "30/03/2026 14:30" // dayOfWeek — get day number (0=Sunday) dayOfWeek(utcNow()) // ticks — compare dates (larger ticks = later date) greater(ticks(triggerBody()?['DueDate']), ticks(utcNow())) // getPastTime / getFutureTime getPastTime(1, 'Month') getFutureTime(14, 'Day')
Logical Functions: if, and, or, not, equals
Logical functions are the decision-making layer of Power Automate expressions. While the flow designer has dedicated Condition shapes for branching, logical expressions let you inline conditional logic directly inside field values — avoiding extra shapes and making flows more readable. The if() function is the workhorse: if(equals(triggerBody()?['Priority'], 'High'), 'Urgent', 'Normal').
// if(condition, trueValue, falseValue) if(equals(triggerBody()?['Status'], 'Approved'), '✓ Approved', '⚠ Pending') // and / or — combine conditions and( equals(triggerBody()?['Department'], 'Finance'), greater(triggerBody()?['Amount'], 10000) ) or( equals(triggerBody()?['Priority'], 'High'), equals(triggerBody()?['Priority'], 'Critical') ) // not — negate a boolean not(equals(triggerBody()?['IsArchived'], true)) // equals, greater, less, greaterOrEquals, lessOrEquals greaterOrEquals(triggerBody()?['LeaveBalance'], 5) // Nested if for multi-branch logic if( equals(triggerBody()?['Score'], 'A'), 'Excellent', if(equals(triggerBody()?['Score'], 'B'), 'Good', 'Needs Improvement') ) // coalesce — return first non-null value coalesce(triggerBody()?['PreferredName'], triggerBody()?['FirstName'], 'Unknown')
Array Functions: length, first, last, union, intersection, contains, filter
Array functions become essential the moment your flows work with collections — SharePoint list items returned by a "Get items" action, email recipients, multi-select choice values, or JSON arrays from HTTP responses. Mastering array expressions eliminates the need for Apply to Each loops in many common scenarios, dramatically improving flow performance.
// length — count array items length(outputs('Get_items')?['body/value']) // first / last — get first or last element first(outputs('Get_items')?['body/value']) last(outputs('Get_items')?['body/value']) // union — combine two arrays (deduplicated) union(variables('TagsArray1'), variables('TagsArray2')) // intersection — items present in both arrays intersection(variables('RequiredRoles'), variables('UserRoles')) // contains — check if value exists in array contains(variables('ApprovedCountries'), triggerBody()?['Country']) // skip / take — slice an array take(outputs('Get_items')?['body/value'], 5) // first 5 skip(outputs('Get_items')?['body/value'], 10) // from item 11 onwards // join — convert array to delimited string join(variables('TagsArray'), ', ') // Input: ["finance","legal","hr"] → "finance, legal, hr" // createArray — build array from values createArray( triggerBody()?['PrimaryEmail'], triggerBody()?['CCEmail'] ) // range — generate integer sequence range(1, 12) // [1,2,3,...,12]
Use filter() in OData query expressions on SharePoint "Get items" actions rather than filtering with array functions after fetching. Filtering server-side reduces data transferred and speeds up flows significantly — especially on lists with thousands of items.
JSON Functions: json, string, base64, xpath
JSON functions bridge the gap between Power Automate's native data types and raw string payloads. They are essential when integrating with HTTP APIs, parsing webhook bodies, or constructing JSON payloads for outgoing requests.
// json — parse JSON string into an object json(triggerBody()?['RawPayload']) // string — serialise object to JSON string string(variables('MyObject')) // base64 — encode string to Base64 base64(concat(variables('ClientId'), ':', variables('ClientSecret'))) // base64ToString — decode Base64 base64ToString(triggerBody()?['EncodedData']) // decodeUriComponent — decode URL-encoded string decodeUriComponent(triggerBody()?['CallbackUrl']) // encodeURIComponent — URL-encode a string encodeURIComponent(variables('SearchQuery')) // xpath — extract value from XML xpath(xml(body('Parse_XML')), 'string(//Invoice/TotalAmount)') // Construct a JSON object inline json( concat( '{"name":"', triggerBody()?['FullName'], '","email":"', triggerBody()?['Email'], '"}' ) )
Null Handling: Avoiding the ?['field'] vs ['field'] Trap
Null handling is where most Power Automate flows fail silently in production. When a trigger or action output contains a field that may not always be present, accessing it with the standard bracket notation ['FieldName'] throws an error if the field is null or missing. The null-safe operator ?['FieldName'] returns null instead of throwing, allowing your flow to continue and handle the null gracefully.
The correct pattern for null-safe field access is to combine the null-safe operator with coalesce() to provide a default: coalesce(triggerBody()?['OptionalField'], 'Default Value'). This pattern handles three scenarios: the field is present with a value (returns the value), the field is present but null (returns the default), and the field does not exist in the JSON body (returns the default).
// Safe field access with default coalesce(triggerBody()?['ManagerEmail'], '[email protected]') // Check for empty string AND null if( or( empty(triggerBody()?['Description']), equals(triggerBody()?['Description'], null) ), 'No description provided', triggerBody()?['Description'] ) // Check before doing math on a nullable number if( equals(triggerBody()?['Quantity'], null), 0, mul(triggerBody()?['Quantity'], triggerBody()?['UnitPrice']) ) // Null-safe array length (avoids error on null array) length(coalesce(outputs('Get_items')?['body/value'], createArray()))
20+ Real-World Expression Recipes
The following recipes are drawn from production flows across SharePoint, Teams, Outlook, and custom HTTP integrations. Each solves a concrete problem that appears repeatedly in enterprise Power Automate development.
// 1. Extract domain from email address substring(triggerBody()?['Email'], add(indexOf(triggerBody()?['Email'], '@'), 1), 100) // 2. Build SharePoint item URL from site and ID concat(variables('SiteUrl'), '/Lists/Tasks/DispForm.aspx?ID=', triggerBody()?['ID']) // 3. Get current financial year string (Apr-Mar) if( greaterOrEquals(int(formatDateTime(utcNow(), 'M')), 4), concat('FY', formatDateTime(utcNow(), 'yy'), '-', string(add(int(formatDateTime(utcNow(), 'yy')), 1))), concat('FY', string(sub(int(formatDateTime(utcNow(), 'yy')), 1)), '-', formatDateTime(utcNow(), 'yy')) ) // 4. Count business days between two dates (approximate) sub( div(sub(ticks(variables('EndDate')), ticks(variables('StartDate'))), 864000000000), mul(div(sub(ticks(variables('EndDate')), ticks(variables('StartDate'))), 6048000000000), 2) ) // 5. Generate a unique reference number concat('REF-', formatDateTime(utcNow(), 'yyyyMMdd'), '-', substring(guid(), 0, 8)) // 6. Convert array of objects to comma-separated names join( xpath(xml(json(concat('{"r":{"i":', string(variables('PeopleArray')), '}}'))), '//i/DisplayName/text()'), ', ' ) // 7. Truncate long text for email subject if( greater(length(triggerBody()?['Title']), 60), concat(substring(triggerBody()?['Title'], 0, 60), '...'), triggerBody()?['Title'] ) // 8. Check if today is a Monday equals(dayOfWeek(utcNow()), 1) // 9. Pad a number with leading zeros (e.g., 42 → "00042") substring(concat('00000', string(triggerBody()?['InvoiceNumber'])), sub(length(concat('00000', string(triggerBody()?['InvoiceNumber']))), 5), 5) // 10. Calculate VAT amount and total add( float(triggerBody()?['NetAmount']), mul(float(triggerBody()?['NetAmount']), 0.18) )
Key Takeaways
Power Automate works internally in UTC ISO 8601 format — always convert timestamps for display with formatDateTime() and convertTimeZone(), never store timezone-converted values.
Use the null-safe operator ?['field'] combined with coalesce() for any field that may be absent or null — this prevents the majority of production flow failures.
Array functions like join(), first(), take(), and skip() often eliminate the need for Apply to Each loops, improving flow performance dramatically.
Prototype complex expressions in a Compose action and verify the output in run history before using them in consequential actions like Send Email or Update Item.
Nested if() expressions handle multi-branch logic inline without adding extra Condition shapes — keep nesting to a maximum of three levels for readability.