Skip to main content

Actions

Actions are the individual steps in a flow's @Action() method, called through the ctx: FlowContext parameter. Every built-in action takes a unique action name as its first argument, and every call needs await (DSL017).

Connector actions (SharePoint, Dataverse, Office 365, Teams, ...) are documented separately in Connectors.

HTTP​

Make an HTTP request to an external service.

await ctx.http('CallApi', {
method: 'GET',
url: 'https://api.contoso.com/data',
headers: { 'Accept': 'application/json' },
queries: { top: '10' },
});

HttpInputs:

  • method — GET, POST, PUT, PATCH, DELETE, ...
  • url
  • headers?
  • queries? — query parameters. They replace any query string already in url, as in the cloud.
  • cookie? — sent as the Cookie header
  • body? — a string is sent as text, an object as JSON (unless you set Content-Type)
  • authentication? — { type, tenant?, audience?, clientId?, secret?, username?, password?, value?, ... }. A local run applies Basic and Raw authentication; other types (such as managed identity or OAuth) only work in the cloud and fail locally with a clear error.
  • retryPolicy? — { type: 'none' | 'fixed' | 'exponential', count?, interval?, minimumInterval?, maximumInterval? }. Intervals are ISO 8601 durations between PT5S and P1D, count is 1–90 (RETRY_POLICY). You can also set it with the @retryPolicy tag.

Without a retryPolicy, the cloud's default applies: up to 4 retries at exponential intervals on 408, 429 and 5xx answers. Set { type: 'none' } for a call that must not repeat.

A status of 400 or more fails the action, with the response still in its outputs, so a catch scope can read ctx.outputs('CallApi')?.['statusCode']. See HTTP Connector for the full request and response rules.

Compose​

Build or transform a value.

await ctx.compose('Greeting', { message: 'Hello from FlowForger' });

Read a Compose with ctx.outputs('Greeting'): its outputs are the value itself, with no body (see Reading earlier actions).

Save File (debug only)​

Writes a file locally when running via the FlowForger engine — useful for inspecting generated content (e.g. filled templates) during local debugging. In the Maker portal / Logic Apps runtime it compiles to an ordinary Compose action with no special effect; it has no production behavior.

await ctx.saveFile('DebugOutput', {
contentType: 'text/xml',
content: ctx.eval(`@body('BuildXml')`),
fileName: 'invoice.xml', // optional; defaults to '<actionName>.<ext-from-contentType>'
encoding: 'utf8', // 'utf8' | 'base64' (use 'base64' for binary content)
});

Response​

Return an HTTP response from a flow triggered by @HttpTrigger or @ManualTrigger. response has several overloads with increasing detail:

await ctx.response('Ack', 200);
await ctx.response('Ack', 200, { status: 'ok' });
await ctx.response('Ack', 200, { status: 'ok' }, { 'Content-Type': 'application/json' });
await ctx.response('Ack', 200, { status: 'ok' }, undefined, responseSchema);
await ctx.response('Ack', 200, { status: 'ok' }, undefined, responseSchema, 'Http');

Signature: response(name, statusCode?, body?, headers?, schema?, kind?)

  • kind — 'VirtualAgent' | 'PowerApp' | 'Http', selects the response type expected by the trigger's caller (defaults to the standard HTTP response). 'PowerApp' ("Respond to a Power App or flow") pairs with @ManualTrigger, and 'VirtualAgent' with @HttpTrigger returning triggerKind: 'VirtualAgent' (DSL044).

Power Automate rejects a flow on save when a Response is:

  • inside a for...of, while or do...while loop, at any depth (DSL037). Collect the result in a variable and respond once after the loop.
  • in a flow with a recurrence trigger or a polling connector trigger (DSL038). There is no caller to respond to.
  • one of two Responses with the same status code that declare different schemas (RESPONSE_SCHEMA_MISMATCH).

