Skip to main content

Expressions

FlowForger uses the Power Automate expression language. Every function below can be written either as a raw expression string (@concat(...)) or as a ctx method call in the TypeScript DSL (ctx.concat(...)) — the transformer compiles ctx.<name>(...) 1:1 into the matching @<name>(...) expression. Functions without an explicit convenience wrapper still work through the same generic ctx.<name>(args) mapping.

The functions behave as they do in the cloud, not as their JavaScript namesakes: see Cloud Semantics for text conversion, comparisons, integer division and case rules.

Syntax​

Expressions are prefixed with @ and use function-call syntax:

@concat('Hello, ', variables('name'))

Wrapping an expression in @{...} interpolates it into a surrounding string. Values become text the way the cloud writes them: true as True, null as an empty string, numbers as .NET writes them (7.0 as 7), and objects and arrays as JSON:

'Hello @{variables('name')}, your id is @{body('GetUser')?['id']}'

@{...} works anywhere in a string. @@ escapes a literal @. Strings inside an expression use single quotes; a double-quoted string is rejected by Power Automate on save (DSL033).

From TypeScript to Expressions​

You rarely need to write expression strings. The transformer compiles TypeScript operators and ctx calls into expressions:

TypeScriptExpression
ctx.triggerBody()?.['order']?.['id']triggerBody()?['order']?['id'] (null-safe)
ctx.triggerBody().order.idtriggerBody().order.id (fails when missing)
a === b / a !== bequals(a, b) / not(equals(a, b))
a > b, a >= b, a < b, a <= bgreater, greaterOrEquals, less, lessOrEquals
a && b / a || b / !aand(a, b) / or(a, b) / not(a)
a ?? bcoalesce(a, b)
a ?? []coalesce(a, json('[]'))
a + badd(a, b), or concat(a, b) when one side is a string literal
a - b, a * b, a / b, a % bsub, mul, div, mod
x.lengthlength(x)
`Order ${id} received`Order @{id} received (text)
cond ? a : bif(cond, a, b)

Things to keep in mind:

  • / compiles to div(), which is integer division for two integers: 7 / 2 is 3. Use ctx.float() or a decimal literal (7.0) for a fractional result.
  • || is a logical or() and needs booleans. For a fallback value, use ??.
  • An empty array literal in an expression compiles to json('[]'), because Power Automate's createArray() needs at least one argument.
  • A template literal produces text. Where Power Automate expects a single expression, such as a Select map that should produce a string, use ctx.concat(...) instead (SELECT_MAP_TEXT).
  • ?.['x'] is the valid TypeScript for Power Automate's null-safe ?['x']. Without the ?, a missing property fails the action.

Reference Functions​

Access trigger/action data, variables, and loop state:

  • body(actionName) — shorthand for outputs(actionName)?['body']
  • outputs(actionName) — full output object (statusCode, headers, body, ...)
  • actions(actionName) — the action's record: { name, inputs, outputs, startTime, endTime, trackingId, clientTrackingId, code, status, error }
  • action() — the current action's record. The cloud allows it only in tracked properties; a local run fails on it everywhere else
  • actionBody(actionName) / actionOutputs(actionName) — aliases for body() / outputs()
  • result(scopedActionName) — the records of the actions inside a scope or loop, in the same shape as actions(). For a foreach, each child appears once, with every iteration's record in its outputs; for an Until, the last iteration's records, with a repetitionCount
  • triggerBody() / triggerOutputs() / trigger() — the trigger's body, its outputs ({ headers, body }), and its run record (which has no body of its own)
  • variables(name)
  • item() — current item in a foreach loop, Select, Filter array or table column
  • items(loopName) — current item of a named foreach loop
  • iterationIndexes(loopName) — index of an enclosing named loop
  • parameters(name) — flow parameter value
  • workflow() — workflow metadata (name, id, run, tags)
  • listCallbackUrl() — the trigger's invocation URL

All reference functions support property path navigation — see Property Path Navigation. An expression may only reference actions on its own run-after path (DSL048).

String Functions​

  • concat(), substring(), replace(), toLower(), toUpper(), trim()
  • split(), join(), indexOf(), lastIndexOf(), nthIndexOf(text, searchText, occurrence)
  • guid(), base64(), base64ToString(), decodeBase64() (alias for base64ToString())
  • uriComponent(), uriComponentToString(), encodeUriComponent(), decodeUriComponent() (aliases for uriComponent()/uriComponentToString())
  • slice(text, start, end?) — strings only

