Public beta — not for production use. Data may be wiped at any time. Questions? Contact us.
Documentation menu

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 Markdown

A 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_REJECT

The 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 36453ebae3f2dfe28cdb1d61c2ddc42f

Then 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:

PolicyOutcome
CANCELThe execution ends Cancelled, with no result payload. Compensation runs if configured.
AUTO_APPROVEThe pause resumes with default_resume_value as the node's input.
AUTO_REJECTThe same, with default_resume_value as the node's input — the name is a convention; the value you set is what decides.
ESCALATEExtends 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".