Make sure a Response runs when everything succeeds. A Response chained after a catch scope is Skipped on success, so the caller gets no answer and a parent flow fails with 502 (RUNAFTER_SKIPPED_ON_SUCCESS). See Control Flow.

Terminate​

End the flow immediately with an explicit status. Use this instead of return to exit a flow early — return is ignored (DSL018).

await ctx.terminate('Stop', 'Cancelled');
await ctx.terminate('Done', 'Succeeded');
await ctx.terminate('FailFlow', 'Failed', { code: 'ERR001', message: 'Validation failed' });

Signature: terminate(name, runStatus: 'Succeeded' | 'Cancelled' | 'Failed', runError?: { code?, message? }). Only pass runError when runStatus is 'Failed' (TERMINATE_RUNERROR).

Like Response, Terminate cannot be inside a loop (DSL037): set a flag variable in the loop and terminate after it. Every block around a Terminate ends Cancelled, and everything after it is Skipped.

Delay / Delay Until​

Pause execution for a duration, or until an absolute time.

await ctx.delay('Wait5Seconds', 5, 'Second');
await ctx.delayUntil('WaitUntilNewYear', '2026-01-01T00:00:00Z');
  • delay(name, count, unit) — unit is DelayUnit: 'Second' | 'Minute' | 'Hour' | 'Day' | 'Week' | 'Month'
  • delayUntil(name, until) — until is an ISO 8601 timestamp (or expression)

Call Workflow​

Call a child workflow (another Power Automate flow) and read its response like any other action.

// By GUID
await ctx.callWorkflow('GetFolder', '11111111-1111-1111-1111-111111111111', {
projectId: ctx.triggerBody()?.['projectId'],
});

// By friendly name — requires ctx.flow.childFlows in the constructor (see Overview)
await ctx.callWorkflow('RunChildFlow', 'GenerateDocument', {
text: ctx.triggerOutputs()?.['body/recordId'],
});

// Reading the child's result
await ctx.compose('UseResult', {
folderPath: ctx.body('GetFolder')?.['folderPath'],
});

Signature: callWorkflow(name, workflowReferenceName, body?, headers?). workflowReferenceName accepts either a GUID or a name defined in ctx.flow.childFlows.

The call's outputs are what the child's first Response action answered: { statusCode, headers, body }. A response of 400 or more fails the call. A child that ends without responding fails the call with 502 BadGateway, even if the child itself succeeded, so give every child flow a Response on its success path. Locally, a child with a dslPath in ctx.flow.childFlows runs from its .ff.ts file. See Cloud Semantics.

Parse JSON​

Parse a JSON string (or value) against an optional schema.

await ctx.parseJson('ParseData', ctx.body('CallApi'), {
type: 'object',
properties: { name: { type: 'string' }, age: { type: 'number' } },
});

Signature: parseJson<T>(name, content, schema?). Read the result with ctx.body('ParseData'). A value that does not match the schema fails the action (ValidationFailed) and leaves no body. The schema validates; it never converts values. See Cloud Semantics.

Join​

Join array elements into a single string.

await ctx.join('JoinFruits', ctx.variables('items'), ', ');

Signature: join(name, from, joinWith).

Select​

Map array elements to a new shape.

await ctx.select('SelectUppercase', ctx.variables('items'), {
fruit: ctx.item(),
uppercase: ctx.toUpper(ctx.item()),
});

Signature: select<T>(name, from, selectMap).

To map each item to a single string, use one expression such as ctx.concat(ctx.item()?.['first'], ' ', ctx.item()?.['last']), not a template literal. A template literal compiles to @{...} text, which the cloud runs but the designer flags with "Enter a valid JSON" (SELECT_MAP_TEXT).

Filter / Filter Array​

Filter an array down to elements matching a condition. filterArray is an alias of filter. The where clause accepts either:

  • a raw Power Automate expression string, or
  • a TypeScript boolean expression, which the transformer converts to a Power Automate expression automatically.
// Raw PA expression string
await ctx.filterArray('LongNames', ctx.variables('items'), `@greater(length(item()), 5)`);

