Skip to main content

IR Reference

The Intermediate Representation (IR) is the core data format used by all FlowForger tools. It is defined in @flowforger/ir, and every other package (DSL, engine, emitter, validator) depends on these types. flowforger compile flow.ff.ts --out flow.ir.json shows the IR of a DSL flow.

Structure​

A flow IR is a FlowIR object. name and nodes are the only required fields — everything else is optional metadata used to round-trip Logic Apps JSON and DSL-layer configuration (ctx.flow).

{
"name": "MyFlow",
"description": "Optional flow description",
"workflowId": "11111111-1111-1111-1111-111111111111",
"nodes": [
{ "id": "trg_1", "type": "trigger", "name": "manual", "kind": "manual", "inputs": {} },
{ "id": "act_1", "type": "action", "name": "GetData", "kind": "http", "inputs": { "method": "GET", "url": "..." } }
],
"parameters": {
"SiteUrl": { "type": "String", "defaultValue": "https://contoso.sharepoint.com" }
},
"connectionReferences": {
"shared_sharepointonline": {
"apiId": "/providers/Microsoft.PowerApps/apis/shared_sharepointonline",
"connectionReferenceLogicalName": "cr_sharepoint"
}
},
"metadata": { "schemaVersion": "1.0.0.0" },
"childFlows": {
"ProcessDocument": { "workflowId": "22222222-2222-2222-2222-222222222222" }
}
}

FlowIR fields​

FieldTypeNotes
namestringRequired. Flow name.
descriptionstringOptional. Shown in the Power Automate UI.
workflowIdstringOptional. Dataverse workflow GUID. DSL-layer only — not part of Logic Apps JSON.
nodesNode[]Required. See Node Types.
parametersRecord<string, FlowParameter>Flow/workflow parameters (e.g., environment variables).
outputsRecord<string, any>Logic Apps outputs definition.
connectionReferencesRecord<string, ConnectionReference>Connection references for Logic Apps deployment, keyed by reference name.
metadataFlowMetadataSchema metadata: schemaVersion, contentVersion, $schema.
workflowMetadataRecord<string, any>Workflow-level metadata at definition.metadata (creator, flowclientsuspensionreason, etc.), preserved verbatim.
staticResultsRecord<string, any>Static/mock results for testing action responses.
childFlowsRecord<string, ChildFlowDefinition>Child flow definitions for name-based workflow references, keyed by friendly name. See Child Flows.

The validator (@flowforger/validator) requires name to be a non-empty string, workflowId (if present) to be a valid GUID, and exactly one trigger node — either type: 'trigger' or type: 'recurrence' (see Validation).

Node Types​

Every node extends BaseNode:

interface BaseNode {
id: string;
name: string;
description?: string; // Action description
runtimeConfiguration?: Record<string, any>; // e.g. { concurrency: { runs: 20 } }
metadata?: Record<string, any>; // preserves operationMetadataId, etc.
}

The Node union is:

type Node =
| TriggerNode
| RecurrenceTriggerNode
| ActionNode
| ScopeNode
| IfNode
| ForeachNode
| SwitchNode
| DoUntilNode
| ConnectorActionNode
| ConnectorWebhookActionNode;

Trigger Nodes​

There are two trigger node shapes, plus a separate top-level type for recurrence.

TriggerNode (type: 'trigger')​

Covers HTTP, connector (polling/webhook), and manual/button triggers via the kind discriminator:

interface TriggerNode extends BaseNode {
type: 'trigger';
kind: 'http' | 'connector' | 'manual';
inputs: HttpTriggerInputs | ConnectorTriggerInputs | ManualTriggerInputs;
conditions?: TriggerCondition[]; // trigger conditions (e.g. "@equals(triggerBody()?['text'],'test')")
correlation?: Record<string, any>;
}
  • kind: 'http' → inputs: HttpTriggerInputs — { method?, path?, schema?, headersSchema?, triggerAuthenticationType?, triggerKind?: 'Http' | 'VirtualAgent' }
  • kind: 'connector' → inputs: ConnectorTriggerInputs — { connector, operation, params, connectionReferenceName?, triggerType?: 'OpenApiConnection' | 'OpenApiConnectionWebhook' | 'OpenApiConnectionNotification', splitOn?, recurrence?: { interval, frequency }, authentication?, retryPolicy? }. Examples: office365/OnNewEmailV3, sharepoint/OnFileCreated.
  • kind: 'manual' → inputs: ManualTriggerInputs — { schema?, headersSchema?, triggerAuthenticationType?, triggerKind?: 'Button' | 'PowerAppV2' }. This is the manual/button (PowerApps "Run a flow"/instant) trigger.
{ "id": "trg_1", "type": "trigger", "name": "manual", "kind": "manual", "inputs": {} }

