Draft: Duo flow webhook callbacks via regular webhooks (duo_flow_callback_enabled flag + callback_hook_id)
What does this MR do and why?
Adds Duo flow webhook callbacks: a way for an external client to subscribe to a Duo flow's lifecycle over HTTP instead of polling. A regular project or group webhook is opted in as a callback endpoint via a new duo_flow_callback_enabled capability flag; the client then references that webhook by ID (callback_hook_id) when triggering a flow, and GitLab POSTs a small JSON payload on every state change (flow.started, flow.progress, flow.completed, flow.failed).
This reuses the existing GitLab webhook infrastructure end to end — encryption at rest, SSRF protection, delivery logging, retries, and payload signing — and surfaces callback hooks in the standard Settings > Webhooks UI and hooks REST API, so there is no separate webhook-management surface to learn.
Created as part of the DAP Extensibility challenge.
How it works
- Enable once — create or edit a project or group webhook with
duo_flow_callback_enabled: true(UI checkbox or hooks REST API). GitLab stores the URL + secret (encrypted) and returns the webhookid. - Reference many times — pass
callback_hook_id: <id>(and an optionalclient_reference) when creating a flow. Every flow started with that id delivers its lifecycle events to the endpoint.
A group webhook fires for any flow whose project is in that group or a descendant; a project webhook fires only for that project.
Changes
- Migration — add
duo_flow_callback_enabledboolean toweb_hooks. It is a capability flag, not an event subscription: deliberately not named*_eventsand not registered inTriggerableHooks, so it never fires from event dispatch. CallbackHooksFinder— resolves theProjectHook/GroupHookrecords usable as a callback endpoint for a given project (its own project hooks plus hooks on the project's group and any ancestor group).- Hooks API + settings form — optional
duo_flow_callback_enabledparam on the project/group hooks REST API (params + entities), and a "Duo Agent Platform" checkbox in the webhook form that shows the hook ID once saved. - Flow triggering —
build_messaging_callback_contextresolvescallback_hook_idviaCallbackHooksFinder; an out-of-scope hook is rejected with400. Adds an optionalclient_reference(max 255 chars) stored in the callback context and echoed in every payload so clients can correlate callbacks with their own records. - Delivery — a webhook messaging adapter builds the payload;
WebhookDeliveryWorkersends it viaWebHookServiceand raises on failure so Sidekiq retries with backoff, using a deterministic idempotency key per(workflow, event)for client-side de-duplication. - Docs — engineer integration guide (
webhook_integration.md).
Payload shape
{
"object_kind": "duo_workflow",
"version": "1",
"event": "flow.completed",
"event_id": "flow.completed-2722",
"project": {
"id": 17,
"path_with_namespace": "mygroup/myproject",
"web_url": "https://gitlab.example.com/mygroup/myproject"
},
"workflow": {
"id": 2722,
"status": "finished",
"web_url": "https://gitlab.example.com/mygroup/myproject/-/automate/agent-sessions/2722"
},
"client_reference": "run-abc123",
"message": "Done!"
}message is present on flow.completed (the agent's final answer); error is present on flow.failed. client_reference is present when supplied at trigger time.
Permissions
- Configuring the flag: Maintainer+ on the project (project webhook) or Owner on the group (group webhook) — the standard
admin_web_hookability. - Triggering a flow with
callback_hook_id: any user who can create the flow. The webhook was opted in by a Maintainer/Owner at configuration time, and scope containment (CallbackHooksFinder) is the authorization boundary — no separateadmin_web_hookcheck is re-run at trigger time.
Design history / rationale (high-signal)
Paths deliberately not taken, recorded so we don't re-litigate them later:
- A flag on a regular webhook, not a bespoke model + API. An earlier iteration used a dedicated
FlowCallbackHookSTI model and a/ai/duo_workflows/flow_callbacksCRUD API. Folding it into regular webhooks reuses encryption, SSRF protection, delivery logging, retries, and signing for free, avoids a parallel management surface, and makes callback hooks visible in the standard Webhooks UI/API. Trade-off:duo_flow_callback_enabledlives on the CEweb_hookstable / project hooks API while the consuming feature is EE-only (see GA checklist). - Scoped to group/project, not organization. Anchoring to a group or project gives isolation on GitLab.com and a real permission model (
admin_web_hook); an organization anchor gave neither. - A dedicated
WebhookDeliveryWorker, notWebHook#async_execute. The standard async path (WebHookWorker) swallows HTTP failures and never redelivers — acceptable for event-stream webhooks that fire again on the next event, but a dropped terminal event (flow.completed) leaves a client hanging. So delivery runsWebHookServicesynchronously and raises on non-success to get Sidekiq retries + backoff, with a deterministic idempotency key for client-side dedup.
Commit structure (future split boundaries)
This MR is a reference for the full implementation and is intended to be split. Commits are organized along the likely slice boundaries:
web_hookscolumn (migration)CallbackHooksFinder- Hooks API + settings-form checkbox
- Flow-triggering wiring (
callback_hook_id,client_reference) - Delivery (payload adapter +
WebhookDeliveryWorker) - Integration guide docs
How to set up and validate locally (GDK)
Prereqs: an EE GDK with a license; Duo / agent platform usable in your instance; run gdk restart rails-web sidekiq after checkout so the new worker queue is picked up. Callback URLs must be public HTTPS (local/internal is SSRF-blocked) — use a https://webhook.site/<uuid> endpoint, or enable Admin > Settings > Network > Outbound requests > "Allow requests to the local network from webhooks and integrations".
- Enable the flag on a webhook (UI): Project (or Group) > Settings > Webhooks. Create a webhook pointing at your webhook.site URL, tick Duo Agent Platform > "Allow this webhook to be used as a Duo flow callback endpoint", and save. Note the Hook ID shown in that section.
- Or via API:
POST /api/v4/projects/:id/hookswith{"url": "https://webhook.site/<uuid>", "duo_flow_callback_enabled": true}.
- Or via API:
- Trigger a flow referencing the hook id:
curl --request POST "http://gdk.test:3000/api/v4/ai/duo_workflows/workflows" \ --header "PRIVATE-TOKEN: <api token>" --header "Content-Type: application/json" \ --data '{"project_id": "<id>", "goal": "…", "workflow_definition": "developer/v1", "start_workflow": true, "environment": "web", "callback_hook_id": <hook id>, "client_reference": "run-1"}' - Watch deliveries land on webhook.site (
object_kind,version,project,client_reference, and theflow.*events). Delivery history is also logged under the webhook's Edit > Recent events. - Verify scoping: referencing a hook that is not in scope for the flow's project (e.g. a project hook from a different project) is rejected with
400.
GA readiness checklist (known follow-ups)
Deliberately out of scope for this MR; tracked here so nothing is lost:
- Feature flag for rollout (disabled by default, group actor), checked at trigger time in the workflows API.
- License/tier gating: the checkbox and the
callback_hook_idpath should be gated on Duo Agent Platform availability for the namespace (Premium/Ultimate + Duo add-on). Currently the flag is visible/settable on any EE instance, and the param is declared in the CE project hooks API while the consuming feature is EE-only — decide whether to move it to an EE API extension. - "Test" button support: the webhook UI's Test dropdown fires sample payloads per trigger (via
TestHooks::ProjectService/TestHooks::GroupService). Add aduo_flow_callbacktest trigger that sends a sampleflow.completedpayload so integrators can verify their endpoint + signature handling without running a real flow. -
flow.progressopt-in per hook: progress frames contain agent reasoning/content and are more sensitive than lifecycle pings — consider a second checkbox instead of always-on. - Auto-disable & rate limiting verification: these now apply automatically (regular hooks), but the terminal-event path goes through
WebhookDeliveryWorker/WebHookServicedirectly rather than#async_execute— verify failure accounting feeds auto-disable as expected, and that per-plan webhook rate limits apply on this path. - Granular permission (optional): configuration reuses standard webhook permissions, which is fine; revisit only if product wants "may enable Duo callbacks" separated from "may manage webhooks".
- User-facing docs: move/replace
ee/app/services/ai/messaging/adapters/webhook_integration.mdwith real docs underdoc/(webhook events page + Duo Agent Platform page). (OpenAPI docs for the new hooks API field andcallback_hook_id/client_referenceparams are already regenerated in this MR.) - Payload contract:
version: "1"is in place; document the additive-only change policy. - Minor cleanup:
CallbackHooksFinder.usable_for_projectdocstring describes a Relation/array — it returns the finder instance. - Audit events for enabling/disabling the flag (webhook update audit events may already cover this — verify).
References
- DAP Extensibility challenge: gitlab-org#22652 (work item)
- Prior exploration MR: !246770 (closed)
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.