Connector Outputs and Failures
This page covers what every connector action has in common: the shape of its outputs, what a failure looks like and how to handle it, pagination, connector triggers, and how mocks, the --live guard and cloud-run replays apply to connector calls. The connector pages (HTTP, SharePoint, Dataverse, Microsoft 365) describe what is specific to each.
Success outputs
A successful connector action's outputs are { statusCode, body }, as in the cloud:
{ "statusCode": 200, "body": { "value": [ { "ID": 1, "Title": "First" } ] } }
The status code follows the cloud connector:
| Action | statusCode | body |
|---|---|---|
| Most reads and updates | 200 | The result |
| SharePoint create item, Dataverse add a row | 201 | The created item or row |
Dataverse upsert (UpdateRecord) | 201 when it created the row, 200 when it updated it | The row |
| Dataverse delete, relate, unrelate, file upload | 204 | none |
| Dataverse file download of a file column (the cloud asks for the first 4 MB) | none; the code is PartialContent | Binary content |
The HTTP action and SharePoint's "Send an HTTP request" add headers: { statusCode, headers, body }.
ctx.body() vs ctx.outputs()
| You want | Write | Compiles to |
|---|---|---|
| The result | ctx.body('GetItems') | body('GetItems') |
| A field of the result | ctx.body('GetItems')?.['value'] | body('GetItems')?['value'] |
| The status code | ctx.outputs('GetItems')?.['statusCode'] | outputs('GetItems')?['statusCode'] |
| Status, code, inputs, outputs, error | ctx.actions('GetItems') | actions('GetItems') |
For a connector or HTTP action, ctx.body('X') is the same as ctx.outputs('X')?.['body']. A Compose is different: its outputs are the value itself, so read it with ctx.outputs('X'). An action with no body (a 204) and an action that did not run both read as null.
Inputs
What an action was called with, after its expressions were evaluated, is in actions('X')?['inputs'] as { parameters: { ... } }, in the inputs of each trace entry (flowforger run --json), and in the debugger. Failed calls keep their inputs too, so you can see what was sent.
A parameter whose expression finds nothing is sent as null, as in the cloud. For a SharePoint or Dataverse column that clears the value.
Failures
A connector action fails in one of two ways.
The service answered with an error (a 4xx or 5xx). The action is Failed and keeps outputs: { statusCode, body }, where the body is the error the cloud connector records:
| Connector | Failure body |
|---|---|
| SharePoint | { status, message }, with the cloud's rewrites ("Item Not Found", "List not found") |
| SharePoint "Send an HTTP request", add attachment | { status, message, source, errors } |
| Dataverse | The Web API's { error: { code, message } } |
| Microsoft 365 (Graph) | Graph's { error: { code, message } } |
| HTTP action | The response as received, with headers |
actions('X')?['code'] is the HTTP status name (NotFound, BadRequest, InternalServerError, ...). Like the cloud, such a failure has no error object: read the status code and the body.
The call never reached the service: no token for that connector ("Connector 'sharepoint' not provided"), a missing required parameter, an unknown operation. The action is Failed without outputs, code is BadRequest, and actions('X')?['error']?['message'] says what went wrong. These failures come from the local setup, so do not build flow logic on them.
Handling failures in a catch scope
@Action()
async run(ctx: FlowContext) {
/** @action Try @type scope */
{
await ctx.connectors.dataverse.GetItem('GetProject', {
entityName: 'contoso_projects',
recordId: ctx.triggerBody()?.['projectId'],
});
}
/** @action Catch @type scope @runAfter Try: Failed */
{
await ctx.compose('Failure', {
statusCode: ctx.outputs('GetProject')?.['statusCode'],
message: ctx.body('GetProject')?.['error']?.['message'],
code: ctx.actions('GetProject')?.['code'],
failedActions: ctx.result('Try'),
});
}
/** @runAfter Catch: Succeeded, Skipped */
await ctx.response('Respond', 200, { done: true });
}
- The catch scope runs only when the try scope failed: an unhandled failure inside a scope fails the scope.
ctx.result('Try')lists the try scope's actions as the cloud's records:{ name, inputs, outputs, startTime, endTime, trackingId, clientTrackingId, code, status, error }.- An action after the catch scope runs only when the catch scope succeeded, unless you say otherwise.
@runAfter Catch: Succeeded, Skippedmakes it run on both paths (the catch scope is Skipped when the try scope succeeded). Writing two@runAftertags would AND them, and the action would run on neither path. - Use the null-safe
?.['x']in a catch scope: on the other failure kind the outputs are missing, and a plain['x']on a missing value fails the action, as in the cloud.
Pagination
List operations return one page unless the action has a pagination policy, the designer's "Pagination" setting:
/** @runtimeConfig {"paginationPolicy":{"minimumItemCount":5000}} */
await ctx.connectors.sharepoint.GetItems('GetAllTasks', {
dataset: 'https://contoso.sharepoint.com/sites/projects',
table: '6f1c2b6a-3f0e-4d7a-9b1e-2a4c8d0e5f71',
});
With a policy, the engine follows @odata.nextLink until at least minimumItemCount items are collected. As in the cloud:
- it keeps whole pages, so the result can hold more than the threshold (150 on 100-item pages gives 200);
- the result is
{ value: [...] }alone, with no next link and no page-level annotations, even when there was a single page.
Locally, SharePoint (GetItems, GetFileItems) and Dataverse (ListRecords) follow next pages; the Microsoft 365 connectors return their first page. A Dataverse FetchXML paging cookie is not followed. A threshold above 5,000 needs a premium plan in the cloud, and flowforger validate warns about it (PAGINATION_THRESHOLD).
Connector triggers
Connector triggers (SharePoint "When an item is created", Dataverse "When a row is added, modified or deleted", Outlook "When a new email arrives", ...) are written into the compiled flow with their parameters, but never polled or subscribed locally. A local run starts immediately, with the trigger body you pass:
flowforger run flow.ff.ts --in item.json
The file's JSON is the trigger body: ctx.triggerBody() returns it, and ctx.triggerOutputs() returns { headers: { "Content-Type": "application/json" }, body }. A file that is already an outputs object (a body plus only headers, queries, statusCode or relativePathParameters) is used as is. The easiest way to get a realistic body is from a real run: flowforger run-show --run <runId> --file flow.ff.ts --auth prints the trigger's recorded outputs, and run --replay-run <runId> uses them directly. See Cloud runs.
Mocks, live calls and replays
These run options apply to every connector, HTTP and child-flow action. The full walkthrough is in Local testing.
| Option | Effect on a connector action |
|---|---|
--mock <Action>=<outputs.json> | The action does not run and needs no token. Its inputs are still resolved and recorded, its status is Succeeded, and the file's JSON is used as its outputs as is, so write the full { "statusCode": 200, "body": { ... } }. The trace marks it (mocked). Applies at every depth and in every loop iteration. |
--mocks <file.json> | One JSON object mapping action names to outputs; combines with --mock. |
--stop-before <Action> | Ends the run before the action (exit code 3) and prints the inputs it would have been called with. No token is needed for the stopped action. |
--live <Action>[,<Action>] | Deny by default: every connector, HTTP and child-flow action must be mocked or listed, otherwise the run stops before it (exit code 3), as with --stop-before. Use it to make sure a test run never writes anything you did not allow. |
--replay-run <runId> (with --auth) | Pins each connector, HTTP and child-flow action to the outputs the cloud recorded for that run, per loop iteration, including failures with their code and error; Composes, variables and conditions recompute. An action without a pin stops the run. --live X lets X call out instead, and a --mock wins over a pin. |
An unknown action name in --mock is rejected; a mock that is never reached prints a warning.
Webhook actions such as approvals are simulated, not called, so --live does not check them; see Approvals and webhook actions.
A mocked action is always Succeeded. To exercise a catch scope, replay a cloud run where the call failed (--replay-run pins failures as failures), or let a live call fail.