Approvals and Webhook Actions
Some connector actions do not answer right away: they register a callback and wait until something happens, such as a person approving a request or answering an Adaptive Card. Power Automate runs these as webhook actions (OpenApiConnectionWebhook). The best-known one is the Approvals connector's "Start and wait for an approval".
Webhook actions work once the flow is deployed to Power Automate or Logic Apps. They cannot be executed locally: there is no local Approvals connector, and nothing on your machine could receive the callback. Locally FlowForger simulates them, as described below.
Writing a webhook action
Use ctx.connectorWebhook with positional arguments:
ctx.connectorWebhook(
actionName, // 'WaitForApproval'
connector, // 'approvals'
operation, // 'StartAndWaitForAnApproval'
params, // the operation's parameters
connectionReferenceName?, // 'shared_approvals'
authentication?,
);
The arguments are read by position; an object form such as ctx.connectorWebhook({ connector, operation, params }) is not supported.
/**
* Expense approval: a manager approves or rejects each expense claim.
*/
@Flow({ name: 'Expense approval' })
class ExpenseApproval {
@HttpTrigger()
trigger(ctx: FlowContext) {
return { method: 'POST' };
}
@Action()
async run(ctx: FlowContext) {
await ctx.connectorWebhook('WaitForApproval', 'approvals', 'StartAndWaitForAnApproval', {
approvalType: 'Basic',
WebhookApprovalCreationInput: {
title: `Expense ${ctx.triggerBody()?.['id']}`,
assignedTo: 'manager@contoso.com',
details: `Amount: ${ctx.triggerBody()?.['amount']}`,
enableNotifications: true,
enableReassignment: true,
},
}, 'shared_approvals');
if (ctx.body('WaitForApproval')?.['outcome'] === 'Approve') {
await ctx.compose('Approved', true);
} else {
await ctx.compose('Rejected', ctx.body('WaitForApproval')?.['responses']?.[0]?.['comments']);
}
}
constructor(ctx: FlowContext) {
ctx.flow.connectionReferences = {
shared_approvals: {
apiId: '/providers/Microsoft.PowerApps/apis/shared_approvals',
connectionReferenceLogicalName: 'contoso_approvals',
},
};
}
}
The other Approvals operations are written the same way: CreateAnApproval and WaitForAnApproval (the two halves of "Start and wait", for flows that do work between creating the approval and waiting for it).
Do not use
ctx.connectors.approvals.StartAndWaitForAnApproval(...)even though the editor offers it. The typed namespace compiles to an ordinary connector action (OpenApiConnection), not a webhook, and locally it fails because no Approvals connector exists.ctx.connectorWebhookis the form that compiles correctly.
What it compiles to
A nested parameter object is flattened to the designer's parent/child keys:
"WaitForApproval": {
"type": "OpenApiConnectionWebhook",
"inputs": {
"host": {
"connectionName": "shared_approvals",
"operationId": "StartAndWaitForAnApproval",
"apiId": "/providers/Microsoft.PowerApps/apis/shared_approvals"
},
"parameters": {
"approvalType": "Basic",
"WebhookApprovalCreationInput/title": "Expense @{triggerBody()?['id']}",
"WebhookApprovalCreationInput/assignedTo": "manager@contoso.com",
"WebhookApprovalCreationInput/details": "Amount: @{triggerBody()?['amount']}",
"WebhookApprovalCreationInput/enableNotifications": true,
"WebhookApprovalCreationInput/enableReassignment": true
},
"authentication": "@parameters('$authentication')"
}
}
Running locally
When the flow reaches a webhook action whose connector is not available locally, the action succeeds without doing anything, and its outputs are a placeholder:
{ "body": { "simulated": true, "message": "Webhook action cannot be executed locally" } }
The placeholder has no outcome and no responses, so a condition on the approval result always takes the "not approved" path. To test either branch, mock the action with the answer you want:
{
"statusCode": 200,
"body": {
"outcome": "Approve",
"responses": [{ "responder": { "email": "manager@contoso.com" }, "comments": "Looks fine" }]
}
}
flowforger run expense-approval.ff.ts --in expense.json --mock WaitForApproval=approved.json
A mocked action does not run, is marked (mocked) in the trace, and its value is used as the action's outputs as is, so ctx.body('WaitForApproval') reads its body. Make a second file with "outcome": "Reject" for the other branch. See Local testing for --mock, --mocks and --stop-before.
Things to know:
--livedoes not guard webhook actions. With--live, every connector, HTTP and child-flow action must be mocked or listed, otherwise the run stops before it. Webhook actions are simulated rather than called, so they are not part of that check: they still get the placeholder unless you mock them.--stop-before WaitForApprovalstops the run before the webhook action, like any other action.- A webhook action on a connector that is available locally is passed to that connector. Microsoft Teams' "Post adaptive card and wait for a response" (
PostCardAndWaitForResponse) therefore fails when a Graph token is loaded, and gets the placeholder when it is not. Mock it either way. - Replaying a cloud run (
run --replay-run) does not pin webhook actions to what the cloud recorded, so they get the placeholder too. To replay the real approval answer, copy it fromflowforger run-showinto a mock file.
Packages
None: the Approvals connector has no local package. The action is written into the compiled flow by @flowforger/emitter-logicapps and runs in Power Automate.