indexOf(), lastIndexOf(), nthIndexOf(), startsWith() and endsWith() ignore case; contains(), replace() and split() do not.

Collection Functions​

  • createArray() (at least one argument), range(), length(), empty()
  • first(), last(), skip(), take()
  • union(), intersection(), contains() (strings, arrays, and keys of objects)
  • chunk(value, size) — splits an array or string into pieces of at most size
  • sort(array, propertyName?) — ascending; sorts by propertyName when given, otherwise by value
  • reverse(array) — arrays only

Math Functions​

  • add(), sub(), mul(), div(), mod()
  • min(), max(), rand()
  • int(), float() (there is no abs(), ceil(), floor() or round() in Power Automate, and a local run fails on them too; formatNumber(x, 'F0') rounds)
  • formatNumber(number, format, locale?) — formats using a .NET numeric format string

div() of two integers is integer division (div(7, 2) is 3); a float literal or float() gives a float result (div(7.0, 2) is 3.5).

formatNumber supports the standard .NET specifiers (C, N, F, D, P, E, G, X) plus custom patterns of 0/#/./,:

ctx.formatNumber(1234.5, 'C2')      // "$1,234.50" (currency, 2 decimals)
ctx.formatNumber(0.256, 'P2') // "25.60%" (percentage, 2 decimals)
ctx.formatNumber(1234.5, 'N0') // "1,235" (grouped, 0 decimals)
ctx.formatNumber(1234.5, '0.00', 'de-DE') // "1234,50" (custom pattern, German locale)

Comparison and Logical​

  • equals(), greater(), less(), greaterOrEquals(), lessOrEquals()
  • and(), or(), not()
  • startsWith(), endsWith() — string predicates

equals() is type-strict (equals(1, '1') is false) and compares arrays and objects deeply. greater() and friends compare strings ignoring case. and(), or() and not() need booleans. There are no ge/le shorthands.

Conditional​

  • if(), coalesce()

Type Conversion and Inspection​

  • json(), string(), array(), bool(), decimal()
  • isFloat(), isInt() — validate whether a string represents that numeric type

Object Manipulation​

  • addProperty(object, name, value) — throws if name already exists
  • setProperty(object, name, value) — adds or overwrites
  • removeProperty(object, name)

Date and Time Functions​

  • utcNow(format?)
  • addDays(timestamp, days, format?), addHours(timestamp, hours, format?), addMinutes(timestamp, minutes, format?), addSeconds(timestamp, seconds, format?)
  • addToTime(timestamp, interval, unit, format?), subtractFromTime(timestamp, interval, unit, format?) — unit is 'Second' | 'Minute' | 'Hour' | 'Day' | 'Week' | 'Month' | 'Year'
  • getFutureTime(interval, unit, format?), getPastTime(interval, unit, format?) — relative to now
  • formatDateTime(timestamp, format?, locale?)
  • parseDateTime(timestamp, locale?, format?)
  • dateDifference(startTimestamp, endTimestamp) — returns a D.HH:mm:ss-style duration string
  • dayOfMonth(timestamp), dayOfWeek(timestamp) (0 = Sunday), dayOfYear(timestamp)
  • startOfDay(timestamp, format?), startOfHour(timestamp, format?), startOfMonth(timestamp, format?)
  • ticks(timestamp) — 100-nanosecond intervals since 0001-01-01
  • convertFromUtc(timestamp, timezone, format?), convertToUtc(timestamp, sourceTimezone, format?), convertTimeZone(timestamp, sourceTimezone, targetTimezone, format?) — accepts both Windows ('Pacific Standard Time') and IANA ('America/Los_Angeles') zone names

Timestamps keep seven fraction digits and their kind (Z, no zone, or an offset), and format strings are .NET's.

URI Functions​

Parse components out of a URI string:

  • uriHost(), uriScheme(), uriPort()
  • uriPath(), uriPathAndQuery(), uriQuery()

XML Functions​

  • xml(value) — parses a string into canonical XML
  • xpath(xml, expression) — evaluates an XPath expression; returns an array of nodes for node-set queries, or a primitive for count()/string()/sum()-style queries

Binary and Data URI Functions​

  • base64ToBinary(), binary(), dataUri(), dataUriToString(), dataUriToBinary(), decodeDataUri(), uriComponentToBinary()

Form Data Functions​

