Skip to main content

Variables

FlowForger variables are written as native TypeScript let (or var) declarations — there is no ctx.initVariable(), ctx.setVariable(), or ctx.appendToArrayVariable() API. The transformer's VariableTracker watches the action method body for declarations, assignments, mutations, and .push() calls on tracked variables, and turns them into the matching Power Automate variable actions.

let and const​

Only let/var become Power Automate variables (InitializeVariable actions). A const never does:

  • const name = ctx.triggerBody()?.['name'] is an alias: every read of name is replaced by the expression. Use it for a shorter name, not to store a value.
  • const x = await ctx.http(...) (or any other action call) does not capture the result (DSL032). Call the action as a statement and read it with ctx.body('Name') or ctx.outputs('Name').
  • const n = 5 or const o = { ... } compiles to broken text such as @5 (DSL025). Use let.

Declaring a Variable​

A let declaration becomes an InitializeVariable action. The action name defaults to Initialize_<variableName> (override with @action in a JSDoc comment):

@Action()
async run(ctx: FlowContext) {
/** @action Initialize_counter */
let counter: number = 0;

let name: string = 'default';

let items: any[] = [];
}
  • Declare variables at the top level of the @Action method. Power Automate only allows Initialize variable outside every scope, condition and loop (DSL014, VAR_INIT_NESTED); declare at the top and assign inside the block.
  • Each variable name may only be declared once per flow (DSL030). Names are compared case-insensitively by Power Automate (VAR_INIT_DUPLICATE).
  • A flow can have at most 250 variables (DSL042).

Type Inference​

The Power Automate variable type is inferred, in order of priority:

  1. An explicit @varType JSDoc override, which always wins
  2. The TypeScript type annotation, if present (let x: number = 0)
  3. The initializer's literal kind, if there's no annotation (let x = 0)
Written asInferred PA type
string annotation, or a string/template literal initializerstring
number annotation (whatever the value)integer
a whole-number literal (0, 5) with no annotationinteger
a decimal-point numeric literal (0.0, 3.14) with no annotationfloat
boolean annotation, or true/false initializerboolean
T[] / Array<T> annotation, or an array literal initializerarray
object, Record<K, V> annotation, or an object literal initializerobject
any annotationfalls through to the initializer's literal kind
an expression initializer (ctx.triggerBody()?.['total']) with no annotationobject
no initializer and no annotationstring (with a default empty-string value)

Two cases catch people out:

  • A number annotation always gives integer, even with let rate: number = 0.5. For a float, leave the annotation out and write a decimal literal (let rate = 0.5), or add @varType float.
  • A variable initialized from an expression without an annotation becomes object. Annotate it: let total: number = ctx.triggerBody()?.['total'].
/** @varType float */
let rate: number = 0;

Valid @varType values are string, integer, float, boolean, array, object (case-insensitive). The IR and the compiled Logic Apps JSON both store the type in lowercase ("type": "integer").

Setting a Variable​

A plain assignment to a tracked variable becomes a SetVariable action (Set_<name>, then Set_<name>_2, ...):

counter = 5;

Assigning an object/array/expression works the same way — the right-hand side is transformed to a Power Automate expression or literal as needed:

status = ctx.body('Get_order')?.['status'] ?? 'Unknown';

To add to an array, use .push() (below). Rebuilding the array from itself (items = [...items, value] or items.concat(value)) compiles to a Set variable, not an append (DSL029).

Incrementing / Decrementing​

For integer/float variables, ++, --, +=, -=, and the self-referencing pattern x = x + 1 all compile to IncrementVariable/DecrementVariable actions (not a generic SetVariable):

retryCount++;        // IncrementVariable, value: 1
retryCount += 1; // IncrementVariable, value: 1
retryCount = retryCount + 1; // IncrementVariable, value: 1
retryCount -= 1; // DecrementVariable, value: 1

The amount may be an expression: total += ctx.body('Get_line')?.['amount'] adds that number.

x++ and x-- ignore an @action name. Write x += 1 when you want to name the step.

For string variables, += compiles to AppendToStringVariable instead:

let log: string = '';
log += 'next step\n'; // AppendToStringVariable

Appending to an Array​

Calling .push(...) on a tracked array variable compiles to AppendToArrayVariable (Append_to_<name>):

let items: any[] = [];

items.push({ id: 1, name: 'Widget' });

The value must be a single item — a string, number, boolean or object. Appending an array fails the action when it runs; push the items one by one.

Variables in Loops​

Power Automate runs the iterations of a for...of loop in parallel (up to 20 at a time), while a local run of a loop without a concurrency setting takes them one at a time. Setting or appending to a variable inside such a loop can therefore give a different result in the cloud, and the editor warns with DSL047. Increments and decrements are safe. Either run the loop one item at a time:

/** @action Collect_titles @runtimeConfig {"concurrency":{"repetitions":1}} */
for (const row of ctx.body('Get_rows')?.['value'] ?? []) {
titles.push(row?.['title']);
}

or build the result without a variable:

await ctx.select('Titles', ctx.body('Get_rows')?.['value'], ctx.item()?.['title']);

Accessing Variables​

Use ctx.variables('name') inside the DSL — it's transformed to the Power Automate variables('name') expression function wherever it appears (conditions, compose inputs, connector params, etc.):

await ctx.compose('Summary', {
total: ctx.variables('counter'),
label: ctx.variables('name'),
});

In a raw Power Automate expression string (e.g. inside ctx.eval() or hand-written JSON), reference it the same way Power Automate always has:

@variables('counter')

Naming and Overrides​

JSDoc comments directly above the let/assignment let you override generated names and other metadata, mirroring the annotations available on other actions (see Annotations):

  • @action Name — override the generated action name
  • @originalName "Original Name" — preserve a variable name containing spaces/special characters distinct from the sanitized TypeScript identifier
  • @varType type — force the PA variable type (see above)
  • @runAfter ... — explicit run-after dependencies instead of the default sequential chain