Skip to main content

Human Approval Node

Flow node

The Human Approval node pauses the workflow run per input row, asks a human to approve or reject, and routes the row to the matching output port when they respond. It sits in the Decision group.

When to use

  • Rows need a human decision before they proceed downstream (refunds above a threshold, offboarding events, terms updates).
  • The human is not in the DataLug UI: reach them on Slack, Teams, or email; the reply lands back in the workflow.
  • The wait is bounded — the run should time out and route the row to the timeout port if the human doesn't respond in a configurable window.

Ports

PortDirectionMeaning
ininRow waiting on human approval.
pendingoutEmits every row waiting on a decision. When a real approval backend is registered, the executor throws SuspendSignal and the run transitions to SUSPENDED until the human responds.
approvedoutRows are materialized into this port's downstream queue on Approve.
rejectedoutRows are materialized into this port's downstream queue on Reject.
timeoutoutRows are materialized here after expiresAt passes without a response.
erroroutRows the executor could not register (backend down, unknown target, etc.).
notifyoutOne control-plane row per registered approval (per_row) or one summary row (per_batch). Chain a Notification node downstream to fan out "an approval was raised" notifications.

Configuration

FieldDefaultMeaning
targetwebWhere to deliver the approve/reject URLs: slack | teams | discord | email | web.
targetAddrColumnRow column carrying the delivery address (Slack handle, email, etc.).
messageTemplate{{ column }} template rendered per row for the human message.
rowKeyColumnRow column carrying the row identity string used for audit + display.
ttlSeconds86400Row-level TTL. Row auto-times-out after this many seconds. Capped server-side at 30 days.
batchModeper_rowper_row creates one approval per input row. per_batch creates ONE approval for the whole batch; resolution routes ALL rows to the resolved port together.

Signed callback URLs

Each ApprovalRequest row carries a per-request secret. The register response includes signed URLs:

POST /api/approvals/<approvalId>/approve?t=<token>
POST /api/approvals/<approvalId>/reject?t=<token>

Token format: t=<issuedAtUnix>.<verb>.<hex-hmac> where the HMAC input is ${approvalId}.${verb}.${issuedAtUnix} and the key is the row's secret. Verb is pinned into the signature so an "approve" URL cannot be tampered into a "reject". A callback whose now > ApprovalRequest.expiresAt fails with expired regardless of the token being otherwise valid.

Row output shape (pending)

ColumnNotes
approval_idThe persisted ApprovalRequest id, or null when no backend registrar is wired (dev mode).
approval_statusInitially set to Pending. Flips to Approved / Rejected / TimedOut on the matching port when a human responds.
approval_targetNormalized target (slack / teams / email / web).
approval_messageTemplate-substituted message.
approval_approve_urlSigned callback URL forwarded to the human for approve.
approval_reject_urlSigned callback URL for reject.

Failure modes

ReasonWhere the row lands
registerApproval returns { ok:false, error }error port with the backend's reason
registerApproval throwserror port with the thrown message

Delivery pipeline

The register endpoint fires an async delivery based on the request's target. A routing seam supports per-channel adapters:

TargetAdapterNotes
web(none)Owner/Admin resolves from Account -> Manage -> Config -> Human Approvals.
slacksendSlackDm({ to, text, approval })Requires the Slack bot connector.
teamssendTeamsMessage({ to, text, approval })Uses the Teams incoming webhook.
emailsendEmail({ to, subject, text, approval })Uses the existing SMTP service.

When the corresponding sender is null, the register response still returns the approve/reject URLs (the row is still persisted); the delivery step returns { ok:false, error:"<channel>_sender_not_configured" } — the operator wires the adapter when they configure that channel.

Auto-timeout

A 60-second interval calls the timeout scheduler which flips any waiting row where expiresAt < now to TimedOut and signals the runtime with the timeout status. The operator can tune the row-level TTL from the inspector's slider (default 24 h, max 7 days) or from config.ttlSeconds.

Web review page

Two entry points list waiting / approved / rejected / timed-out rows:

  • Account -> Manage -> Config -> Human Approvals - embedded card.
  • /account/approvals - standalone page with breadcrumb + filter controls for status, projectId, runId, and date range.

Owner/Admin can approve or reject inline with POST /api/account/approvals/:id/resolve { decision: "approve" | "reject" }. The DataLug UI is a real alternative to clicking a signed URL in Slack.

A Resend button on the WebApprovalsPanel row list retries delivery via POST /api/account/approvals/:id/resend-delivery (Owner/Admin). Each resend records a new ApprovalDelivery attempt so failed deliveries are traceable.

Delivery ledger

Every delivery attempt is persisted to the ApprovalDelivery table with { channel, status, messageId, error, attemptedAt }. Success rows also populate ApprovalRequest.deliveryMessageId so operators can correlate with Slack threads / email Message-Ids. Failed rows carry the underlying error string (channel_not_found, smtp_not_configured, etc.) so the operator can debug from the review UI without digging through server logs.

Suspend / resume state machine

The runtime suspends and resumes as follows:

  1. Executor throws SuspendSignal after the backend registers at least one approval.
  2. Worker's outer catch snapshots the run's inputs map, persists a SuspendCheckpoint under <checkpointDir>/<runId>.suspend.json, and transitions the run's status to SUSPENDED.
  3. Backend callback (approve / reject / timeout / web-resolve) fires notifyRuntimeApprovalResolved -> runtime's POST /approvals/resolved.
  4. Runtime endpoint loads the checkpoint, injects the resolved row (with approval_status populated) into the correct downstream port's queue, and persists the updated checkpoint.

Actual re-execution of the SUSPENDED run (draining the queued rows through the rest of the topology) is a downstream ops refinement; the checkpoint captures everything needed for a controller to pick it up.

Example flow fixture

qa/fixtures/approval-node-example-flow.json — source (expense reports) -> AI classify (auto vs review) -> router -> approval (email target, approverEmail column) -> load (accounting).