Manual Approval
Pauses workflow execution until a human approves or rejects. Sends the approval request via configured Slack/Email/Telegram connections, falls back to an in-app notification if no channel is configured.
How it works
- The Approval node generates a unique approval token.
- It dispatches the request in parallel to every channel you configured (Slack, Email, Telegram). Each delivery contains an Approve and a Reject link.
- If no channel is configured (or every dispatch fails), an in-app notification appears in the bell-icon panel with inline Approve/Reject buttons.
- A human clicks Approve or Reject — from chat, email, the dashboard, or any of them; first-to-respond wins.
- The workflow resumes on the matching output handle (
approveorreject), and also onoutput, with decision data.
Configuration
| Field | Description | Notes |
|---|---|---|
| Timeout (seconds) | How long to wait for a decision before failing the node | Default 3600 (1 hour). Hard maximum 86400 (24 hours) in v1. |
| Approval Message | Message shown to the approver | Optional, templatable — e.g., Approve order #{{data.id}}?. |
| Slack Connection | Slack workspace to notify | Optional. When set, Slack Channel becomes required. |
| Slack Channel | Channel name or ID | e.g., #approvals or C0123456. Templatable. |
| Email Connection | SMTP connection to send through | Optional. When set, Recipients becomes required. |
| Recipients | Comma-separated email addresses | e.g., alice@example.com, bob@example.com. Templatable. |
| Telegram Connection | Telegram bot connection | Optional. When set, Telegram Chat ID becomes required. |
| Telegram Chat ID | Numeric chat ID or @channelname | Templatable. |
You can configure any combination — none, one, or all three. Multiple channels fire in parallel.
Output handles
Three outputs are emitted; downstream nodes can be wired to any of them.
| Handle | Fires when | Use for |
|---|---|---|
output | Always (any decision) | Default-wired downstream that needs the decision data without branching |
approve | Decision was approve | Branch for the approved path |
reject | Decision was reject | Branch for the rejected path |
Each output item contains:
| Field | Description |
|---|---|
decision | "approve" or "reject". |
comment | Optional comment submitted via the JSON API. |
decidedAt | RFC 3339 timestamp of the decision. |
token | The approval token. |
approveUrl | The absolute approve URL. |
rejectUrl | The absolute reject URL. |
message | The configured approval message (template-resolved). |
data | Pass-through of upstream input data. |
TIP
For simple "do X if approved, otherwise Y" flows, wire the approve and reject handles directly. For more elaborate branching, wire the default output and use a Switch node downstream on the decision field.
Channel delivery formats
- Slack — A message in the chosen channel with the approval message and
<approve_url|✅ Approve> • <reject_url|❌ Reject>mrkdwn links. - Email — An HTML email to the listed recipients with styled Approve/Reject buttons; subject = the approval message.
- Telegram — A message in the target chat with an inline keyboard: two URL buttons (Approve / Reject).
Per-channel delivery failures are logged but don't fail the node — as long as one channel reached a human (or the in-app fallback fired), the workflow keeps waiting.
In-app fallback
When no channel is configured, the approval appears as a notification in the bell icon with inline Approve/Reject buttons. Click either to record the decision and resume the workflow. The fallback is gated by the user's notification preferences (channel in_app, event type approval_request); the default is enabled.
WARNING
The in-app fallback fires only when zero channels are configured. If you set up Slack but the Slack workspace is offline, the workflow still waits — it doesn't fall back to in-app. Configure multiple channels for redundancy.
API endpoints
Approval URLs work for any HTTP client; they're not Slack-specific.
GET /api/approvals/{token}/{decision}— Renders an HTML confirmation page. Suitable for email/chat clicks.POST /api/approvals/{token}/{decision}— JSON API. Body:{"comment": "optional comment"}. Returns{"decision": "...", "status": "recorded"}.
{decision} must be approve or reject. Tokens are single-use; a second submission returns 409 Conflict with already decided.
TIP
The token is the credential — keep approval URLs out of public logs. Tokens are 32 random bytes, URL-safe, and expire when the timeout elapses.
Timeout behaviour
If the timeout elapses with no decision, the node fails. Use an on-error edge to handle the timeout (e.g., notify a manager, auto-reject, escalate).
Limits
- A waiting approval node holds one worker slot in the per-tenant concurrency limit. With the default 1-hour timeout this is fine; very long pending approvals (8–24h) tie up a slot.
- v1 does not survive worker restarts — if the worker process restarts while waiting, the pending approval is orphaned and the workflow times out. Restart-safe suspension is on the v2 roadmap.
- For Slack/Email/Telegram links to be clickable from external clients, the backend's
WEBHOOK_BASE_URLenv var must be set to the public URL of the API (e.g.,https://api.example.com). Local dev defaults tohttp://localhost:8000.
Example
Order requires manager approval before shipping
Webhook trigger (order received)
→ Manual Approval
timeout: 14400 (4 hours)
message: "Approve order #{{data.id}} ({{data.total}})"
slack: #approvals
email: ops@example.com
├ approve → Shopify (mark fulfilled) → Email (shipping confirmation)
└ reject → Email (notify customer, refund)When the workflow runs, the manager receives both a Slack message and an email with Approve/Reject buttons. Either click decides the approval; the workflow continues on the matching branch.