Loopfour
Blocks

Approval block

Configure the Approval block: approvers, channels, timeout and escalation, plus the API call that records a decision on a request.

The Approval block (type: "approval") pauses the run until a named approver approves or rejects the request, then continues or stops. For the plain-language guides, see Add an approval step and Approve or reject a request.

When to use it

  • Sign-off on a payment, a vendor bill over a limit, or a journal entry before it is posted.
  • A review point after an AI Agent step, before anything is written to the ledger.

While the step waits, the run's status is waiting_approval and the request appears in Studio under Approvals.

Configuration

Field (config key)Editor labelTypeRequiredMeaning
approversApproversstring arrayYesEmail addresses of the people who may decide. The editor takes a comma-separated list.
reasonReasonstringYesText shown to approvers in every channel. Template references are resolved, for example Approve bill {{steps.extract.structured.billNumber}}.
notifyChannelsNotification channelsarrayNoHow approvers are told. See below. With no channel, nobody is notified and the request is decided in Studio or by API.
timeoutTimeoutstringNoHow long to wait: a number and d, h, m or s, such as 7d or 24h. Default 7d.
timeoutActionNot shownstringNoWhat happens at the timeout: fail (default), reject, approve or escalate.
escalateToEscalation Approversstring arrayFor escalateWho receives the request when it times out with timeoutAction: "escalate".
escalationTimeoutNot shownstringNoHow long the escalated request waits. Defaults to timeout.
gracePeriodNot shownstringNoExtra time after the timeout before timeoutAction applies, such as 2h.
remindersNot shownobjectNo{ "before": ["24h", "1h"], "channels": ["email"] }: reminders sent that long before the timeout. Channels are email (default) and slack.
contextNot shownobjectNoLabel and value pairs shown to approvers, such as { "Vendor": "{{input.vendorName}}" }.
approveLabel, rejectLabelNot shownstringNoButton text. Defaults Approve and Reject.
decisionMessageSlack message after decisionstringNoreplace (default) swaps the Slack request for a short decision note. keep_request keeps it, removes the buttons and adds the decision.
decisionMessageTextText added after decisionstringNoText added to the end of the Slack message after a decision.
requireAllRequire All ApproversbooleanNoReserved. Ignored today: the first decision from any approver applies.

Escalation Approvers has an effect only with timeoutAction: "escalate", which the editor does not set. Set it in the workflow definition. Escalation goes one level: if the escalated request also times out, the step fails.

Notification channels

ChannelShapeBehaviour
Email{ "type": "email", "instructions": "...", "subject"?, "bodyHtml"?, "sender"? }One email per approver. instructions tells the AI how to write the email. A subject or bodyHtml you set is used instead of the AI draft. sender overrides the from address.
Slack{ "type": "slack", "channel": "C12345678", "threadTs"? }One request posted in the channel. Only the listed approvers can approve or reject from Slack.

If channels are set and the request reaches no approver, the request is closed and the step fails, naming why.

Decisions

  • The first decision from any approver applies; other approvers' links stop working.
  • In Studio, a person who is not a listed approver cannot decide, except a company owner or admin. Their decision is recorded as an override in the Audit trail.
  • Approve: the step succeeds and the run continues.
  • Reject: the step fails and the run stops with Approval step "<name>" was rejected. To branch on a rejection instead, set "errorHandling": { "strategy": "continue" } on the step and test decision.approved in a later Condition.
  • Timeout: fail fails the step; reject records an automatic rejection by system; approve records an automatic approval by system; escalate sends a new request to escalateTo.

Output

FieldTypeMeaning
decision.approvedbooleantrue when approved.
decision.decidedBystringThe approver's email, api_key:<id> for an API decision, or system for a timeout action.
decision.decidedAtstringISO timestamp of the decision.
decision.reasonstringThe comment given with the decision.
timedOutbooleantrue when the timeout decided the step.
durationMsnumberTime from request to decision.
approvalobjectThe approval record, including id, status, approvers and requestedAt.

Read the decision as {{steps.<step id>.decision.approved}}. The editor's output list shows approved, approvedBy, approvedAt and comment; the executor returns the fields above.

Example

{
  "id": "controller_approval",
  "type": "approval",
  "name": "Controller approves payment run",
  "config": {
    "approvers": ["controller@example.com", "cfo@example.com"],
    "reason": "Payment run of {{steps.build_run.result.total}} across {{steps.build_run.result.count}} vendors",
    "context": { "Pay date": "{{input.payDate}}" },
    "timeout": "2d",
    "timeoutAction": "escalate",
    "escalateTo": ["cfo@example.com"],
    "escalationTimeout": "1d",
    "reminders": { "before": ["4h"], "channels": ["email"] },
    "notifyChannels": [
      { "type": "email", "instructions": "Write a short, plain email asking the approver to review this payment run." },
      { "type": "slack", "channel": "C12345678" }
    ]
  }
}

Record a decision by API

Find pending requests with GET /api/v1/approvals (workflows:read scope). With no status filter it returns pending requests; each item's id is the approval id. Then post the decision:

curl -X POST https://workflow.loopfour.ai/api/v1/approvals/<approval id>/decide \
  -H 'x-api-key: wfk_live_your_api_key' \
  -H 'Content-Type: application/json' \
  -d '{ "approved": true, "reason": "Checked against the remittance" }'

The key needs the approvals:decide scope (workflows:write is also accepted). An API decision is recorded with decidedBy set to api_key:<key id>.

{
  "success": true,
  "data": {
    "approvalId": "3f6c2b1e-8a4d-4f1b-9c7e-2d5a6b8c9e01",
    "status": "approved",
    "decidedAt": "2026-11-12T17:04:11.000Z",
    "decidedBy": "api_key:7b1d…",
    "reason": "Checked against the remittance"
  }
}
Statuserror.codeWhen
404APPROVAL_NOT_FOUNDNo approval with this id in your company.
400APPROVAL_ALREADY_DECIDEDThe request is no longer pending.
400APPROVAL_EXPIREDThe decision deadline has passed.
403NOT_AN_APPROVERA signed-in user who is not an approver, owner or admin.
409RUN_NOT_WAITINGThe run has ended, or the decision could not be recorded.
500APPROVAL_ERRORThe decision was recorded but the run could not be resumed.

See Runs API for the run endpoints.

On this page