// TypeScript expression (transformer converts this)
await ctx.filter('ActiveItems', ctx.variables('items'),
ctx.item()?.['type'] === 'X' && ctx.item()?.['isEnabled']);

Signature: filter<T>(name, from, where: string | boolean) (same for filterArray).

Create CSV Table / Create HTML Table​

Render an array as a CSV or HTML table.

await ctx.createCsvTable('ItemsCsv', ctx.variables('items'));

await ctx.createHtmlTable('ItemsHtml', ctx.variables('items'), [
{ header: 'Name', value: ctx.item()?.['name'] },
{ header: 'Count', value: ctx.item()?.['count'] },
]);

Signature: createCsvTable(name, from, columns?) / createHtmlTable(name, from, columns?), where columns is Array<{ header: string; value: any }> (omit to auto-generate columns from the keys of the rows).

Append To String Variable​

Append text (or a JSON-serialized object) to a string variable.

await ctx.appendToStringVariable('message', ' World!');

Signature: appendToStringVariable(name, value: string | Record<string, any>). Objects are JSON-serialized, matching Logic Apps' implicit coercion.

Usually you write message += ' World!' instead, which compiles to the same action (see Variables). appendToStringVariable is declared in the web app's editor, but not in the FlowContext type that @flowforger/dsl-native exports, so a TypeScript compiler using that type may flag it. += works everywhere.

Reading Earlier Actions​

These functions read trigger data, previous actions, variables and loop items. They are not actions themselves; they compile to Power Automate expressions:

  • ctx.body<T>(actionName) — the body of a previous action's outputs
  • ctx.outputs<T>(actionName) — the outputs of a previous action
  • ctx.actions(actionName) — the action's full record: name, inputs, outputs, status ('Succeeded' | 'Failed' | 'Skipped' | 'TimedOut' | 'Cancelled'), code, error, start and end times
  • ctx.result(scopeName) — the records of the actions inside a scope or loop, for error handling in a catch scope
  • ctx.triggerBody<T>() / ctx.triggerOutputs<T>() / ctx.trigger<T>() — the trigger's body, its outputs ({ headers, body }), and its run record
  • ctx.variables<T>(name) — a variable's current value
  • ctx.parameters<T>(name) — a flow parameter (the name must match a key in ctx.flow.parameters exactly)
  • ctx.workflow<T>() — the flow's own metadata (name, run, tags)
  • ctx.item<T>() — the current item inside a for...of loop, Select, Filter array or table column
  • ctx.items<T>(loopName) — the current item of a named for...of loop
  • ctx.iterationIndexes(loopName) — the current index of a named loop
  • ctx.listCallbackUrl() — the trigger's invocation URL

Which one to use depends on the kind of action:

Actionctx.outputs('X')ctx.body('X')
Composethe valuenot meaningful: the outputs have no body. Use ctx.outputs().
HTTP, connector actions, child flow{ statusCode, headers, body }the response body
Select, Filter array, Join, Parse JSON, Create CSV/HTML table{ body }the result
Variable actionsnot used: read the variable with ctx.variables()–

ctx.body('X')?.['field'] and ctx.outputs('X')?.['body/field'] are the same read; the designer writes the second form for connector fields.

A local run is lenient and returns a Compose's value for ctx.body() as well, which hides the mistake until the flow runs in the cloud; always read a Compose with ctx.outputs().

An action can only read actions that finish before it on its run-after path. With the default sequential order that is every earlier statement; with @runAfter and parallel branches it is not, and the editor reports a read of an action on another branch as DSL048.

@Action()
async processResult(ctx: FlowContext) {
await ctx.http('CallApi', { method: 'GET', url: 'https://api.contoso.com/data' });
await ctx.compose('Summary', {
data: ctx.body('CallApi'),
status: ctx.outputs('CallApi')?.['statusCode'],
caller: ctx.triggerBody()?.['user'],
});
}