RecurrenceTriggerNode (type: 'recurrence')​

interface RecurrenceTriggerNode extends BaseNode {
type: 'recurrence';
inputs: RecurrenceTriggerInputs;
evaluatedRecurrence?: RecurrenceTriggerInputs; // effective schedule after Power Automate processing
conditions?: TriggerCondition[];
correlation?: Record<string, any>;
}

interface RecurrenceTriggerInputs {
frequency: 'Second' | 'Minute' | 'Hour' | 'Day' | 'Week' | 'Month' | 'Year';
interval: number;
count?: number;
startTime?: string; // ISO 8601
endTime?: string; // ISO 8601
timeZone?: string; // e.g. 'UTC'
schedule?: {
minutes?: number[];
hours?: number[];
weekDays?: ('Monday' | ... | 'Sunday')[];
monthDays?: number[];
monthlyOccurrences?: Array<{ dayOfWeek: string; occurrence: number }>;
};
}
{
"id": "trg_1",
"type": "recurrence",
"name": "Recurrence",
"inputs": { "frequency": "Day", "interval": 1, "timeZone": "UTC" }
}

Action Nodes​

A single ActionNode type (type: 'action') represents most built-in actions. Which action it is comes from the kind discriminator, and inputs is shaped accordingly:

interface ActionNode extends BaseNode {
type: 'action';
kind: 'http' | 'compose' | 'expression' | 'initializevariable' | 'setvariable' |
'incrementvariable' | 'decrementvariable' | 'appendtoarrayvariable' |
'appendtostringvariable' | 'join' | 'select' | 'filterarray' | 'parsejson' |
'createcsvtable' | 'createhtmltable' | 'response' | 'terminate' | 'delay' |
'delayuntil' | 'workflow';
inputs: HttpActionInputs | ComposeActionInputs | ExpressionActionInputs |
VariableActionInputs | DataOperationInputs | ResponseInputs |
TerminateInputs | DelayInputs | WorkflowActionInputs;
retryPolicy?: RetryPolicy;
runAfter?: Record<string, StepResultStatus[]>;
limit?: ActionLimit; // e.g. timeout for workflow calls
trackedProperties?: Record<string, string>;
}

Kind-by-kind input shapes:

kindInputs typeShape
httpHttpActionInputs{ method, url, headers?, body? }, plus queries?, cookie? and authentication? when the DSL sets them
composeComposeActionInputs{ value } (any value or expression string)
expressionExpressionActionInputs{ expressionKind: string, [key: string]: any } — e.g. IndexOf, Add, arbitrary expression-function actions
initializevariable / setvariable / incrementvariable / decrementvariable / appendtoarrayvariable / appendtostringvariableVariableActionInputs{ variableName?, variableType?, value?, name? } — initializevariable uses variableName and variableType, the others name their target in name. The DSL writes variableType in lowercase ('integer'); the TypeScript type lists the capitalized forms, and the validator accepts either case
joinDataOperationInputs{ from, joinWith }
selectDataOperationInputs{ from, select }
filterarrayDataOperationInputs{ from, where }
parsejsonDataOperationInputs{ from, schema } (via content/schema at the DSL layer, from in IR)
createcsvtable / createhtmltableDataOperationInputs{ from, columns?: Array<{ header, value }> }
responseResponseInputs{ statusCode?, headers?, body?, schema?, kind?: 'VirtualAgent' | 'PowerApp' | 'Http' }
terminateTerminateInputs{ runStatus: 'Succeeded' | 'Cancelled' | 'Failed', runError?: { code?, message? } }
delay / delayuntilDelayInputs{ interval?: { count, unit }, until? } — delay uses interval, delayuntil uses until
workflowWorkflowActionInputs{ workflowReferenceName?, workflowId?, body?, headers? } — child-workflow invocation (ctx.callWorkflow(...))
{
"id": "act_1",
"type": "action",
"name": "ComposeAbsoluteUrl",
"kind": "compose",
"inputs": { "value": "@concat('https://sharepoint.com/', triggerBody()?['text'])" },
"runAfter": { "manual": ["Succeeded"] }
}

Child-workflow call example:

{
"id": "act_call",
"type": "action",
"name": "CallProcessDocument",
"kind": "workflow",
"inputs": { "workflowReferenceName": "22222222-2222-2222-2222-222222222222", "body": { "text": "@triggerBody()" } },
"limit": { "timeout": "PT1H" }
}

Note: ctx.saveFile(...) (debug-only file dump) does not have a dedicated kind. It compiles to an ordinary kind: 'compose' action whose value carries an @@ff:saveFile sentinel object ({ '@@ff:saveFile': true, contentType, content, fileName?, encoding? }), recognized only by local FlowForger hosts (CLI/engine); it has no effect in the Maker portal.

