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:
| TypeScript | Expression |
|---|---|
ctx.triggerBody()?.['order']?.['id'] | triggerBody()?['order']?['id'] (null-safe) |
ctx.triggerBody().order.id | triggerBody().order.id (fails when missing) |
a === b / a !== b | equals(a, b) / not(equals(a, b)) |
a > b, a >= b, a < b, a <= b | greater, greaterOrEquals, less, lessOrEquals |
a && b / a || b / !a | and(a, b) / or(a, b) / not(a) |
a ?? b | coalesce(a, b) |
a ?? [] | coalesce(a, json('[]')) |
a + b | add(a, b), or concat(a, b) when one side is a string literal |
a - b, a * b, a / b, a % b | sub, mul, div, mod |
x.length | length(x) |
`Order ${id} received` | Order @{id} received (text) |
cond ? a : b | if(cond, a, b) |
Things to keep in mind:
/compiles todiv(), which is integer division for two integers:7 / 2is3. Usectx.float()or a decimal literal (7.0) for a fractional result.||is a logicalor()and needs booleans. For a fallback value, use??.- An empty array literal in an expression compiles to
json('[]'), because Power Automate'screateArray()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 foroutputs(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 elseactionBody(actionName)/actionOutputs(actionName)— aliases forbody()/outputs()result(scopedActionName)— the records of the actions inside a scope or loop, in the same shape asactions(). For a foreach, each child appears once, with every iteration's record in itsoutputs; for an Until, the last iteration's records, with arepetitionCounttriggerBody()/triggerOutputs()/trigger()— the trigger's body, its outputs ({ headers, body }), and its run record (which has nobodyof its own)variables(name)item()— current item in aforeachloop, Select, Filter array or table columnitems(loopName)— current item of a namedforeachloopiterationIndexes(loopName)— index of an enclosing named loopparameters(name)— flow parameter valueworkflow()— 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 forbase64ToString())uriComponent(),uriComponentToString(),encodeUriComponent(),decodeUriComponent()(aliases foruriComponent()/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 mostsizesort(array, propertyName?)— ascending; sorts bypropertyNamewhen given, otherwise by valuereverse(array)— arrays only
Math Functions
add(),sub(),mul(),div(),mod()min(),max(),rand()int(),float()(there is noabs(),ceil(),floor()orround()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 ifnamealready existssetProperty(object, name, value)— adds or overwritesremoveProperty(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?)—unitis'Second' | 'Minute' | 'Hour' | 'Day' | 'Week' | 'Month' | 'Year'getFutureTime(interval, unit, format?),getPastTime(interval, unit, format?)— relative to nowformatDateTime(timestamp, format?, locale?)parseDateTime(timestamp, locale?, format?)dateDifference(startTimestamp, endTimestamp)— returns aD.HH:mm:ss-style duration stringdayOfMonth(timestamp),dayOfWeek(timestamp)(0 = Sunday),dayOfYear(timestamp)startOfDay(timestamp, format?),startOfHour(timestamp, format?),startOfMonth(timestamp, format?)ticks(timestamp)— 100-nanosecond intervals since0001-01-01convertFromUtc(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 XMLxpath(xml, expression)— evaluates an XPath expression; returns an array of nodes for node-set queries, or a primitive forcount()/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:
| TypeScript | OData |
|---|---|
== / != | 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.