Expressions & JSON Cheat Sheet
The Workflow Definition Language (WDL) expressions behind cloud flows — string, collection, logical, date, encoding, and reference functions, plus JSON handling, array/date/JSON cookbooks, and the gotchas that burn everyone. Filter the playground to find a function fast.
- concatString
Join strings or values
concat('Hi ', variables('name')) - substringString
Extract by index + length
substring('FlowLibs', 0, 4) - sliceString
Substring by start/end index (negatives OK)
slice('report.csv', -4) - replaceString
Replace all occurrences
replace('a-b-c', '-', '/') - toLowerString
Lowercase a string
toLower('HELLO') - splitString
String to array
split('a,b,c', ',') - indexOfString
Position of substring
indexOf('a@b.com', '@') - nthIndexOfString
Position of the nth match
nthIndexOf('a.b.c.d', '.', 2) - guidString
New globally-unique id
guid() - uriComponentString
URL-encode a value
uriComponent('a b&c') - base64String
Encode a string to base64
base64('Hello') - dataUriString
Build a data: URI
dataUri('Hello') - firstCollection
First item of an array
first(body('Get_items')?['value']) - lastCollection
Last item of an array
last(variables('arr')) - lengthCollection
String or array length
length(variables('arr')) - emptyCollection
Is empty / blank
empty(variables('arr')) - containsCollection
Membership test
contains(variables('arr'), 'x') - joinCollection
Array to delimited string
join(variables('arr'), ', ') - unionCollection
Merge + dedupe arrays
union(arr1, arr2) - chunkCollection
Split into equal-size groups
chunk(variables('arr'), 100) - reverseCollection
Reverse array order
reverse(variables('arr')) - sortCollection
Sort ascending
sort(variables('arr')) - ifLogical
Inline branch
if(greater(x, 10), 'high', 'low') - andLogical
Boolean AND
and(equals(a, 1), greater(b, 2)) - equalsLogical
Equality test
equals(status, 'Open') - coalesceLogical
First non-null value
coalesce(value, 'N/A') - greaterLogical
Greater-than compare
greater(ticks(a), ticks(b)) - intLogical
Cast string to integer
int('42') - decimalLogical
Exact decimal (avoids float error)
decimal('1.99') - utcNowDate
Current UTC timestamp
utcNow() - formatDateTimeDate
Format a timestamp
formatDateTime(utcNow(), 'yyyy-MM-dd') - addDaysDate
Shift a date by days
addDays(utcNow(), 7) - addToTimeDate
Shift by any unit
addToTime(utcNow(), 1, 'Month') - getPastTimeDate
Timestamp relative to now
getPastTime(7, 'Day') - convertTimeZoneDate
Change time zone
convertTimeZone(utcNow(), 'UTC', 'Eastern Standard Time', 'yyyy-MM-dd') - dayOfWeekDate
0 = Sun … 6 = Sat
dayOfWeek(utcNow()) - ticksDate
100-ns ticks (for date math)
ticks(utcNow()) - jsonJSON
Parse a JSON string
json(variables('rawString')) - addPropertyJSON
Add a key to an object
addProperty(variables('obj'), 'status', 'open') - setPropertyJSON
Overwrite a key
setProperty(variables('obj'), 'status', 'done') - xpathJSON
Query XML
first(xpath(xml(body('HTTP')), '//Id/text()')) - triggerBodyReference
The trigger's body output
triggerBody()?['email'] - triggerOutputsReference
The whole trigger output
triggerOutputs()?['headers'] - outputsReference
An action's raw output
outputs('Compose')?['data'] - bodyReference
An action's body only
body('Get_items')?['value'] - itemsReference
Current for-each item
items('Apply_to_each')?['Title'] - iterationIndexesReference
Current Until-loop index
iterationIndexes('Until') - actionsReference
Full action object (status, etc.)
actions('HTTP')?['status'] - resultReference
A scope's child inputs + outputs
result('Scope') - variablesReference
A variable's value
variables('counter') - parametersReference
A workflow/env parameter
parameters('apiBaseUrl') - workflowReference
Run + workflow metadata
workflow()?['run']?['name']
WDL is not Power Fx
Cloud-flow expressions use the Workflow Definition Language (shared with Azure Logic Apps), not Power Fx. Functions are prefix-style — add(1,2), not 1 + 2 — and reference runtime data with triggerBody(), body('Action'), outputs('Action'), variables('x'), and items('Apply_to_each'). The same reference exists for Azure Logic Apps and Power Automate.
Null-safe navigation
Use ?[ ] to read properties that may be missing: body('HTTP')?['data']?['id']. Plain ['id'] throws when a parent is null; ?[ ] returns null instead.
@ vs @{ }
A field that is *only* an expression starts with a leading @. To embed an expression inside literal text, wrap it: Order @{outputs('Compose')} shipped. In the modern editor you rarely type these — but they show up when you peek at the code view.
Reference functions — reaching runtime data
Before any other function, you need a handle on the data flowing through the run. These functions pull values out of the trigger, prior actions, loops, variables, and the run itself. Most are *shorthands* — body('X') is just outputs('X')?['body'], and triggerBody() is trigger().outputs.body.
| Function | Returns | When it applies |
|---|---|---|
| triggerBody() | The trigger's body output | The data that started the flow — usually what you reach for first. |
| triggerOutputs() | The whole trigger output | Headers, queries, and body: triggerOutputs()?['headers']?['x-id']. |
| outputs('Action') | An action's raw output object | Anything an action produced; drill in with ?['body']?['value']. |
| body('Action') | An action's body only | Shorthand for outputs('Action')?['body'] — the common case. |
| items('Loop') | Current item in a for-each | Only inside Apply to each; arg = the loop action's name. |
| iterationIndexes('Loop') | Current index in an Until loop | Only inside Until; gives the 0-based iteration counter. |
| actions('Action') | Full action object | status / startTime / code, not just the body. Great for error handling. |
| result('Scope') | Inputs + outputs of a scope's children | Aggregate or log every action inside a Scope / For each. |
| variables('name') | A variable's current value | Declared by an Initialize variable action. |
| parameters('name') | A workflow parameter value | Solution environment variables surface through here in Power Automate. |
| workflow() | Run + workflow metadata | Run id via workflow()?['run']?['name']; also flow name and tags. |
body() vs outputs()
Many connector actions nest their payload under body, so body('Get_items')?['value'] works but outputs('Get_items')?['value'] is usually null. When in doubt, run once and read the raw outputs in the run history before guessing the path.
String functions
| Function | What it does | Example |
|---|---|---|
| concat | Join strings/values | concat('Hi ', variables('name')) |
| substring | Extract by index + length | substring('FlowLibs', 0, 4) |
| slice | Substring by start/end index (end exclusive, negatives OK) | slice('FlowLibs', 0, 4) |
| replace | Replace all occurrences | replace('a-b-c', '-', '/') |
| toLower / toUpper | Change case | toLower('HELLO') |
| trim | Strip leading/trailing space | trim(' hi ') |
| split | String to array | split('a,b,c', ',') |
| indexOf / lastIndexOf | Position of substring | indexOf('a@b.com', '@') |
| nthIndexOf | Position of the nth match (negative n = from end) | nthIndexOf('a.b.c.d', '.', 2) |
| startsWith / endsWith | Prefix / suffix test | endsWith('x.pdf', '.pdf') |
| length | String or array length | length('abc') |
| guid | New unique id | guid() |
| formatNumber | Format a number as text | formatNumber(12.5, 'C2', 'en-US') |
slice vs substring
substring(text, start, length) needs a *length* and throws if it runs past the end. slice(text, start, end) takes an *end index*, tolerates overshooting, and accepts negative indexes — slice('report.csv', -4) returns .csv.
Encoding & conversion
| Function | What it does | Example |
|---|---|---|
| uriComponent | URL-encode (escape URL-unsafe chars) | uriComponent('a b&c') |
| uriComponentToString | Decode a URL-encoded string | uriComponentToString('a%20b%26c') |
| encodeUriComponent | Legacy URL-encode (prefer uriComponent) | encodeUriComponent('a b') |
| base64 | Encode a string to base64 | base64('Hello') |
| base64ToString | Decode base64 back to a string | base64ToString('SGVsbG8=') |
| dataUri | Build a data: URI (e.g. for inline attachments) | dataUri('Hello') |
| decimal | Exact decimal — avoids float rounding error | decimal('1.99') |
| int / float | Cast string to a number | int('42') |
| string | Cast any value to a string | string(123) |
| bool | Cast to boolean | bool('true') |
| json | Parse a JSON string into an object | json(variables('rawString')) |
| xml | Convert a string to XML (for xpath) | xml(body('HTTP')) |
Encoding a query string
Always uriComponent() values before pasting them into a URL: concat('https://api/search?q=', uriComponent(variables('term'))). Skipping this is the usual cause of mysterious 400s when a search term contains a space or &.
decimal for money
add(0.1, 0.2) is 0.30000000000000004 in floating point. For currency, wrap operands in decimal(): add(decimal('0.1'), decimal('0.2')) returns exactly 0.3.
Collection & array functions
| Function | What it does | Example |
|---|---|---|
| first / last | First / last item | first(body('Get_items')?['value']) |
| length | Item count | length(variables('arr')) |
| empty | Is empty / blank | empty(variables('arr')) |
| contains | Membership test | contains(variables('arr'), 'x') |
| join | Array to string | join(variables('arr'), ', ') |
| union / intersection | Set operations (union also dedupes) | union(arr1, arr2) |
| take / skip | Slice an array | take(variables('arr'), 5) |
| chunk | Split into equal-size groups | chunk(variables('arr'), 100) |
| reverse | Reverse array order | reverse(variables('arr')) |
| sort | Sort ascending (wrap in reverse for desc) | sort(variables('arr')) |
| createArray | Build an array | createArray('a', 'b', 'c') |
| range | Sequence of integers | range(1, 5) |
| item / items | Current loop item | items('Apply_to_each')?['Title'] |
chunk beats the 100-item batch limit
Connectors like Dataverse or SharePoint cap a batch at a fixed size. chunk(variables('rows'), 100) turns one big array into an array of ≤100-item arrays you can loop over and send one batch per iteration.
Logical & comparison
| Function | What it does | Example |
|---|---|---|
| if | Inline branch | if(greater(x, 10), 'high', 'low') |
| and / or / not | Boolean composition | and(equals(a, 1), greater(b, 2)) |
| equals | Equality test | equals(status, 'Open') |
| greater / less / greaterOrEquals | Compare values | greater(ticks(a), ticks(b)) |
| coalesce | First non-null value | coalesce(value, 'N/A') |
| int / float / string / bool | Type conversions | int('42') |
Date & time
| Function | What it does | Example |
|---|---|---|
| utcNow | Current UTC timestamp | utcNow() |
| formatDateTime | Format a timestamp | formatDateTime(utcNow(), 'yyyy-MM-dd') |
| addDays / addHours / addMinutes | Shift a date | addDays(utcNow(), 7) |
| addToTime / subtractFromTime | Shift by any unit | addToTime(utcNow(), 1, 'Month') |
| getFutureTime / getPastTime | Relative to now | getPastTime(7, 'Day') |
| convertTimeZone | Change time zone | convertTimeZone(utcNow(), 'UTC', 'Eastern Standard Time', 'yyyy-MM-dd') |
| dayOfWeek / dayOfMonth / dayOfYear | Day numbers | dayOfWeek(utcNow()) |
| ticks | 100-ns ticks for math | ticks(utcNow()) |
| startOfDay / startOfMonth | Truncate a date | startOfDay(utcNow()) |
| days between (pattern) | No dateDiff() exists | div(sub(ticks(b), ticks(a)), 864000000000) |
The format-string trap
In formatDateTime, MM is month and mm is minutes; dd is day. formatDateTime(d, 'mm/dd/yyyy') silently emits minutes, not month.
JSON & objects
json(variables('rawString')) // string -> object
outputs('Compose')?['data']?['id'] // null-safe read
addProperty(variables('obj'), 'status', 'open') // add a key
setProperty(variables('obj'), 'status', 'closed') // overwrite a key
removeProperty(variables('obj'), 'temp') // drop a key
xpath(xml(body('HTTP')), '//Order/Id/text()') // query XML{
"name": "@item()?['displayName']",
"email": "@item()?['mail']"
}XML & xpath deep-dive
When a connector hands back XML (legacy SOAP, RSS, some HTTP APIs), convert it with xml() first, then query with xpath(). xpath returns an array of matches, so wrap single-value reads in first(). Use /text() to get the inner value rather than the node.
// single value (note: xpath returns an array)
first(xpath(xml(body('HTTP')), '//Order/Id/text()'))
// all matching values as an array
xpath(xml(body('HTTP')), '//Order/LineItem/Sku/text()')
// an attribute
first(xpath(xml(body('HTTP')), 'string(//Order/@status)'))
// count of nodes
xpath(xml(body('HTTP')), 'count(//Order/LineItem)')Namespaces break naive paths
If the XML declares a default namespace, //Order matches nothing. Either use local-name() — //*[local-name()='Order'] — or register the namespace in the xpath query.
Array cookbook
Reshape, filter, and aggregate arrays without an Apply to each wherever you can — Select and Filter array run server-side and are far faster than a loop.
Reshape objects with a Select action
{
"id": "@item()?['ID']",
"title": "@item()?['Title']",
"owner": "@item()?['Author']?['DisplayName']"
}Filter array — advanced (query) syntax
@and(equals(item()?['Status'], 'Active'), greater(item()?['Amount'], 1000))Dedupe a flat array
union(variables('tags'), variables('tags'))Pull one column, then dedupe (group-by keys)
// Select (text/code) outputs an array of department strings:
@item()?['Department']
// then on that output:
union(body('Select_departments'), body('Select_departments'))Flatten an array of arrays
union(variables('listA'), variables('listB'))Build an Adaptive Card payload from rows
{
"type": "TextBlock",
"text": "@{item()?['Title']} — @{item()?['Status']}",
"wrap": true
}Date cookbook
Start and end of the current month
startOfMonth(utcNow()) // first day, 00:00
addDays(startOfMonth(addToTime(utcNow(), 1, 'Month')), -1) // last day of this monthBusiness days between two dates (approx)
sub(
div(sub(ticks(variables('end')), ticks(variables('start'))), 864000000000),
mul(2, div(div(sub(ticks(variables('end')), ticks(variables('start'))), 864000000000), 7))
)Age in whole years
div(sub(ticks(utcNow()), ticks(variables('dob'))), 315360000000000)Next occurrence of a weekday (e.g. next Monday)
addDays(startOfDay(utcNow()), add(mod(sub(8, dayOfWeek(utcNow())), 7), if(equals(dayOfWeek(utcNow()), 1), 7, 0)))Calendar quarter (1–4)
add(div(sub(int(formatDateTime(utcNow(), 'MM')), 1), 3), 1)Relative timestamps
getPastTime(24, 'Hour') // 24h ago, ISO 8601
getFutureTime(2, 'Week') // 2 weeks out
subtractFromTime(utcNow(), 30, 'Day')ISO week numbers
WDL has no native ISO-week function. If you need it, compute it in an Office Script or a small Compose chain, or store the ISO week from the source system — the day-of-year div approximations drift around year boundaries.
JSON cookbook
Safe deep access with a default
coalesce(body('HTTP')?['data']?['user']?['email'], 'unknown@contoso.com')Build a nested object inline
json(concat('{"id":', variables('id'), ',"name":"', variables('name'), '"}'))Merge two objects (shallow)
union(variables('defaults'), variables('overrides'))Add a property only when a condition holds
if(empty(variables('note')),
variables('payload'),
addProperty(variables('payload'), 'note', variables('note')))Escape a value for embedding in JSON text
replace(replace(variables('text'), '\\', '\\\\'), '"', '\\"')Prefer object functions over string-building
Concatenating JSON by hand breaks the moment a value contains a quote, newline, or unicode. Reach for addProperty / setProperty / json() first; only hand-build strings when the shape is trivial and the inputs are controlled.
No native regex in WDL
There is no regular-expression function in cloud-flow expressions. Most validation and extraction can still be done with the string primitives — and for anything heavier, push the work to a tool that does have regex.
- Contains / matches —
contains(),startsWith(),endsWith()cover prefix/suffix/substring checks. - Extract between markers — combine
indexOf()/nthIndexOf()/lastIndexOf()withsubstring()orslice(). - Tokenize —
split()on a delimiter, then index into the resulting array. - Real regex — call an Office Scripts action (Excel) or a small Azure Function, both of which run JavaScript/.NET regex.
- Fuzzy / semantic matching — AI Builder or a model action when the pattern isn't strictly lexical.
slice(
triggerBody()?['text'],
add(indexOf(triggerBody()?['text'], '('), 1),
indexOf(triggerBody()?['text'], ')')
)Common patterns
Real expressions that nest several functions — paste into the expression editor and swap the references for your own.
Days between two dates
div(sub(ticks(variables('endDate')), ticks(variables('startDate'))), 864000000000)Now, formatted in local time
formatDateTime(convertTimeZone(utcNow(), 'UTC', 'Eastern Standard Time'), 'yyyy-MM-dd HH:mm')Safe nested read with a fallback
coalesce(body('HTTP')?['data']?['email'], 'no-email@contoso.com')First name from a full name
first(split(triggerBody()?['name'], ' '))Timestamped file name
concat('export-', formatDateTime(utcNow(), 'yyyyMMdd-HHmmss'), '.csv')Inline if / else (ternary)
if(greater(int(triggerBody()?['amount']), 1000), 'High', 'Standard')Percentage complete as text
concat(formatNumber(div(mul(float(variables('done')), 100), float(variables('total'))), 'F1'), '%')Weekend check
or(equals(dayOfWeek(utcNow()), 0), equals(dayOfWeek(utcNow()), 6))Normalize then compare (trim + lowercase)
equals(toLower(trim(triggerBody()?['email'])), 'admin@contoso.com')Empty-safe join of an array
if(empty(variables('tags')), 'none', join(variables('tags'), ', '))Domain from an email address
last(split(triggerBody()?['email'], '@'))File extension (lowercased)
toLower(last(split(triggerBody()?['fileName'], '.')))Pad a number to 4 digits
slice(concat('0000', string(variables('seq'))), -4)Title-case the first letter of a word
concat(toUpper(substring(variables('w'), 0, 1)), toLower(substring(variables('w'), 1)))Strip the query string from a URL
if(contains(variables('url'), '?'), first(split(variables('url'), '?')), variables('url'))Pluralize a label by count
concat(string(variables('n')), ' item', if(equals(variables('n'), 1), '', 's'))Build a mailto link
concat('mailto:', variables('to'), '?subject=', uriComponent(variables('subject')))Minutes as h:mm
concat(string(div(variables('mins'), 60)), ':', slice(concat('0', string(mod(variables('mins'), 60))), -2))Clamp a number into a range
max(0, min(100, variables('score')))Common gotchas
- No `dateDiff()` — subtract
ticks()and divide (days =864000000000, hours =36000000000, minutes =600000000). - `dayOfWeek` Sunday is `0`, not `7` — weekend test:
or(equals(dayOfWeek(d),0), equals(dayOfWeek(d),6)). - No regex — use
indexOf/split/slice, or offload to Office Scripts / AI Builder. - `body()` is not `outputs()` — most connector payloads live under
body;outputs(...)?['body']is the long form ofbody(...). - `xpath` returns an array — wrap single reads in
first(), and watch for default XML namespaces breaking plain//Nodepaths. - Float math drifts — use
decimal()for currency;div()on two integers truncates, so cast tofloat()first. - Strings vs interpolation — in a text field use
@{expression}; a field that is *only* an expression uses a leading@. - Parse JSON nulls — make optional properties nullable in the schema or the action fails on a missing field.
- Apply to each is slow — enable concurrency (Settings) for independent iterations, or replace the loop with
Select/Filter arraywhere possible.