Annotations (JSDoc Tags)
Most of a flow is plain TypeScript, but some Power Automate settings have no TypeScript syntax: an action's run-after conditions, a loop's concurrency, a retry policy, a tracked property. You set these with JSDoc tags in a /** ... */ comment directly above the statement.
/** @action Get_open_orders @runAfter Initialize_total: Succeeded @retryPolicy {"type":"fixed","count":3,"interval":"PT10S"} */
await ctx.http('Get_open_orders', { method: 'GET', url: 'https://api.contoso.com/orders' });
Several tags can share one comment, on one line or on several. JSON values must be valid JSON (double-quoted keys, no trailing commas); the editor reports invalid JSON as DSL027 and values out of range as DSL043 (see Diagnostics).
Where each tag applies
| Tag | ctx action calls | if / loops / switch / scope | let and variable updates | Trigger method |
|---|---|---|---|---|
@action | – (the name is the first argument) | ✓ | ✓ | – |
@type | – | ✓ (needed only for scope) | – | – |
@runAfter | ✓ | ✓ | ✓ | – |
@runtimeConfig | ✓ | ✓ | ✓ | – |
@limit | ✓ (timeout) | loops (count, timeout) | – | – |
@retryPolicy | ✓ | – | – | – |
@operationOptions | ✓ | – | – | – |
@varType | – | – | let only | – |
@originalName | – | – | let only | – |
@description | ✓ | ✓ | ✓ | ✓ |
@metadata | ✓ | ✓ | ✓ | connector triggers |
@trackedProperties | ✓ | ✓ | ✓ | – |
@conditionFormat | – | if only | – | – |
@trigger | – | – | – | recurrence and connector triggers |
@action
Names the action that a statement compiles to. Use it on control-flow statements, variable declarations and variable updates, which have no name argument. Action calls (ctx.http('Name', ...), ctx.compose('Name', ...), connector calls) are named by their first argument, so @action above them has no effect.
/** @action Initialize_retry_count */
let retryCount: number = 0;
/** @action Check_order_total */
if (ctx.triggerBody()?.['total'] > 1000) {
await ctx.compose('Large_order', true);
}
/** @action Add_line_to_report */
lines.push(ctx.item()?.['title']);
The name is the rest of the line, up to the next @tag or the end of the comment, and may contain spaces (@action Case 2). Anything you write after the name on the same line becomes part of the name, so put notes on their own line. Power Automate rejects names with @{, control characters or any of < > % & \ ? / (DSL035), and names longer than 80 characters (DSL039).
x++ and x-- ignore @action. Write x += 1 or x -= 1 when you want to name the step.
A switch case is named with @action in a comment directly above the case:
/** @action Route_by_region */
switch (ctx.triggerBody()?.['region']) {
/** @action Case_US */
case 'US':
await ctx.compose('Tax_rate_US', 0.08);
break;
}
@type
Marks a bare block { ... } as a Power Automate Scope. Without it, a bare block is just a TypeScript block: its statements are flattened into the surrounding list.
/** @action Try @type scope */
{
await ctx.http('Call_api', { method: 'GET', url: 'https://api.contoso.com/status' });
}
The other accepted values (if, foreach, switch, until, dountil, case) are recognized for compatibility, but if, for...of, switch, while and do...while are detected from the statement itself and never need a tag. Any other value is reported as DSL024.
@runAfter
Sets the run-after conditions of the action, replacing the default (run after the previous statement, when it succeeded).
/** @action Catch @type scope @runAfter Try: Failed, TimedOut */
{
await ctx.compose('Error_details', ctx.result('Try'));
}
-
Format:
@runAfter <ActionName>: <Status>, <Status>. Statuses areSucceeded,Failed,SkippedandTimedOut(DSL021for anything else). The target must exist (DSL022) and be a sibling in the same block. -
The statuses in one tag are alternatives (any of them will do).
-
Several
@runAftertags are ANDed: the action runs only when every target finished in one of its listed statuses./** @runAfter Notify_team: Succeeded @runAfter Update_record: Succeeded */
await ctx.compose('Both_done', true); -
An action name with a colon is quoted:
@runAfter "Step: one": Succeeded. -
@runAfter trigger(at the top level) and@runAfter first(inside a block) give the action an empty run-after, so it starts at the beginning of its container, in parallel with the other branch. The next statement without a tag still runs after this one./** @runAfter trigger */
await ctx.http('Branch_A', { method: 'GET', url: 'https://api.contoso.com/a' });
/** @runAfter trigger */
await ctx.http('Branch_B', { method: 'GET', url: 'https://api.contoso.com/b' });
/** @runAfter Branch_A: Succeeded @runAfter Branch_B: Succeeded */
await ctx.compose('Join', true);
Because the entries are ANDed, it is easy to write a condition that can never hold. The validator reports those as RUNAFTER_DEAD_STATUS and RUNAFTER_UNSATISFIABLE, and an action that reads an action not on its run-after path as DSL048. See Control Flow for the try/catch/finally pattern.
@runtimeConfig
Sets the action's runtimeConfiguration object as JSON. Common uses:
/** @action For_each_order @runtimeConfig {"concurrency":{"repetitions":1}} */
for (const order of ctx.body('Get_orders')?.['value'] ?? []) {
total += 1;
}
/** @runtimeConfig {"paginationPolicy":{"minimumItemCount":2000}} */
await ctx.connectors.sharepoint.GetItems('Get_all_tasks', {
dataset: ctx.parameters('SiteUrl'),
table: ctx.parameters('TasksListId'),
});
/** @runtimeConfig {"secureData":{"properties":["inputs","outputs"]}} */
await ctx.http('Get_token', { method: 'POST', url: 'https://login.contoso.com/token' });
concurrency.repetitionson afor...ofloop: how many iterations run at once in the cloud (1–50). Without it, Power Automate runs up to 20 at a time, while a local run takes one item at a time, so a loop that changes a variable should setrepetitionsto 1 (DSL047).paginationPolicy.minimumItemCounton a list operation: follow paging links until at least that many items are read. Above 5,000 needs a premium plan (PAGINATION_THRESHOLD).
There is no @parallel tag; parallelism is set through @runtimeConfig.
@limit
On a while / do...while loop (an Until in Power Automate), sets the iteration limit and timeout. A number is the count; an object sets either or both. The default is 60 iterations and one hour.
/** @action Poll_status @limit {"count": 20, "timeout": "PT30M"} */
do {
await ctx.delay('Wait', 1, 'Minute');
} while (ctx.variables('status') !== 'Done');
On an action call, @limit {"timeout":"PT2M"} sets the action's timeout (an ISO 8601 duration).
@retryPolicy
Sets the retry policy of an HTTP or connector action as JSON. type is none, fixed or exponential; count is 1–90; interval, minimumInterval and maximumInterval are ISO 8601 durations between PT5S and P1D.
/** @retryPolicy {"type":"exponential","count":4,"interval":"PT10S"} */
await ctx.http('Call_api', { method: 'GET', url: 'https://api.contoso.com/data' });
/** @retryPolicy {"type":"none"} */
await ctx.http('Call_once', { method: 'POST', url: 'https://api.contoso.com/submit' });
Without a policy, the cloud's default applies (see Cloud Semantics).
@operationOptions
Sets the action's operationOptions string. It is written as a JSON string, in double quotes.
/** @operationOptions "DisableAsyncPattern" */
await ctx.http('Start_export', { method: 'POST', url: 'https://api.contoso.com/export' });
DisableAsyncPattern stops an HTTP action from polling the Location of a 202 Accepted answer.
@varType
Forces the Power Automate type of a let variable when the inferred type is not the one you want. Values: string, integer, float, boolean, array, object (case-insensitive).
/** @varType float */
let rate: number = 1;
A number annotation otherwise always gives integer. See Variables.
@originalName
Gives a variable a Power Automate name that is not a valid TypeScript identifier. The TypeScript name is what you use in code; the original name is what the flow uses.
/** @originalName "Email Column" */
let emailColumn: string = 'mail';
await ctx.compose('Column', ctx.variables('Email Column'));
@description
Sets the action's description (the note shown on the card in the designer). Usually you don't need the tag: a // comment directly above a statement becomes its description.
// Read the open orders for this customer
await ctx.http('Get_orders', { method: 'GET', url: 'https://api.contoso.com/orders' });
/** @description Read the open orders for this customer */
await ctx.http('Get_orders_2', { method: 'GET', url: 'https://api.contoso.com/orders' });
Free text in a /** ... */ comment is not a description; use a // comment or the tag. The text after @description runs until the next tag or the end of the comment.
Power Automate limits action descriptions to 256 characters. Longer ones are emitted as a 256-character excerpt, with the full text kept in the action's metadata (flowforgerDescription), so converting the flow back to the DSL restores it. If someone edits the note in the designer, the edited text wins.
The flow's own description is the JSDoc comment above the class (or description in @Flow({ ... }), which takes precedence). It has no length limit.
@metadata
Sets the action's metadata object as JSON. Generated DSL uses it to keep values such as operationMetadataId when a flow is converted back and forth; you rarely write it by hand.
/** @metadata {"operationMetadataId":"8f2d6b9e-0f3a-4c1e-9d5b-2a7c4e6f1b3d"} */
await ctx.compose('Summary', ctx.variables('total'));
On a @ConnectorTrigger method it sets the trigger's metadata.
@trackedProperties
Sets the action's tracked properties as a JSON object of name to expression.
/** @trackedProperties {"orderId":"@triggerBody()?['orderId']"} */
await ctx.http('Submit_order', { method: 'POST', url: 'https://api.contoso.com/orders', body: ctx.triggerBody() });
@conditionFormat
Chooses how an if condition is written in Logic Apps JSON: object ({"equals": [...]}, the designer's format) or string ("@equals(...)"). By default the emitter writes the object form wrapped in an and, the way the designer does. Generated DSL uses the tag to keep a pulled flow's JSON unchanged.
/** @action Is_approved @conditionFormat string */
if (ctx.variables('status') === 'Approved') {
await ctx.compose('Approved', true);
}
@trigger
Names a @RecurrenceTrigger or @ConnectorTrigger. Put it in a JSDoc comment above the decorator. Without it, a recurrence trigger is called Recurrence and a connector trigger is named after its operation. HTTP and manual triggers are always called manual.
/** @trigger Every_morning */
@RecurrenceTrigger({ frequency: 'Day', interval: 1, schedule: { hours: [7], minutes: [0] } })
trigger() {}
A trigger method also takes @description (or a // comment above it).
Tags written by the generator
When FlowForger converts Logic Apps JSON to the DSL, it may add tags that keep the JSON byte-for-byte the same after a round trip. Leave them in place; you don't need to write them yourself.
| Tag | Keeps |
|---|---|
@paramsOmitted | A connector action that had no parameters key at all. |
@varNameCase "name" | The exact casing of a variable name in a variable action. |
@valueArrayForm array / createArrayString | Whether an array value was written as a JSON array or as a @createArray(...) string. |
@typeCase "foreach" | The casing of a loop's type field. |
Names you get without @action
Statements without @action get these names. Action calls always use their first argument.
| Statement | Action | Name |
|---|---|---|
let x = ... | Initialize variable | Initialize_x |
x = ... | Set variable | Set_x |
x++, x += n, x = x + n (number) | Increment variable | Increment_x |
x--, x -= n, x = x - n | Decrement variable | Decrement_x |
s += '...' (string) | Append to string variable | Append_s |
arr.push(v) | Append to array variable | Append_to_arr |
if (...) | Condition | Check_<name> for an ===/== comparison (the first quoted argument, e.g. Check_status for ctx.variables('status') === 'x'); Compare_Values for </>; Check_And for &&; Check_Or for ||; otherwise Condition |
for (const row of ...) | Apply to each | ForEach_row |
switch (...) | Switch | Switch |
case 'US': | Switch case | Case_US |
while / do...while | Do until | DoUntil |
/** @type scope */ { } | Scope | Scope |
When a name is already taken, the next one gets _2, _3, and so on (Set_x, Set_x_2). Generated names are generic and collide easily, and they are what you reference later with ctx.outputs() and ctx.body(), so name anything you intend to read.