Source:
apps/core-api/src/modules/automation-hooks and apps/core-api/src/workers/hook-delivery-worker.ts.
Events
HOOK_EVENTS in automation-hooks.schema.ts has one entry today, FORM_FILLED. The event column is a string and the lane is generic. Adding an event is a new value in that list plus a call to a dispatcher from wherever the event happens.
The subscription name and the payload name differ in case. You subscribe to
FORM_FILLED, the header says FORM_FILLED, and the payload’s event field is form_filled.Registered hooks
Register
string
default:"FORM_FILLED"
One of
HOOK_EVENTS.string
required
Where deliveries go. Up to 2048 characters. Must be
https on a public host.string
Leave empty to receive every form. Set it to receive only that form. An id that is not in the studio is
NOT_FOUND.secret is 24 random bytes as hex. It is returned once, here, and never again, the way an API key is. Store it.
Rules:
- A key can hold at most 50 hooks (
MAX_HOOKS_PER_KEY). The next one fails withBAD_REQUESTand a message telling you to delete one first. - Registering and removing a hook are audited as
automation.hook.registerandautomation.hook.remove, because a webhook sends studio data to an address a partner chose.
The target URL fence
A hook URL is attacker-supplied as far as the server is concerned, and it is the server that makes the request.hookTargetUrl refuses anything that is not https, and any host matching localhost, 127., 10., 192.168., 169.254., 172.16. through 172.31., the IPv6 loopback, or a .local or .internal name.
List and remove
Hooks are scoped to the key that registered them. One key cannot list or delete another key’s hooks, even in the same studio. Deleting a
StudioApiKey row cascades to its hooks.
A row in AutomationHook is a live subscription, not a setting a coach maintains. Make creates it when a scenario’s instant trigger is switched on and deletes it when it is switched off.
The form_filled payload
Built by buildFormFilledPayload in automation-hooks.dispatcher.ts.
fields exists so a receiver can map an answer straight off the trigger with its label, without following ids back with a second call. It is built from the schema snapshot stored on the response, so it reflects the form as the trainee saw it.
For a signed document, response.answers holds every value: the coach-filled fields, the trainee’s fields and the signature image URLs.
The flow webhook payload
Sent bywebhookEffect in task-automations/flow-runtime.ts when a flow run reaches a webhook node.
Headers:
Content-Type: application/json and X-Perform-Event: automation_flow. There is no signature and no hook id, because there is no hook row and no stored secret. The URL was validated when the flow was saved, and the payload holds nothing the studio did not put in its own flow. A receiver that needs authentication should put an unguessable token in the URL.
The FlowNodeExecution ledger makes sure a node is enqueued once per run, even across crash retries.
Delivery
Dispatch is best effort
dispatchFormFilled is called from forms.wiring.ts after the submission is saved, without awaiting it. No webhook is worth failing a trainee’s submission for. If queueing one delivery fails, it is logged and the others continue.
Matching hooks are those in the studio, active, subscribed to the event, in a studio that is not archived, and either unscoped or scoped to that form.
Request
Timeout: 10 seconds (
TIMEOUT_MS). The response body is never read. It is cancelled so the socket is released.
Retries
Every failed attempt increments
failureCount and stamps lastStatus.
failureCount is recorded, and active exists on the row, but no code was found that switches a hook to inactive after repeated failures. A dead endpoint keeps receiving attempts for each new event until its hook is deleted.submissionNumber and occurredAt if order matters.
Verifying a delivery
Compute the HMAC over the raw body bytes, before parsing JSON. Parsing and re-serializing changes the bytes and the signature no longer matches.- Answer 2xx quickly and do the work afterwards. Anything slower than 10 seconds is a timeout and a retry.
- Treat deliveries as at-least-once. A timeout after the receiver already processed the event leads to a second delivery. Deduplicate on
response.id. - Answer 4xx only when the hook should stop receiving that delivery for good.
response.id covers that.
Testing
- Register a hook with an empty
formTemplateId. - Confirm it with
GET /v1/automation/hooks. - Send yourself a form from the coach web app and fill it in the trainee app.
- Check
lastDeliveryAtandlastStatuson the hook.