Approval Node
Overview
The Approval node is the human gate. It holds the incoming payload, asks a person the question you wrote, and continues the pipeline with their verdict.
Use it wherever a pipeline is about to do something a person should sign off: change a setpoint, release a batch, push a price list, act on an agent's proposal.
Core Functionality
It parks, then resumes
This node behaves differently from every other node in a pipeline, and the difference matters when you read an execution:
- The parking run. The payload becomes a pending question and the run finishes right there. Nothing goes downstream. In the execution view this looks like a run that stopped at the gate, because it did.
- The verdict run. When someone answers — or the deadline passes — the gate emits one envelope as a new execution starting from this node. That is the run in which your downstream nodes do their work.
So one approved payload produces two executions. That is expected.
One socket, branch on the verdict
The node has a single output. Both an approval and a rejection travel
down it, and the envelope says which happened. Put a Condition node
after it and branch on $node["Approve"].result.decision when the two
outcomes need different paths.
Who gets asked
audienceRoles is a comma-separated list of role names whose
notification feed the question lands in, for example
Process Engineer, Organization.Admin. Leave it empty and everyone who
can decide approvals is notified.
Where the question is answered
A parked question appears under Orchestrate → Approvals, on the
Pending tab. The person answering sees the held payload, clicks
Approve or Reject, and can add a note. Answering needs the
approval:decide permission; audienceRoles only steers notifications
and does not limit who can answer. See
Approvals for the whole
inbox, including why an approval can turn into a stale rejection when
the pipeline was saved or disabled after the question was asked.
The deadline
decisionTTL is how long the question may wait, 1h by default. Past
it the gate rejects with reason: timeout. The gate never waits forever
and it never auto-approves — a deadline that passes is a no.
Guarding against stale approvals
proposalFreshness bounds how old the proposal may be at the moment it
is approved. Set it to 5m and an approval that lands an hour later
rejects with reason: stale rather than acting on data that has moved
on. Leave it empty for no bound.
Guarding against a queue nobody drains
pendingCap caps how many undecided questions one gate may hold, 100 by
default. Past the cap a new payload is not parked: it continues
immediately with decision: rejected, reason: overflow. The pipeline
keeps moving, and the drop leaves evidence in the run rather than
disappearing.
Input / Output
Input
Whatever the upstream node produced. It is held verbatim and handed back in the envelope, so nothing is lost across the wait.
Output
The parking run emits nothing. The verdict run emits:
{
"result": {
"decision": "approved",
"reason": "",
"decidedBy": "user:8f2c…",
"note": "Checked against the batch record.",
"decisionId": "d-91a4…",
"title": "Raise setpoint on PMP-104?",
"payload": { "setpoint": 74.5 }
}
}
| Field | What it tells you |
|---|---|
decision | approved or rejected — the field to branch on. |
reason | Empty when approved. Otherwise rejected (a person said no), timeout (nobody answered in time), stale (approved too late for proposalFreshness), or overflow (the pending cap refused the park). |
decidedBy | Who decided, as a canonical subject. Empty for timeout and overflow, because nobody did. |
note | The decider's note; for stale it also carries the staleness detail. |
decisionId | The decision row this verdict answered — the audit handle. |
title | The question that was asked. |
payload | The held input, unchanged. |
Read them downstream as $node["Approve"].result.decision.
Testing in the designer
A sandbox test run does not park — you are sitting right there. The
node prompts inline on the canvas and waits for your answer, and the
envelope flows on in the same test run carrying sandbox: true. If the
node's execution timeout fires first you get rejected / timeout,
exactly the live behaviour.
Usage examples
Sign off a setpoint change
title:Raise setpoint on PMP-104?decisionTTL:4haudienceRoles:Process Engineer- Downstream: a Condition on
result.decision, with the write on the true branch.
Release a batch, but only on fresh data
proposalFreshness:10m— an approval that arrives after the batch has moved on rejects asstaleinstead of releasing the wrong batch.
Keep a busy gate honest
pendingCap:20— if twenty questions are already waiting, the twenty-first continues asrejected/overflowrather than growing a queue nobody is reading.
A single socket means the rejected path runs your downstream nodes too, unless you branch. If a rejection should do nothing, send the false branch of a Condition to a No Operation node.
The parking run and the verdict run are separate executions. When looking for what happened after an approval, open the later one.
Configuration reference
The fields below are generated from the node's config contract, so they match what the pipeline validator enforces and what the designer's form offers.
control.approval
| Field | Type | Required | Default | Values | Description |
|---|---|---|---|---|---|
title | string | yes | — | — | The question the approver is asked, e.g. "Raise setpoint on PMP-104?" — shown in the approval inbox and the decision history |
decisionTTL | string | no | 1h | — | How long the question may wait for an answer, as a duration such as "30m" or "4h"; must be positive. Past it the gate rejects with reason=timeout — it never auto-approves |
proposalFreshness | string | no | — | at least 0 | Optional bound on how old the PROPOSAL may be when approved, as a duration such as "5m": an approval landing after it rejects with reason=stale instead of executing old data. Empty = no bound |
pendingCap | integer | no | 100 | at least 1 | Maximum undecided questions this gate may hold. An overflow proposal is not parked — it continues at once with decision=rejected, reason=overflow |
audienceRoles | string | no | — | — | Comma-separated role names whose notification feeds this gate's questions land in, e.g. "Process Engineer, Organization.Admin". Empty notifies everyone who can decide approvals. A single string, not a list |