brain/wiki/eventing-webhooks/event-destinations.md
Streams platform/project activity events to webhook URLs in real time. Internal Activepieces flow webhooks are valid targets, so operators can route events into a flow and fan out to Slack/Gmail/Teams/HTTP without leaving the platform. EE/Cloud only, gated by auditLogEnabled (shares audit-log gating). Lives under the Observability sidebar group.
ApplicationEventName events, at PLATFORM or PROJECT scope.FLOW_RUN_FINISHED).WorkerJobType.EVENT_DESTINATION).eventDestinationService.trigger() finds matching destinations and classifies each URL as internal vs external./v1/webhooks/, suffix '' or /sync): dispatched directly via webhookService.handleWebhook — no outbound HTTP, so the SSRF filter never sees a self-referential private-IP call (GIT-1539). /draft and /test URLs stay on the outbound path.safeHttp (SSRF-protected); delivery failures logged at error level.POST/GET/DELETE /v1/event-destinations (+ POST /:id), and POST /test sends a mock event (defaults to FLOW_CREATED) built by buildMockEvent().assertUrlIsExternal check was removed to support internal handler flows; recursion is instead prevented by a server-side cycle guard keyed on the target flow of any webhook URL, regardless of route suffix (so a self-targeting /draft destination is still dropped on its own run events)./v1/webhooks/<flowId>, percent-decoding the id segment. It used to require destination.origin === getPublicApiUrl().origin, which is AP_FRONTEND_URL: platforms with an embed subdomain get WEBHOOK_URL_PREFIX rewritten to https://<embed-hostname>/api/v1/webhooks, so the origin never matched, the guard failed open, and a handler flow re-triggered on its own runs — one webhook POST produced 838 runs in ~3 min (GIT-1641, Pylon 5378). domainHelper.getPublicApiUrl() is called with no platformId here, so it can never resolve to the embed host. The path is the stable part (/v1/webhooks is fixed by the route registration and every URL builder); the host is not./v1/webhooks/...) is also dropped for the looping flow's events; the droppedDestinations field on the cycle-break warn is how you see that happened.FLOW_RUN_STARTED, Y→X on FLOW_RUN_FINISHED) never put an emitter-targeting destination in the same event's candidate list, so the guard never fires and the loop survives. URL matching cannot close this; a hop/depth counter on the dispatch chain would.matchInternalWebhookFlowId (origin-strict, prefix-based, does not percent-decode), extractWebhookFlowIdCandidate (host-agnostic, marker-based, decodes), and webhookRouteSuffix. They can disagree on the same URL. Dispatch classification stays origin-strict deliberately, so cross-instance chaining still delivers over HTTP outside cycle events.useLiveQuery), not standard React Query.Entry point: eventDestinationService, a log-scoped factory wired up in platform-webhooks.module.ts under the /v1/event-destinations prefix.
packages/server/api/src/app/event-destinations/ — the service (trigger, internal vs external routing, cycle guard) and the typeorm entitypackages/server/api/src/app/ee/platform-webhooks/ — the HTTP controller and module that expose the endpointspackages/server/worker/src/lib/execute/jobs/event-destination.ts — worker side of the BullMQ job, POSTs external destinations via safeHttppackages/core/shared/src/lib/ee/event-destinations/ — request/response zod schemas and barrel exportpackages/core/shared/src/lib/ee/audit-events/ — the ApplicationEventName enum plus buildMockEvent() for test deliveriespackages/web/src/app/routes/platform/infra/event-destinations/ — the page, its TanStack DB collection, handler-flow builder, and dialog/row/action componentspackages/server/api/test/integration/cloud/event-destinations/ — integration tests covering CRUD and trigger dispatchPaths verified 2026-07-17.