Control Flow Nodes​

All control nodes nest child nodes in actions (and, for if/switch, alternate branches), so any node type — including other control nodes — can be nested.

ScopeNode (type: 'scope')​

interface ScopeNode extends BaseNode {
type: 'scope';
actions: Node[];
runAfter?: Record<string, StepResultStatus[]>;
trackedProperties?: Record<string, string>;
}

IfNode (type: 'if')​

interface IfNode extends BaseNode {
type: 'if';
condition: string; // e.g. "@equals(variables('x'), 1)"
conditionFormat?: 'string' | 'object'; // 'string' for "@equals(...)", 'object' for {"equals":[...]}
actions: Node[]; // then-branch
elseActions?: Node[]; // else-branch
runAfter?: Record<string, StepResultStatus[]>;
trackedProperties?: Record<string, string>;
}

ForeachNode (type: 'foreach')​

interface ForeachNode extends BaseNode {
type: 'foreach';
itemsExpression: string; // items to iterate, e.g. "@body('GetItems')?['value']"
actions: Node[];
parallel?: boolean;
runAfter?: Record<string, StepResultStatus[]>;
trackedProperties?: Record<string, string>;
}

The loop's concurrency lives in runtimeConfiguration.concurrency.repetitions (1–50), as in Logic Apps JSON. parallel: true is set when a Logic Apps loop with repetitions above 1 is parsed; the engine then runs that many iterations at once. Without either, the engine runs the iterations one at a time.

SwitchNode (type: 'switch')​

interface SwitchCase {
name: string; // case key in Logic Apps JSON
value: string | number; // matched "case" value
actions: Node[];
}

interface SwitchNode extends BaseNode {
type: 'switch';
expression: string; // expression to evaluate
cases: SwitchCase[];
defaultActions?: Node[];
runAfter?: Record<string, StepResultStatus[]>;
trackedProperties?: Record<string, string>;
}
{
"id": "sw_1",
"type": "switch",
"name": "SwitchOnStatus",
"expression": "@variables('status')",
"cases": [
{ "name": "Case_Approved", "value": "Approved", "actions": [] },
{ "name": "Case_Rejected", "value": "Rejected", "actions": [] }
],
"defaultActions": []
}

DoUntilNode (type: 'dountil')​

interface DoUntilNode extends BaseNode {
type: 'dountil';
condition: string; // checked after each iteration
actions: Node[];
limit?: number; // max iterations (default 60)
timeout?: string; // ISO 8601 duration, e.g. 'PT1H'
runAfter?: Record<string, StepResultStatus[]>;
trackedProperties?: Record<string, string>;
}

Note: initializevariable actions are not allowed inside any control node (if, scope, foreach, switch, dountil) — Logic Apps requires them at the root level. The validator flags this (VAR_INIT_NESTED).

Connector Nodes​

Two node types cover connector operations (SharePoint, Dataverse, Office 365, Teams, Approvals, etc.), split by whether the underlying Logic Apps action is a plain OpenApiConnection or a webhook/wait-for-callback style action.

ConnectorActionNode (type: 'connector')​

interface ConnectorActionNode extends BaseNode {
type: 'connector';
connector: 'sharepoint' | 'dataverse' | string;
operation: string;
params: Record<string, any>;
paramsOmitted?: boolean; // true if source had no `parameters` key (parity: don't emit `parameters: {}`)
connectionReferenceName?: string; // e.g. 'shared_sharepointonline_2'
runAfter?: Record<string, StepResultStatus[]>;
retryPolicy?: RetryPolicy;
limit?: ActionLimit;
trackedProperties?: Record<string, string>;
}
{
"id": "con_1",
"type": "connector",
"name": "GetItems",
"connector": "sharepoint",
"operation": "GetItems",
"params": { "dataset": "https://contoso.sharepoint.com/sites/hr", "table": "11111111-2222-3333-4444-555555555555", "$top": 100 },
"connectionReferenceName": "shared_sharepointonline"
}

params use the operation's Power Automate parameter names, with nested values flattened to /-joined keys ("item/Title", "item/Status/Value"), exactly as they appear in the Logic Apps action's inputs.parameters.

ConnectorWebhookActionNode (type: 'connectorwebhook')​

For connector operations that wait on a callback/webhook (e.g. Approvals StartAndWaitForAnApproval, Teams adaptive-card responses):