For actions/triggers that receive multipart/form-data or application/x-www-form-urlencoded content:

  • formDataValue(actionName, key) / formDataMultiValues(actionName, key)
  • triggerFormDataValue(key) / triggerFormDataMultiValues(key)
  • multipartBody(actionName, index) / triggerMultipartBody(index)

They read the content type from the outputs' Content-Type header. To test them locally, pass the raw body with flowforger run --in <file> --in-content-type <type>.

Property Path Navigation​

Functions support dot notation and optional-chaining bracket notation for nested access:

@body('GetData').user.name
@body('GetData')?['user']?['name']

Property names match case-insensitively. The ? form gives null for a missing property; the other forms fail the action.

Runtime Expression Evaluation​

Use ctx.eval<T>(expression) to drop down to a raw Power Automate expression string when there's no TypeScript equivalent (or the transformer can't express what you need):

await ctx.compose('Next_count', ctx.eval<number>("@add(variables('count'), 1)"));

The editor checks the expression in a ctx.eval() string literal: syntax (DSL033), unknown functions (DSL034), reference functions without a name (DSL049), and argument counts (DSL050). See Diagnostics.

ctx.parameters<T>(name), ctx.trigger<T>(), and ctx.workflow<T>() accept a generic type parameter for typed access to flow parameters, trigger info, and workflow metadata.

OData Query Builder​

ctx.odata builds $filter query strings for connector operations that accept OData filters (e.g. ctx.connectors.dataverse.ListRecords). It supports two syntaxes.

Builder methods​

Compose filters from ctx.odata.* functions — verbose, but fully type-checked:

await ctx.connectors.dataverse.ListRecords('GetRecords', {
entityName: 'accounts',
'$filter': ctx.odata.and(
ctx.odata.eq('statecode', 0),
ctx.odata.eq('statuscode', ctx.parameters('StatusValue')),
ctx.odata.gt('revenue', ctx.parameters('MinAmount'))
),
'$top': 10,
});
// $filter -> "statecode eq 0 and statuscode eq @{parameters('StatusValue')} and revenue gt @{parameters('MinAmount')}"

Available methods:

  • Comparison: eq(field, value), ne(field, value), gt(field, value), ge(field, value), lt(field, value), le(field, value)
  • Logical: and(...expressions), or(...expressions), not(expression)
  • String: contains(field, value), startsWith(field, value), endsWith(field, value)
  • Null checks: isNull(field), isNotNull(field)
  • Escape hatch: raw(expression) — pass through a literal OData string unchanged

These ge/le are OData operators for $filter, not expression functions.

Tagged template​

Write familiar TypeScript-style operators inside ctx.odata backticks; they compile to the equivalent OData syntax:

'$filter': ctx.odata`statecode == 0 && statuscode != null`
// -> "statecode eq 0 and statuscode ne null"

'$filter': ctx.odata`statecode == 0 && statuscode == ${ctx.parameters('StatusValue')} && revenue > ${ctx.parameters('MinAmount')}`
// -> "statecode eq 0 and statuscode eq @{parameters('StatusValue')} and revenue gt @{parameters('MinAmount')}"

'$filter': ctx.odata`(statecode == 0 || statecode == 1) && (revenue >= ${ctx.parameters('MinAmount')} || customertype == 3)`
// -> "(statecode eq 0 or statecode eq 1) and (revenue ge @{parameters('MinAmount')} or customertype eq 3)"

'$filter': ctx.odata`statecode == 0 && !(statuscode == 2)`
// -> "statecode eq 0 and not (statuscode eq 2)"

Supported operators: ==, !=, <, >, <=, >=, &&, ||, !, and parentheses for grouping. Interpolated ${...} values may be parameters, action outputs, variables, or literals:

TypeScriptOData
== / !=eq / ne
> / >=gt / ge
< / <=lt / le
&& / ||and / or
!(...)not (...)
${expr}interpolated value

Chaining a filter off a previous action's output​

await ctx.connectors.dataverse.ListRecords('GetActiveAccounts', {
entityName: 'accounts',
'$filter': ctx.odata`statecode == 0`,
'$top': 1,
});

await ctx.connectors.dataverse.ListRecords('GetRelatedContacts', {
entityName: 'contacts',
'$filter': ctx.odata`_parentcustomerid_value == ${ctx.body('GetActiveAccounts')?.['value']?.[0]?.['accountid']}`,
'$top': 10,
});

A complete flow demonstrating both syntaxes is in the odata-tagged-template example; see Examples.