Execution Hooks
A form can route each accepted submission onward without any glue code. The
execution.on_valid_submission array holds hooks, one per target service,
and every submission carries an execution_status object recording where
each one landed.
"execution": {
"on_valid_submission": [
{
"kind": "fsm",
"fsm_name": "docs4-intake",
"target_state": "triage"
}
]
}Hooks fire once, at submission time, after validation and encryption. They
are dispatched best-effort: a hook that fails is recorded as failed and the
submission is still stored.
The four kinds
job enqueues a Job Queue task that POSTs the rendered payload to
payload_template.webhook_url. The URL is mandatory and checked at
registration - a job hook without one is rejected with
400 invalid_form: job execution hook requires a payload_template.webhook_url http(s) target,
and a bad scheme with
... is not an allowed target: outbound URL scheme is not allowed. queue
selects the queue and defaults to default. Verified end to end: a
submission produced an HTTP POST at the configured endpoint carrying the
rendered template.
fsm transitions an entity named after the submission_id into
target_state on the named FSM, so snug fsm state -n <fsm> -e <submission_id>
reports the new state right after a submission. fsm_name and target_state
are both required; if either is missing the hook records skipped.
hitl opens a Human-in-the-Loop approval request titled
Form submission <submission_id> requires approval, with
{"form_id": ..., "submission_id": ...} as its metadata so the request can
be traced back. Approvers come from payload_template.approvers; with none
listed the submitter approves their own request. The idempotency key is
derived from the submission id, so a retry reuses the existing request rather
than opening a second one.
webhook enqueues a form.submission event for the webhook named by
webhook_id. See the caveat below before relying on it.
Payload templates
payload_template is copied into the outgoing payload with two
substitutions, {{submission_id}} and {{form_id}}:
"payload_template": {
"submission": "{{submission_id}}",
"form": "{{form_id}}",
"channel": "ops"
}With no template at all the payload is
{"submission_id": ..., "form_id": ...}. The same field doubles as the hook's
configuration surface: webhook_url for job, approvers for hitl.
Approval status versus an approval hook
Two separate switches, easy to conflate:
approval.enabledon the form definition sets the submission's ownstatustopending_approvalinstead ofsubmitted, and marksexecution_status.approvalasrequested. On its own it creates no approval request anywhere.- A
hitlhook creates the real HITL request. It leaves the submission'sstatusatsubmittedunlessapproval.enabledis also set.
Set both when you want a submission that both reads as pending and has a request someone can act on.
The decision does not flow back. Approving the HITL request leaves the
submission at status: submitted (or pending_approval) with
execution_status.approval: "requested" unchanged, and no endpoint moves a
submission between statuses. Treat the HITL request as the authoritative
record of the decision and query it there.
Per-hook status
execution_status has one slot per kind - job, webhook, fsm,
approval - each null when the form configures no hook of that kind:
| Value | Meaning |
|---|---|
queued | job accepted by the Job Queue |
delivered | fsm transition applied, or webhook event enqueued |
requested | HITL request created, or approval flagged |
skipped | hook is configured but missing required fields |
failed | dispatch errored; the submission was still stored |
Retrying
snug form retry --form <form_id> --submission <submission_id> --hook webhookHook names are job, webhook, fsm, and approval (hitl is accepted as
an alias). Only the form owner or a platform admin may retry; the submitter
themselves gets 403 insufficient_permissions. A name the form does not
configure is 400 invalid_request: hook 'fsm' is not configured for this submission, and a name that is not a hook kind at all is
unknown hook 'email'. A retry re-dispatches the hook and updates that one
slot, leaving the rest of execution_status alone. Retrying a hitl hook
reuses the existing approval request rather than opening a second one -
verified by retrying and finding the request count unchanged.
Caveat on the webhook kind
execution_status.webhook: "delivered" means the event was enqueued onto the
webhook delivery stream, not that any endpoint answered. As of this writing
those events do not reach their endpoint: the form service emits event type
form.submission, which is not in the webhook service's event catalog, so
the delivery worker cannot parse it and dead-letters every one -
snug webhooks dlq <webhook_id> reports
Failed to parse event: unknown variant 'form.submission'. Until that is
reconciled, use a job hook for outbound HTTP: it delivers through the Job
Queue and was confirmed reaching its endpoint.
Reference
- Form API - hook fields on the definition, and the retry endpoint
- Targets: Job Queue, FSM, Human-in-the-Loop, Webhooks