interface ConnectorWebhookActionNode extends BaseNode {
type: 'connectorwebhook';
connector: string; // e.g. 'approvals', 'teams'
operation: string; // e.g. 'StartAndWaitForAnApproval'
params: Record<string, any>;
connectionReferenceName?: string;
runAfter?: Record<string, StepResultStatus[]>;
retryPolicy?: RetryPolicy;
limit?: ActionLimit;
trackedProperties?: Record<string, string>;
}

Despite being a distinct node type, connectorwebhook nodes are generated with the same con_ ID prefix as connector nodes (see ID Conventions).

Supporting Types​

  • RetryPolicy — { type: 'none' | 'fixed' | 'exponential', count?, interval?, minimumInterval?, maximumInterval? }. Write intervals as ISO 8601 duration strings between PT5S and P1D ('PT20S') and count as 1–90; the TypeScript type also allows a number of milliseconds for interval, but the validator and Power Automate do not (RETRY_POLICY)
  • ActionLimit — { timeout?: string } (ISO 8601 duration, e.g. 'PT2M')
  • StepResultStatus — 'Succeeded' | 'Failed' | 'Skipped' | 'TimedOut' | 'Cancelled'. runAfter maps use the first four; Cancelled is the status of a block that a Terminate inside it ended
  • StepResult — { status: StepResultStatus, outputs?: any, error?: any } (engine execution result, not itself an IR field)
  • TriggerCondition — { expression: string }, used in TriggerNode.conditions / RecurrenceTriggerNode.conditions

Connection references, parameters, and metadata​

interface ConnectionReference {
apiId: string; // '/providers/Microsoft.PowerApps/apis/shared_sharepointonline'
connectionReferenceLogicalName?: string; // Dataverse logical name (solution-aware flows)
connectionName?: string; // direct connection ID (embedded connections)
runtimeSource?: string; // typically 'embedded' or 'invoker'
impersonation?: Record<string, unknown>;
}

interface FlowMetadata {
schemaVersion?: string; // e.g. "1.0.0.0"
contentVersion?: string;
$schema?: string;
}

interface FlowParameter {
type: 'String' | 'Int' | 'Float' | 'Bool' | 'Array' | 'Object' | 'SecureString' | 'SecureObject';
defaultValue?: any;
allowedValues?: any[];
metadata?: { schemaName?: string; description?: string; displayName?: string };
}

Child Flows​

FlowIR.childFlows lets a flow reference other flows by friendly name (resolved to a workflow action's workflowReferenceName/workflowId at compile/emit time):

interface ChildFlowParameter {
title: string;
type: string; // 'string' | 'number' | 'boolean' | 'object' | 'array'
required: boolean;
}

interface ChildFlowDefinition {
workflowId: string; // Dataverse workflow GUID
description?: string;
parameters?: Record<string, ChildFlowParameter>; // child flow's trigger parameter schema
dslPath?: string; // path to the child flow's .ff.ts file, relative to the parent
}

This mirrors ctx.flow.childFlows in the DSL layer, along with ctx.flow.metadata, ctx.flow.connectionReferences, and ctx.flow.parameters, which map directly onto FlowIR.metadata, FlowIR.connectionReferences, and FlowIR.parameters.

ID Conventions​

The DSL transformer generates IDs from a global counter, with a prefix per node type:

  • trg_ — Triggers (trigger and recurrence types)
  • act_ — Actions
  • scp_ — Scopes
  • if_ — Conditionals
  • fe_ — ForEach loops
  • sw_ — Switch nodes
  • du_ — Do-until nodes
  • con_ — Connector actions and connector webhook actions (both connector and connectorwebhook node types share this prefix)

Validation​

validateFlowIR (@flowforger/validator) checks an IR document against the rules Power Automate enforces when a flow is saved, plus a few problems the cloud only shows at run time or in the designer; validateLogicApps runs the same rules on Logic Apps JSON. flowforger validate <file.json> runs whichever fits and prints { ok, issues }, where each issue has a level (error, warning or info), a code, a message and usually a path.

The checks cover the IR shape (IR_NAME, IR_WORKFLOW_ID, IR_TRIGGER, IR_HTTP, ...), names and limits, action placement, variables, runAfter, expressions and connector parameters. Every code, its severity, the cloud error it prevents and how to fix it are listed in Validation Rules & Diagnostics.

Type Guards​

The @flowforger/ir package exports one type guard per node type:

  • isTrigger(n) → TriggerNode
  • isRecurrenceTrigger(n) → RecurrenceTriggerNode
  • isAction(n) → ActionNode
  • isScope(n) → ScopeNode
  • isIf(n) → IfNode
  • isForeach(n) → ForeachNode
  • isSwitch(n) → SwitchNode
  • isDoUntil(n) → DoUntilNode
  • isConnector(n) → ConnectorActionNode
  • isConnectorWebhook(n) → ConnectorWebhookActionNode