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 ofnameis 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 withctx.body('Name')orctx.outputs('Name').const n = 5orconst o = { ... }compiles to broken text such as@5(DSL025). Uselet.
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
@Actionmethod. 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:
- An explicit
@varTypeJSDoc override, which always wins - The TypeScript type annotation, if present (
let x: number = 0) - The initializer's literal kind, if there's no annotation (
let x = 0)
| Written as | Inferred PA type |
|---|---|
string annotation, or a string/template literal initializer | string |
number annotation (whatever the value) | integer |
a whole-number literal (0, 5) with no annotation | integer |
a decimal-point numeric literal (0.0, 3.14) with no annotation | float |
boolean annotation, or true/false initializer | boolean |
T[] / Array<T> annotation, or an array literal initializer | array |
object, Record<K, V> annotation, or an object literal initializer | object |
any annotation | falls through to the initializer's literal kind |
an expression initializer (ctx.triggerBody()?.['total']) with no annotation | object |
| no initializer and no annotation | string (with a default empty-string value) |
Two cases catch people out:
- A
numberannotation always givesinteger, even withlet 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