---
title: "Pause a flow for a human decision"
description: "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."
category: guide
surfaces: [canvas, console, http-api]
related: [guides/flow-configs, guides/debug-a-flow, concepts/execution-model, reference/http-api]
last_reviewed: 2026-08-03
---

# Pause a flow for a human decision

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](../getting-started/first-flow.md)).

## Add a pause to a node

Put a `hitl:` block in the node's `config:`:

```yaml
# 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](../concepts/execution-model.md)).

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

```bash
axiom executions get 36453ebae3f2dfe28cdb1d61c2ddc42f
```

Then post the decision:

```bash
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](../reference/http-api.md#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".
