Validation Rules & Diagnostics
FlowForger checks a flow before Power Automate does. Most of these checks mirror a rule the cloud enforces when you save or activate a flow, so you see the problem in your editor instead of as a failed save. Others catch things the cloud accepts but that fail when the flow runs, or that the designer flags later.
There are two families of codes:
- DSL codes (
DSL001–DSL051) come from the DSL language service. They work on your.ff.tssource, so they point at the exact line. - Validator codes (
ACTION_NAME_DUPLICATE,RUNAFTER_DEAD_STATUS, ...) come from@flowforger/validator. They work on the compiled IR or on Logic Apps JSON, so they also cover flows you did not write in the DSL (pulled flows, hand-editedclientdata.json).
Many rules exist in both families: the DSL code fires in the editor, and the validator code catches the same thing in JSON.
Severity
| Severity | Meaning |
|---|---|
| Error | The flow will not compile correctly, or Power Automate rejects it on save. flowforger validate exits with code 1. |
| Warning | Power Automate saves the flow, but it fails or misbehaves at run time, or the designer flags it. Fix it unless you know why it is there. |
| Info | A hint. Nothing is wrong on its own. |
Where you see them
- Editors. The web app's Flow Editor and the VS Code extension show DSL codes as squiggles while you type. Both hosts already show TypeScript's own syntax errors, so they do not repeat them as
DSL051. flowforger validate <flow.ff.ts>prints every DSL diagnostic asfile:line:col SEVERITY [CODE] message. When there are no DSL errors, it then compiles the flow to IR and runs the validator too. It skips validator codes that have a DSL twin, so nothing is reported twice, and it prints info-level validator hints (exceptHTTP_RETRY).flowforger validate <file.json>auto-detects IR or Logic Apps JSON and prints{ ok, issues }.- Compiling.
compile,run,pushand the debuggers refuse a file with TypeScript syntax errors and list them withfile:line:col(seeDSL051). Other diagnostics do not stop a compile, so runvalidatebefore you push. - Saving in the web app. The Flow Editor runs the Logic Apps JSON checks before it saves a flow: errors block the save, and warnings ask you to confirm. Expression problems are also marked inline in the Logic Apps JSON tab.
flowforger validate flows/order-intake.ff.ts
flowforger validate clientdata.json
Flow structure and naming
| Code | Severity | What triggers it | Fix |
|---|---|---|---|
DSL001 | Error | The class has no @Flow decorator. | Add @Flow('Name') or @Flow({ name: 'Name' }). |
DSL002 | Error | No method has a trigger decorator. | Add one of @HttpTrigger, @ManualTrigger, @RecurrenceTrigger, @ConnectorTrigger. |
DSL003 | Error | No method has @Action(). | Put the flow logic in one @Action() method. |
DSL019 | Error | More than one @Action() method. Only the first is compiled; the others are silently ignored. | Merge the logic into one method. |
DSL023 | Error | @Flow has an empty name. | Give the flow a name. |
DSL008 | Error | Two actions have the same name. | Rename one. Names must be unique across the whole flow, including inside scopes, loops and switch cases. |
ACTION_NAME_DUPLICATE | Error | Same rule on IR/JSON. The comparison is case-insensitive (Get_items and get_items collide), as in the cloud's DuplicateActionName. | Rename one. |
DSL035 | Error | An @action value contains @{, a control character, or one of < > % & \ ? /. Power Automate rejects such a name on save. The name is the rest of the @action line, so a note written after the name on the same line becomes part of it. | Put notes on their own line, or in a // comment above. |
ACTION_NAME_INVALID | Error | Same rule on IR/JSON, for action and trigger names. | Rename. |
DSL039 | Error | An action name is longer than 80 characters. | Shorten it. |
ACTION_NAME_LENGTH | Error | Same rule on IR/JSON, also for the trigger name. | Shorten it. |
DSL024 | Warning | @type has a value other than scope, if, foreach, switch, until, dountil or case. | Fix the value. Only @type scope is ever needed (see Annotations). |
DSL027 | Warning | The JSON after @metadata, @retryPolicy, @trackedProperties or @runtimeConfig does not parse. The annotation is ignored. | Fix the JSON (double-quoted keys, no trailing commas). |
DSL026 | Info | A ctx.<name>() call that is not a known FlowContext method. It is passed through as the expression function <name>(). | Check for a typo. |
IR_NAME | Error | IR without a flow name. | Add one. |
IR_WORKFLOW_ID | Error | IR workflowId that is not a GUID string. | Fix or remove it. |
IR_TRIGGER | Error | IR without exactly one trigger node (type: 'trigger' or type: 'recurrence'). | Keep one trigger. |
IR_ACTIONS | Warning | The flow has no actions. | Add actions. |
TRIGGER_COUNT | Warning | Logic Apps JSON with more than one trigger. The cloud saves it, but the designer and FlowForger work with one: only the first is shown, run locally and kept when converting to the DSL. | Keep one trigger. |
DEF_MISSING, DEF_TYPE, DEF_TRIGGER, DEF_TRIGGER_TYPE, DEF_TRIGGER_EMPTY, TRIGGER_INVALID | Error | Logic Apps JSON whose definition or triggers is missing or not an object, or a trigger that is not an object. | Fix the JSON shape. |
TRIGGER_TYPE | Warning | A Logic Apps trigger without type. | Add it. |
DESCRIPTION_LENGTH | Error | An action or trigger description in Logic Apps JSON is longer than 256 characters. Power Automate refuses it on save. The emitter never writes one: it stores a longer comment as a 256-character excerpt plus the full text in the action's metadata. | Shorten it, or let the emitter handle it. |
TypeScript the compiler cannot use
| Code | Severity | What triggers it | Fix |
|---|---|---|---|
DSL051 | Error | A TypeScript syntax error (a missing comma, an unterminated string). TypeScript recovers from these silently, so without this check the half-parsed code would compile into broken expressions. compile, run, push and the debuggers refuse the file. | Fix the syntax. |
DSL017 | Error | A ctx action call without await. It is left out of the compiled flow. | Add await. |
DSL018 | Error | A return statement in the @Action method. It has no effect: the following actions still run. | Use await ctx.terminate(...) to end the run. |
DSL020 | Warning | throw, try/catch, break or continue. They have no Logic Apps equivalent and are ignored. A break at the end of a switch case is harmless (cases never fall through). | Use scopes with @runAfter for error handling (see Control Flow), and ctx.terminate() to stop. |
DSL032 | Error | const x = await ctx.<action>(...) (or let). The binding is dropped and x compiles to a broken @x reference. | Call the action as a statement and read it with ctx.body('Name') / ctx.outputs('Name'). |
DSL025 | Warning | A const bound to a literal, object or array, or to a ctx reference read such as ctx.variables('x'). A const never creates a Power Automate variable. Reads of a literal const compile to broken text (const n = 5 gives @5); reads of a reference read are inlined, which works. | Use let for a value. A const bound to an expression (const name = ctx.triggerBody()?.['name']) is an alias and is inlined at every read. |
DSL028 | Error | A quoted spread in an array literal: ['...items', value]. The string is taken literally. | Use items.push(value). |
DSL029 | Warning | x = [...x, value] or x = x.concat(value). This compiles to Set variable, not Append to array variable. | Use x.push(value). |
A constant declared above the class is not checked: const SITE = '...' outside the class compiles to the text @SITE. Put configuration in flow parameters and read it with ctx.parameters('Name').
Variables
| Code | Severity | What triggers it | Fix |
|---|---|---|---|
DSL005 | Error | A variable is used before it is declared. | Declare it (with let) earlier, at the top level. |
DSL007 | Warning | A variable is declared but never used. | Remove it. |
DSL014 | Error | let inside an if, loop, switch or try. Power Automate only allows Initialize variable at the top level. | Declare the variable at the top of the @Action method and assign it inside the block. |
VAR_INIT_NESTED | Error | Same rule on IR/JSON, including inside a scope. | Move the initialization to the top level. |
DSL030 | Error | The same variable name is declared twice. | Declare it once and assign it afterwards. |
VAR_INIT_DUPLICATE | Error | Same rule on IR/JSON. Names are compared case-insensitively. Cloud error: "A variable must only be initialized once". | Initialize it once. |
VARIABLE_UNDEFINED | Error | An expression reads variables('x'), but nothing initializes x. Cloud error: "The variable 'x' must be initialized before it can be used". | Initialize the variable, or fix the name. |
VARIABLE_UNINITIALIZED | Error | A Set, Increment, Decrement or Append action targets a variable that nothing initializes. | Same. |
VARIABLE_TYPE | Error | A variable type other than String, Integer, Float, Boolean, Array or Object. | Fix the type. |
DSL042 | Error | More than 250 variables. | Group related values into one object variable. |
VARIABLE_COUNT | Error | Same limit on IR/JSON. | Same. |
DSL047 | Warning | A variable is set, appended to (.push()), or appended to as a string (+= on a string) inside a for...of loop that runs in parallel. In the cloud an Apply to each runs up to 20 iterations at once, while a local run takes one at a time, so the result can differ after you deploy. Increments and decrements are not reported: the cloud applies them atomically. | Add @runtimeConfig {"concurrency":{"repetitions":1}} to the loop, or build the result with ctx.select() / ctx.filterArray(). |
Parameters and connection references
| Code | Severity | What triggers it | Fix |
|---|---|---|---|
DSL015 | Warning | ctx.parameters('X') names a parameter that is not in ctx.flow.parameters. Names must match exactly, including case. The cloud saves the flow, but the expression has nothing to read. | Use the exact key from the constructor. |
EXPR_UNKNOWN_PARAMETER | Warning | Same rule on IR/JSON. $connections and $authentication always exist. | Same. |
DSL016 | Error | A connector call names a connection reference that is not in ctx.flow.connectionReferences. | Add it to the constructor, or fix the name. |
CONNECTION_REF_MISSING | Error | Logic Apps JSON only: an OpenApiConnection action or trigger uses a connectionName that is not in connectionReferences. | Add the reference. |
PARAMETER_COUNT | Error | More than 50 parameters, counting $connections and $authentication. Cloud error: "The number of template input parameters limit exceeded". | Group related values into one Object parameter. |
Placement: where an action may sit
| Code | Severity | What triggers it | Fix |
|---|---|---|---|
DSL037 | Error | ctx.response() or ctx.terminate() inside a for...of, while or do...while loop, at any depth. Cloud error: "... has type 'Response' that could not be nested under an action of type 'foreach'". | For a response, collect the result in a variable and respond once after the loop. For terminate, set a flag in the loop and terminate after it. |
RESPONSE_NESTED, TERMINATE_NESTED | Error | Same rule on IR/JSON. | Same. |
DSL038 | Error | ctx.response() with @RecurrenceTrigger or a polling @ConnectorTrigger (one with a recurrence). Cloud error: "The workflow with 'Response' action type should not have triggers with 'recurrence' property defined". A webhook connector trigger is fine. | Remove the response (nobody is waiting for it), or use an HTTP, manual or webhook trigger. |
RESPONSE_TRIGGER | Error | Same rule on IR/JSON. | Same. |
RESPONSE_SCHEMA_MISMATCH | Error | Two Response actions answer the same status code and both declare a schema, but the schemas differ (key order aside). Cloud error: "The schema definitions for actions with same status code must match." | Give both the same schema, or use different status codes. |
NESTING_DEPTH | Error | An action nested deeper than level 8 (top-level actions are level 0). Cloud error: "... nested at level '9' which exceeds the maximum nesting limit of '8'". | Flatten the logic, or move the inner part into a child flow. |
DSL044 | Warning | A 'PowerApp' response in a flow without @ManualTrigger, or a 'VirtualAgent' response without @HttpTrigger({ triggerKind: 'VirtualAgent' }). The cloud saves it, but the maker portal does not offer that combination. | Match the response kind to the trigger. |
RESPONSE_KIND | Warning | Same rule on IR/JSON. | Same. |
RUNAFTER_SKIPPED_ON_SUCCESS | Warning | No Response action runs on the path where every action succeeds. The usual cause is a Response chained after a catch scope: on success the catch is Skipped, and so is the Response. An HTTP caller then gets no answer, and a parent flow fails with 502 BadGateway (NoResponse). | Run the Response after a Finally scope (@runAfter Try: Succeeded, Failed, Skipped), or list both statuses of the catch scope (@runAfter Catch: Succeeded, Skipped). |
A Response in a parallel branch is accepted by the cloud, so it is not reported.
runAfter
| Code | Severity | What triggers it | Fix |
|---|---|---|---|
DSL021 | Error | An @runAfter status other than Succeeded, Failed, Skipped or TimedOut. | Fix the status. |
RUNAFTER_STATUS | Error | Same rule on IR/JSON, or a runAfter entry with no status at all. | List at least one valid status. |
DSL022 | Error | @runAfter names an action that does not exist. | Fix the name. |
RUNAFTER_UNKNOWN | Error | IR/JSON: a runAfter target that is not a sibling in the same container. runAfter can only point at actions in the same scope or branch. | Point at a sibling, or restructure. |
RUNAFTER_SELF | Error | An action runs after itself. | Remove the entry. |
RUNAFTER_CYCLE | Error | runAfter entries form a cycle. | Break the cycle. |
RUNAFTER_SHAPE | Error | A runAfter that is not an object. | Fix the JSON. |
DSL048 | Error | An action reads (ctx.body(), ctx.outputs(), ctx.actions(), ctx.result(), or the same functions in ctx.eval()) an action that is not on its runAfter path, or reads itself. Cloud error: "Action 'X' must either be in 'runAfter' path or within a scope action on the 'runAfter' path". An action may read only what finishes before it on its runAfter chain, or on the chain of a block enclosing it, including anything nested inside those. It may not read a parallel branch, the other branch of an if/switch, or a block that encloses it. An Until's condition may read its own body; an if, switch or loop header may not. | Read the action after the branches join, or add it to the reader's @runAfter. |
EXPR_RUNAFTER_PATH, EXPR_SELF_REFERENCE | Error | Same rule on IR/JSON. In IR, a node without runAfter runs after its previous sibling; in Logic Apps JSON it starts its container. | Same. |
RUNAFTER_DEAD_STATUS | Warning | An action lists a status that can never hold together with its other runAfter entries. runAfter entries are ANDed, so @runAfter Catch: Succeeded, Skipped plus @runAfter Try: Succeeded (where Catch runs after Try: Failed) never runs on the catch path: when Catch succeeded, Try failed. That action and everything chained after it is Skipped, and the run still ends Succeeded. | Remove the conflicting status or entry, or run the action after one Finally scope that covers every path. |
RUNAFTER_UNSATISFIABLE | Warning | No outcome of the runAfter dependencies satisfies all of them at once, so the action can never run. | Same. |
Over-listing statuses is not reported: @runAfter Try: Succeeded, Failed, Skipped is fine even when one of those can never happen.
Expressions
| Code | Severity | What triggers it | Fix |
|---|---|---|---|
DSL033 | Error | An expression inside ctx.eval('...') does not parse, or uses a double-quoted string (concat("a")). Power Automate expressions use single quotes. | Fix the syntax. |
EXPR_SYNTAX | Error | Same rule on IR/JSON, for every @... string and every @{...} segment, including an unclosed @{. | Same. |
DSL049 | Error | body(), outputs(), actions(), actionBody(), actionOutputs(), variables(), items() or result() in ctx.eval() without a name, or with a number as the name. The cloud refuses to save it. | Pass the name as text: body('Get_items'). |
EXPR_ARG_COUNT (missing name), EXPR_ARG_TYPE | Error | Same rule on IR/JSON. | Same. |
DSL050 | Warning | A function in ctx.eval() is called with a number of arguments it does not take (add(1), createArray()). The cloud saves it, but the action fails with the function's own message when it runs. | Fix the call. For an empty array write json('[]'), or [] in TypeScript. |
EXPR_ARG_COUNT (wrong count) | Warning | Same rule on IR/JSON, for any expression in the flow. | Same. |
DSL034 | Warning | An unknown function in ctx.eval(). The cloud saves it and fails at run time; the local engine fails too. | Check for a typo. There is no abs(), ceil(), floor() or round() in Power Automate. |
EXPR_UNKNOWN_FUNCTION | Warning | Same rule on IR/JSON. | Same. |
DSL004 | Error | ctx.body(), ctx.outputs() or ctx.actions() names an action that is not declared before it. | Fix the name, or move the reading action after it. |
EXPR_UNKNOWN_ACTION | Error | IR/JSON: an expression references an action that does not exist (InvalidTemplate). | Fix the name. |
EXPR_LOOP_REFERENCE | Error | items('X') or iterationIndexes('X') outside a loop named X. Cloud error: "repetition actions referenced by inputs are not defined". | Use the enclosing loop's name. |
EXPR_LOOP_REFERENCE (item()) | Warning | item() outside a foreach loop, Select, Filter array or Create table. The cloud saves it; there is no current item when it runs. | Move it into a loop, or use items('Loop'). |
EXPR_LENGTH | Error | A single expression longer than 8,192 characters (counted without the leading @, each @{...} on its own). Cloud error: "The template language expression limit exceeded". | Split it across Compose actions. |
CONDITION_NOT_STRING | Error | Logic Apps JSON: an If condition with { "not": "@empty(x)" }. The cloud refuses a not whose operand is an expression string. The emitter never writes this; it writes { "not": { "equals": ["@empty(x)", true] } }. | Wrap the operand as a comparison. |
Descriptions are never checked as expressions: Power Automate saves @{ or a leading @ in a note as plain text.
Control flow shapes
| Code | Severity | What triggers it | Fix |
|---|---|---|---|
DSL041 | Error | A switch with more than 25 cases. | Split it into nested switches, or use a lookup object. |
SWITCH_CASES | Error | Same limit on IR/JSON. | Same. |
SWITCH_EXPRESSION | Error | IR/JSON: a Switch on plain text or a literal instead of an expression. | Switch on an expression starting with @. |
IF_CONDITION | Error | IR: an if node without a condition. | Add one. |
FOREACH_ITEMS | Error | IR: a foreach node without itemsExpression. | Add one. |
UNTIL_LIMIT | Error | Logic Apps JSON: an Until without limit. Cloud error: "The property 'limit' is required on action type 'Until'". The emitter always writes one (60 iterations, PT1H by default). | Add limit.count and/or limit.timeout. |
UNTIL_EMPTY | Warning | An Until with no actions: it only re-evaluates its condition until the limit is reached. | Add the loop body. |
UNTIL_COUNT, UNTIL_TIMEOUT | Warning | An Until count outside 1–5,000, or a timeout that is not an ISO 8601 duration. The cloud saves both. | Fix the value. |
TERMINATE_STATUS | Error | A Terminate status other than Succeeded, Cancelled or Failed. | Fix it. |
TERMINATE_RUNERROR | Error | A Terminate with runError but a status other than Failed. | Only pass a run error with 'Failed'. |
Annotation values and limits
| Code | Severity | What triggers it | Fix |
|---|---|---|---|
DSL043 | Error | A value out of range in a JSDoc annotation: @runtimeConfig concurrency.repetitions outside 1–50 or concurrency.runs outside 1–100; @retryPolicy type other than none/fixed/exponential, count outside 1–90, or an interval that is not an ISO 8601 duration between PT5S and P1D. | Fix the value. |
DSL043 (@limit) | Warning | @limit with a count outside 1–5,000, or a timeout that is not an ISO 8601 duration. The cloud saves it. | Fix the value. |
FOREACH_CONCURRENCY | Error | Foreach concurrency outside 1–50. | Fix it. |
TRIGGER_CONCURRENCY | Error | Trigger concurrency (runtimeConfiguration.concurrency.runs) outside 1–100. | Fix it. |
RETRY_POLICY | Error | A retry policy that is not an object, has an unknown type, a count outside 1–90, or an interval that is not an ISO 8601 duration between PT5S and P1D. Intervals in milliseconds are not accepted. | Use ISO durations such as "PT20S". |
DSL040 | Error | More than 500 actions. | Move part of the logic into a child flow. |
ACTION_COUNT | Error | Same limit on IR/JSON. | Same. |
PAGINATION_THRESHOLD | Warning | paginationPolicy.minimumItemCount above 5,000 (or not a whole number from 1 to 100,000). Above 5,000 the designer reports the setting as invalid unless the flow owner has a premium Power Automate plan. | Use 5,000 or less for flows that run on Microsoft 365 licenses. |
Triggers
| Code | Severity | What triggers it | Fix |
|---|---|---|---|
DSL045 | Error | A @RecurrenceTrigger with an unknown frequency, an interval that is not a whole number or gives a period over 500 days, a startTime more than 49 years ahead, or a schedule whose next run is more than 18 months away. | Fix the option. |
DSL046 | Error | A recurrence schedule with a frequency other than Day, Week or Month; weekDays without frequency: 'Week'; monthDays without frequency: 'Month'. Power Automate rejects these on save. | Match the schedule to the frequency. |
RECURRENCE, RECURRENCE_SCHEDULE | Error | The same rules on IR/JSON, for the Recurrence trigger and for a polling connector trigger's recurrence. Also an unparseable startTime. | Same. |
Connectors
| Code | Severity | What triggers it | Fix |
|---|---|---|---|
DV_LOOKUP_UNGUARDED | Info | A Dataverse GetItem ("Get a row") whose recordId is a lookup column (_x_value) read from another action, with no enclosing if that checks that same column. When the lookup is empty, recordId is null and the action fails. | If the lookup can be empty, wrap the call in if (!ctx.empty(...)) on the same value. A required lookup is fine as is. |
SELECT_MAP_TEXT | Warning | A Select whose map is text rather than one expression, typically a template literal (`${ctx.item()?.['a']}-x` compiles to @{...} text). The cloud saves and runs it, but the designer reads a text map as JSON and flags the action with "Enter a valid JSON". | Make the map one expression: ctx.concat(ctx.item()?.['a'], '-x'). |
HTTP_RETRY | Info | IR: an HTTP action without retryPolicy. flowforger validate does not print it for .ff.ts files, since the cloud applies its default policy. | Nothing to do, unless you want a specific policy. |
IR_HTTP | Error | IR: an HTTP action without method or url. | Add them. |
CONNECTOR_FIELDS, CONNECTOR_WEBHOOK_FIELDS | Error | IR: a connector node without connector or operation. | Add them. |
SP_PARAMS | Error | IR: a SharePoint GetItems, GetItemById, CreateItem, UpdateItem or DeleteItem without its site (dataset), list (table), item id or fields. | Add the missing parameters. |
DV_PARAMS | Error | IR: a Dataverse node with operation ListRows, CreateRow, UpdateRow, DeleteRow or RetrieveRow without its table, row id or body. | Add the missing parameters. |
APPROVAL_PARAMS | Error | IR: an Approvals StartAndWaitForAnApproval webhook without approvalType. | Add it. |
SP_PARAMS and DV_PARAMS only look at the operation names listed. The typed Dataverse operations (ListRecords, CreateRecord, ...) and most SharePoint operations are not checked this way.
Codes that are not reported
DSL031(retired): "description exceeds 255 characters". Long comments are no longer an error: the emitter writes a 256-character excerpt and keeps the full text in metadata. The number is not reused.DSL036(retired): "comment starting with@". Power Automate saves such a description as is. The number is not reused.DSL035used to mean "@{in a comment" and now means an invalid@actionname.DSL009,DSL010,DSL011,DSL012,DSL013are defined in the language service but no current rule reports them. Their topics are covered by other codes: loop references byEXPR_LOOP_REFERENCE, IR validation byflowforger validate.
See also
- Annotations — the JSDoc tags several of these rules check
- Cloud Semantics — why some problems only show up at run time
- Control Flow — try/catch scopes and
@runAfter