Approval Steps
Pause workflows for human approval or input
Approval Steps
Approval steps enable human-in-the-loop (HITL) workflows by pausing execution until a designated approver takes action. Use them for scenarios requiring human judgment, data entry, or authorization before proceeding.
Configuration
{
"id": "manager-approval",
"type": "approval",
"name": "Request Manager Approval",
"config": {
"approvers": ["user@example.com"],
"reason": "Invoice #{{input.invoice_id}} for ${{input.amount}} requires approval",
"timeoutMs": 86400000,
"notifyChannels": {
"slack": "#approvals",
"email": true
}
}
}| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Unique step identifier |
type | string | Yes | Must be "approval" |
name | string | Yes | Human-readable name |
config.approvers | string[] | Yes | List of approver identifiers (email, user ID, or template) |
config.reason | string | Yes | Reason shown to approvers explaining what needs approval |
config.timeoutMs | number | No | Timeout in milliseconds (default: 86400000 / 24 hours) |
config.notifyChannels | object | No | Channels to send approval notifications |
Notification Channels
Configure where approval requests are sent:
{
"config": {
"notifyChannels": {
"slack": "#finance-approvals",
"slackThreadTs": "{{input.slackEvent.payload.event.ts}}",
"email": true
}
}
}| Field | Type | Description |
|---|---|---|
slack | string | Slack channel ID or name for notifications |
slackThreadTs | string | Thread timestamp to reply in (for threaded conversations) |
email | boolean | Whether to send email notifications |
Slack Threading
Use slackThreadTs to post approval requests as thread replies, keeping conversations organized:
{
"id": "ask-for-input",
"type": "approval",
"config": {
"approvers": ["{{input.slackEvent.payload.event.user}}"],
"reason": "Please provide additional details",
"notifyChannels": {
"slack": "{{input.slackEvent.payload.event.channel}}",
"slackThreadTs": "{{input.slackEvent.payload.event.ts}}",
"email": false
}
}
}Input Types
Control how approvers interact with the approval request:
{
"config": {
"inputType": "text",
"inputLabel": "Amount",
"inputPlaceholder": "Enter the invoice amount",
"approveLabel": "Submit",
"rejectLabel": "Cancel"
}
}| Field | Type | Default | Description |
|---|---|---|---|
inputType | string | "buttons" | Input type: "buttons", "text", or "both" |
inputLabel | string | - | Label for text input field |
inputPlaceholder | string | - | Placeholder text for text input |
approveLabel | string | "Approve" | Custom label for approve/submit button |
rejectLabel | string | "Reject" | Custom label for reject/cancel button |
Button Mode (Default)
Standard approve/reject buttons:
{
"config": {
"inputType": "buttons",
"approveLabel": "Approve Request",
"rejectLabel": "Deny Request"
}
}Text Input Mode
Collect text input from approvers via a modal:
{
"config": {
"inputType": "text",
"inputLabel": "Invoice Amount",
"inputPlaceholder": "Enter amount (e.g., 5000)",
"approveLabel": "Enter Input"
}
}When inputType is "text", clicking the approve button opens a Slack modal for text entry. The entered value is available in the step output.
Both Mode
Show both approve/reject buttons and a text input option:
{
"config": {
"inputType": "both",
"inputLabel": "Notes",
"inputPlaceholder": "Add optional notes",
"approveLabel": "Approve",
"rejectLabel": "Reject"
}
}Output
Approval steps return information about the decision:
{
"decision": {
"approved": true,
"approvedBy": "user@example.com",
"approvedAt": "2024-01-15T10:30:00Z",
"reason": "Looks good, approved."
}
}| Field | Description |
|---|---|
decision.approved | Whether the request was approved (true) or rejected (false) |
decision.approvedBy | Identifier of the user who made the decision |
decision.approvedAt | ISO 8601 timestamp of the decision |
decision.reason | Text input provided by the approver (if inputType includes text) |
Common Patterns
Amount-Based Approval Routing
Route to different approvers based on amount:
{
"steps": [
{
"id": "check-amount",
"type": "condition",
"config": {
"conditions": {
"left": "{{input.amount}}",
"operator": "gt",
"right": "10000"
},
"then": ["executive-approval"],
"else": ["manager-approval"]
}
},
{
"id": "executive-approval",
"type": "approval",
"config": {
"approvers": ["cfo@company.com"],
"reason": "High-value transaction: ${{input.amount}} requires executive approval",
"notifyChannels": {
"slack": "#executive-approvals",
"email": true
}
}
},
{
"id": "manager-approval",
"type": "approval",
"config": {
"approvers": ["{{input.manager_email}}"],
"reason": "Transaction of ${{input.amount}} requires manager approval",
"notifyChannels": {
"slack": "#manager-approvals",
"email": true
}
}
}
]
}Sequential Multi-Level Approval
Require approval from multiple levels:
{
"steps": [
{
"id": "manager-approval",
"type": "approval",
"config": {
"approvers": ["{{input.manager}}"],
"reason": "Step 1/2: Manager approval for {{input.request_type}}",
"notifyChannels": { "slack": "#approvals" }
}
},
{
"id": "check-manager-decision",
"type": "condition",
"config": {
"conditions": {
"left": "{{steps.manager-approval.decision.approved}}",
"operator": "eq",
"right": "true"
},
"then": ["director-approval"],
"else": ["notify-rejection"]
}
},
{
"id": "director-approval",
"type": "approval",
"config": {
"approvers": ["{{input.director}}"],
"reason": "Step 2/2: Director approval for {{input.request_type}}",
"notifyChannels": { "slack": "#approvals" }
}
}
]
}Data Collection in Loops
Collect user input across multiple iterations:
{
"steps": [
{
"id": "setup",
"type": "transform",
"config": {
"operation": "map",
"data": { "iterations": [1, 2, 3] }
}
},
{
"id": "collection-loop",
"type": "loop",
"config": {
"collection": "{{steps.setup.iterations}}",
"itemVariable": "iteration",
"steps": ["collect-value", "process-value"]
}
},
{
"id": "collect-value",
"type": "approval",
"config": {
"approvers": ["{{input.slackEvent.payload.event.user}}"],
"reason": "Iteration {{loop.iteration}}: Enter a value to process",
"inputType": "text",
"inputLabel": "Value",
"inputPlaceholder": "Enter a number",
"approveLabel": "Submit",
"notifyChannels": {
"slack": "{{input.slackEvent.payload.event.channel}}",
"slackThreadTs": "{{input.slackEvent.payload.event.ts}}"
}
}
},
{
"id": "process-value",
"type": "code",
"config": {
"action": "runCode",
"code": "return { value: parseFloat(input.userInput) || 0 };",
"input": {
"userInput": "{{steps.collect-value.decision.reason}}"
}
}
}
]
}Slack-Triggered Approval with Threading
Handle approvals from Slack conversations:
{
"name": "Slack Approval Workflow",
"triggerConfig": {
"type": "slack_event",
"eventType": "message",
"channelTypes": ["channel"],
"keywords": ["request approval"]
},
"steps": [
{
"id": "acknowledge",
"type": "slack",
"config": {
"action": "send_message",
"channel": "{{input.slackEvent.payload.event.channel}}",
"text": "Processing your request...",
"thread_ts": "{{input.slackEvent.payload.event.ts}}"
}
},
{
"id": "get-approval",
"type": "approval",
"config": {
"approvers": ["manager@company.com"],
"reason": "Approval requested by <@{{input.slackEvent.payload.event.user}}>: {{input.slackEvent.payload.event.text}}",
"notifyChannels": {
"slack": "{{input.slackEvent.payload.event.channel}}",
"slackThreadTs": "{{input.slackEvent.payload.event.ts}}"
}
}
},
{
"id": "respond",
"type": "slack",
"config": {
"action": "send_message",
"channel": "{{input.slackEvent.payload.event.channel}}",
"text": "Your request was {{#if steps.get-approval.decision.approved}}approved{{else}}rejected{{/if}} by {{steps.get-approval.decision.approvedBy}}",
"thread_ts": "{{input.slackEvent.payload.event.ts}}"
}
}
]
}Timeout Handling
When an approval times out:
- The step completes with a timeout status
- The workflow can handle this in subsequent condition steps
{
"steps": [
{
"id": "request-approval",
"type": "approval",
"config": {
"approvers": ["approver@company.com"],
"reason": "Urgent: Requires response within 1 hour",
"timeoutMs": 3600000
}
},
{
"id": "check-response",
"type": "condition",
"config": {
"conditions": {
"left": "{{steps.request-approval.decision.approved}}",
"operator": "exists"
},
"then": ["process-decision"],
"else": ["handle-timeout"]
}
}
]
}Best Practices
Clear Approval Reasons
Provide context so approvers can make informed decisions:
{
"reason": "Invoice #{{input.invoice_id}} from {{input.vendor_name}} for ${{input.amount}} (due: {{input.due_date}})"
}Appropriate Timeouts
Set timeouts based on urgency:
| Scenario | Timeout | Milliseconds |
|---|---|---|
| Urgent approval | 1 hour | 3600000 |
| Same-day approval | 8 hours | 28800000 |
| Standard approval | 24 hours | 86400000 |
| Extended review | 72 hours | 259200000 |
Thread Continuity
For Slack-triggered workflows, maintain thread context:
{
"notifyChannels": {
"slack": "{{input.slackEvent.payload.event.channel}}",
"slackThreadTs": "{{input.slackEvent.payload.event.ts}}"
}
}Troubleshooting
Approval Not Received
- Check approvers list - Ensure at least one valid approver is specified
- Verify channel access - Bot must have access to notification channels
- Check timeout - Approval may have expired
Slack Modal Not Opening
- Verify inputType - Must be
"text"or"both"for modal - Check connection - Slack connection must be valid
- Ensure bot permissions - Bot needs appropriate scopes
Loop Iterations Not Waiting
Each iteration within a loop creates its own approval waitpoint. Ensure:
- Loop
concurrencyis set to1for sequential processing - Each iteration waits for user response before continuing