Pause a flow for a human decision
Add a human-in-the-loop pause to a node, resume it with a typed decision that becomes the node's input, and choose what happens when nobody answers in time.
View as MarkdownA node can pause its execution and wait for a person before it runs. The platform durably parks the execution — it holds no worker while it waits — and resumes it when someone submits a decision. The decision the person submits becomes that node's input, so the node runs on what the human chose rather than on what reached it beforehand.
Prerequisites: a flow with at least one published node, authored either on the
canvas or in a flow.yaml
(Push and run your first flow).
Add a pause to a node
Put a hitl: block in the node's config::
# invoice-approval.flow.yaml
nodes:
- alias: review
package: acme/approvals@1.0.0
node: RecordDecision
config:
hitl:
interrupt_payload_template: "value" # what the reviewer is shown
resume_input_schema: DecisionRequest # MUST be the node's input message
timeout_ms: 172800000 # 48 h; 0 waits forever
timeout_policy: AUTO_REJECT # CANCEL | AUTO_APPROVE | AUTO_REJECT | ESCALATE
default_resume_value: '{"approved":false}' # used by AUTO_APPROVE / AUTO_REJECTThe same fields are on the node's Human approval controls in the canvas inspector. While the pause is open, the execution's status is Paused (HITL) and its state survives worker restarts (Execution model).
The resume value becomes the node's input
resume_input_schema names the message the reviewer's decision is typed as —
and that message must be the paused node's own input message. When the
pause is resumed, the platform hands the submitted value to the node as its
input payload. There is no second channel: a node receives exactly one input,
so a resumed node receives the decision and nothing else.
Two consequences worth designing around:
- The pre-pause input is gone. Anything the node needs besides the decision has to be carried into the decision message, or read again downstream from a node that ran before the pause.
- A mismatched schema fails the run. If the submitted value cannot be decoded into the node's input message, the execution ends Failed with a message naming the resume value — it is never silently replaced by the pre-pause input.
A practical shape: make the paused node's input message carry both the
context the reviewer needs to see and the fields they fill in, and use
interrupt_payload_template to project the context part into the approval
form.
Resume from the console
Open the execution, choose the Pauses tab, and fill the form. The form is
generated from resume_input_schema, so its fields are the fields of the
node's input message. Submitting posts the decision and the execution returns
to Running.
Resume over HTTP
Resuming needs the execution ID and the pause ID. axiom executions get
prints the HITL_PAUSED event carrying the pause ID:
axiom executions get 36453ebae3f2dfe28cdb1d61c2ddc42fThen post the decision:
curl -sS -X POST \
"https://api.axiomide.com/invocations/v1/flows/36453ebae3f2dfe28cdb1d61c2ddc42f/resume" \
-H "Authorization: Bearer $AXIOM_API_KEY" \
-H "Content-Type: application/json" \
-d '{"pause_id": "01KZ5G...", "value": {"approved": true, "note": "within policy"}, "resumed_by": "alice@example.com"}'value is a JSON object whose fields are the fields of the node's input
message. See
Pause and resume a human-in-the-loop step
for the response shape, the alternative base64 encoding, and the error codes.
Choose what happens when nobody answers
timeout_policy decides the outcome once timeout_ms elapses:
| Policy | Outcome |
|---|---|
CANCEL | The execution ends Cancelled, with no result payload. Compensation runs if configured. |
AUTO_APPROVE | The pause resumes with default_resume_value as the node's input. |
AUTO_REJECT | The same, with default_resume_value as the node's input — the name is a convention; the value you set is what decides. |
ESCALATE | Extends the deadline once by escalation_extension_ms and emits an escalation event, then falls through to CANCEL on the next expiry. |
Because default_resume_value becomes the node's input on a timeout, it must
decode into the node's input message exactly like a human-submitted value.
Write it as a JSON object with the same fields.
ESCALATE sends no mail, chat message, or webhook — it only extends the
deadline and emits the event. If you want an approval gate that always
produces a typed outcome rather than a bare cancellation, prefer
AUTO_REJECT with an explicit default_resume_value that downstream nodes
can recognise as "expired without a decision".