Add allowPauseResponse option to sync webhooks - #2451
Conversation
Sync webhook callers can opt in via ?respondOnPause=true to receive the response as soon as the flow pauses (e.g. a Request Action step waiting on the user) instead of only on a terminal state. Plain /sync behaviour is unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR adds an opt-in mode for synchronous webhooks to return a successful HTTP response as soon as a flow pauses (instead of only on terminal states), enabling “message delivered” semantics for notification sub-flows that pause waiting for user interaction.
Changes:
- Introduces
ProgressUpdateType.WEBHOOK_RESPONSE_ON_PAUSEto support publishing synchronous webhook responses onPAUSEDas well as terminal statuses. - Adds
/v1/webhooks/:flowId/sync?respondOnPause=truewiring through the webhook controller/handler to start runs with the new progress update type. - Updates engine update-run publishing logic and adds unit tests + webhook trigger documentation.
Blocking
- The
/syncroute’srespondOnPausequerystring is defined asType.Boolean, but the API server Ajv config usescoerceTypes: 'array', so query params won’t be coerced from'true'/'false'strings to booleans in production. This can cause/sync?respondOnPause=trueto be rejected (or mis-parsed), which breaks the primary feature of the PR.
Non-blocking
- Consider returning
pauseMetadata.response(when present) forPAUSEDresponses instead of always returning a hard-coded{ message: 'The flow is paused' }, since the response field already exists and is set by some blocks.
Merge recommendation
Do not merge
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/shared/src/lib/engine/types.ts | Adds a new progress update type to enable publish-on-pause behavior. |
| packages/server/api/src/app/webhooks/webhook-handler.ts | Passes respondOnPause through to run start via progressUpdateType. |
| packages/server/api/src/app/webhooks/webhook-controller.ts | Adds respondOnPause query param on /sync and updates route description. |
| packages/server/api/src/app/workers/engine-controller.ts | Publishes webhook response on PAUSED when opted-in; adds PAUSED response handling. |
| packages/server/api/test/unit/webhooks/webhook-handler.test.ts | Verifies progressUpdateType selection when respondOnPause is enabled/omitted. |
| packages/server/api/test/unit/webhooks/webhook-controller.test.ts | Verifies /sync query param handling and async route behavior. |
| packages/server/api/test/unit/engine/engine-controller.test.ts | Adds coverage for pause publishing behavior and ensures non-opt-in runs don’t publish on pause. |
| packages/blocks/webhook/src/lib/triggers/catch-hook.ts | Updates webhook trigger docs to mention /sync and /sync?respondOnPause=true. |
Suppressed comments (1)
packages/server/api/src/app/webhooks/webhook-controller.ts:89
- The sync route querystring schema uses
Type.Boolean, but the API server’s Ajv config usescoerceTypes: 'array'(seepackages/server/api/src/app/server.ts:40), so query params aren’t coerced from'true'/'false'strings into booleans. This will cause/sync?respondOnPause=trueto be rejected as 400 in production. Consider accepting a string and doing a strict parse (=== 'true') in the handler.
respondOnPause: Type.Optional(
Type.Boolean({
description:
'When true, the response is sent as soon as the flow pauses (e.g. a step waiting for user action), instead of waiting for the flow to reach a terminal state.',
}),
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| }>, | ||
| reply, | ||
| ) => { | ||
| const respondOnPause = request.query.respondOnPause ?? false; |
There was a problem hiding this comment.
The route schema declares respondOnPause as Type.Boolean() and the Fastify AJV config (server.ts, coerceTypes: 'array') coerces "true"/"false" to booleans before the handler runs, so request.query.respondOnPause is already a boolean here; ?? false only covers the omitted-param case. Covered by webhook-controller.test.ts: =false → handler receives false, =banana → 400 and the handler is never called.
Use pauseMetadata.response as the body when a block sets it, falling back to the generic paused message otherwise. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the boolean respondOnPause query parameter with a waitUntil enum (completed | paused) so the option names the point at which the /sync wait ends and leaves room for further modes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Drop the no-op completed value: omitting the parameter already waits for completion, so a single literal keeps the API surface minimal while leaving room to grow into a union later. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the internal respondOnPause boolean with the caller-facing waitUntil option so the request is traced under one name; the mapping to ProgressUpdateType.WEBHOOK_RESPONSE_ON_PAUSE now lives in one place. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|



Part of OPS-4578.
Additional Notes
Bug: when the child Slack/Teams notification workflow failed before actually sending the message, the parent campaign workflow still marked the opportunity as Assigned. Originating incident: the child's "Format message" code step threw, so the flow never reached the Request Action step — no message was sent, yet the campaign table row was flipped to Assigned (and, being no longer Created, was never retried). Root cause: notify-owners called the child via the async webhook endpoint, which returns 200 on enqueue — so the "Webhook succeeded?" branch passed regardless of what happened in the child run.
Fix — three pieces:
ProgressUpdateType.WEBHOOK_RESPONSE_ON_PAUSE(packages/shared): runs started with it get their synchronous webhook response published as soon as the run pauses, in addition to the existing terminal-state responses (FAILED → 500 etc.). Key insight: the Slack/Teams "Request Action" step sends the message and then pauses the run waiting for user action — so run paused ⇔ message delivered. Publish-on-pause is strictly opt-in: plainWEBHOOK_RESPONSEsync calls behave exactly as before (regression-tested), so sync flows that pause on delay steps still receive their final response after resume. The paused response body is the block-providedpauseMetadata.responsewhen set, otherwise{ message: 'The flow is paused' }./v1/webhooks/:flowId/sync?allowPauseResponse=true: the sync route accepts the opt-in booleanallowPauseResponseparameter and starts the run with the new progress-update type. Validated by the route schema:true/falseare accepted (omit the parameter or passfalseto wait for completion); any other value is rejected with 400 before the run starts./sync?allowPauseResponse=true. The existing "Webhook succeeded?" (2xx) branches now mean "message delivered (child paused) or flow completed": child fails before sending → 5xx → the row is left untouched and retried next scheduler cycle. remind/escalate had the same defect class (recording Last notified at / Escalated at on enqueue) — same one-line fix, same mechanism.Testing Checklist
Check all that apply:
I tested the feature thoroughly, including edge cases
I verified all affected areas still work as expected
Automated tests were added/updated if necessary
Changes are backwards compatible with any existing data, otherwise a migration script is provided
Visual Changes (if applicable)
N/A — the only user-visible text change is the Catch Webhook trigger description, which now documents
/syncand/sync?allowPauseResponse=true.