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, ...urlheaders?queries?— query parameters. They replace any query string already inurl, as in the cloud.cookie?— sent as theCookieheaderbody?— a string is sent as text, an object as JSON (unless you setContent-Type)authentication?—{ type, tenant?, audience?, clientId?, secret?, username?, password?, value?, ... }. A local run appliesBasicandRawauthentication; 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 betweenPT5SandP1D,countis 1–90 (RETRY_POLICY). You can also set it with the@retryPolicytag.
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@HttpTriggerreturningtriggerKind: 'VirtualAgent'(DSL044).
Power Automate rejects a flow on save when a Response is:
- inside a
for...of,whileordo...whileloop, 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)—unitisDelayUnit:'Second' | 'Minute' | 'Hour' | 'Day' | 'Week' | 'Month'delayUntil(name, until)—untilis 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 outputsctx.outputs<T>(actionName)— the outputs of a previous actionctx.actions(actionName)— the action's full record:name,inputs,outputs,status('Succeeded' | 'Failed' | 'Skipped' | 'TimedOut' | 'Cancelled'),code,error, start and end timesctx.result(scopeName)— the records of the actions inside a scope or loop, for error handling in a catch scopectx.triggerBody<T>()/ctx.triggerOutputs<T>()/ctx.trigger<T>()— the trigger's body, its outputs ({ headers, body }), and its run recordctx.variables<T>(name)— a variable's current valuectx.parameters<T>(name)— a flow parameter (the name must match a key inctx.flow.parametersexactly)ctx.workflow<T>()— the flow's own metadata (name,run,tags)ctx.item<T>()— the current item inside afor...ofloop, Select, Filter array or table columnctx.items<T>(loopName)— the current item of a namedfor...ofloopctx.iterationIndexes(loopName)— the current index of a named loopctx.listCallbackUrl()— the trigger's invocation URL
Which one to use depends on the kind of action:
| Action | ctx.outputs('X') | ctx.body('X') |
|---|---|---|
| Compose | the value | not 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 actions | not 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'],
});
}