Approval Flows
Human-in-the-loop workflow execution
Some workflows need a human decision before continuing -- approving a high-value invoice, authorizing a discount, or signing off on a sensitive operation. Approval flows pause a run, wait for a person to approve or reject, then resume along the chosen path. This is implemented with the Approval block.
How Suspension Works
When a run reaches an Approval block, the execution engine moves it into the suspended state and stops advancing downstream steps. A notification with approve/reject actions is sent to the configured channel. The run stays suspended -- consuming no compute -- until an authorized approver acts or the optional timeout expires.
| Run State | Meaning |
|---|---|
running | Executing steps normally |
suspended | Paused, waiting for an approval decision or external event |
completed | Resumed and finished successfully |
failed | A step failed, or the request was rejected and the Approval step does not continue on rejection |
The Approval Lifecycle
- Reach the block -- the run reaches an Approval block and suspends
- Notify -- an interactive request is posted to the configured Slack channel with approve/reject buttons
- Wait -- the run remains
suspended(optionally with a timeout) - Decision -- an authorized approver approves or rejects
- Resume -- an approval continues the run; a rejection fails the run at the Approval step unless the workflow defines a rejected path (see Taking the rejected path)
A suspended run does not count against execution time. It can wait for minutes or days depending on your timeout configuration.
Configuring an Approval
Approval behavior is configured on the Approval block:
| Option | Type | Description |
|---|---|---|
title | string | Title shown in the approval request |
description | string | Context about what is being approved (supports templates) |
notifyChannel | string | Slack channel for the request |
approvers | array | Users authorized to approve |
timeout | string | How long to wait for a decision, for example 48h or 7d (default 7d) |
timeoutAction | string | What happens at the timeout: fail (default), reject, approve, or escalate |
The block outputs the decision so downstream steps can branch on it:
| Output | Type | Description |
|---|---|---|
decision.approved | boolean | Whether the request was approved |
decision.reason | string | The approver's comment, or Auto-rejected due to timeout |
decision.decidedBy | string | Who decided (system for a timeout) |
decision.decidedAt | string | Timestamp of the decision |
timedOut | boolean | Whether the decision came from the timeout |
Taking the Rejected Path
By default a rejection stops the run: the Approval step is recorded as failed (APPROVAL_REJECTED, or APPROVAL_TIMEOUT for a timeout auto-reject) and the run ends failed without running any later step.
To handle a rejection inside the workflow, set the Approval step's error handling to continue and branch on its decision right after it:
{ "id": "approval_gate", "type": "approval", "config": { "...": "..." },
"errorHandling": { "strategy": "continue" } },
{ "id": "check_approved", "type": "condition",
"config": {
"conditions": { "left": "{{steps.approval_gate.decision.approved}}", "operator": "eq", "right": "true" },
"then": ["send_invoice"],
"else": ["record_rejection"] } },
{ "id": "record_rejection", "type": "data_table", "terminal": true, "config": { "...": "..." } }With continue, a rejection or a timeout auto-reject (timeoutAction: reject) keeps the run going. The Approval step is still recorded as failed with the same error code, so the run history shows the rejection, and steps.approval_gate.decision holds the real decision and reason. Mark the last step of the rejected branch terminal so the run ends there instead of reaching the approved steps that follow it in the list.
The output also carries error: true and a message, as it did before rejections kept the decision, so a step that checks steps.approval_gate.error still sees the rejection.
A fallback in the error handling is used whenever the step itself fails rather than returning a decision: a delivery or configuration error, a timeout with timeoutAction: fail, an escalation that also times out, or an error saving a decision after someone acted (so even an approval can end up on the fallback). Its fields replace the step's output and an originalError field holds the error message; a real rejection never has originalError. Give the fallback decision.approved: false so these cases also stay off the approved path, and check originalError if the workflow must tell a failed request apart from a rejection.
Restarting a run from a step after an Approval step that did not succeed runs the Approval step again, and every step after it, even ones that succeeded before.
Resuming Suspended Runs
A suspended run resumes when:
- An approver acts -- clicking Approve or Reject in Slack resumes the run along the matching path
- The API is called -- approvals can be resolved programmatically via the REST API for custom approval UIs
- The timeout expires -- what happens follows
timeoutAction; withreject, the request is auto-rejected and handled like any other rejection
Without a timeout, a suspended run waits indefinitely. Set timeout and timeoutAction for time-sensitive workflows so runs cannot hang when an approver is unavailable.
Example
High-value invoice approval before finalizing:
Stripe (create invoice) -> Condition (amount > $5,000?)
-> True: Approval (finance manager review)
-> Approved: Stripe (finalize and send)
-> Rejected: Slack (notify requester)
-> False: Stripe (finalize and send)Best Practices
- Set timeouts. Prevent runs from hanging when approvers are away.
- Include context in the description. Use template variables to show amounts, customer names, and details so approvers can decide without leaving Slack.
- Notify via Slack for speed. Interactive Slack requests get faster responses than email.
- Chain blocks for multi-level approval. The block resolves on the first authorized decision; chain multiple Approval blocks for tiered sign-off.
Loopfour