LoopFour

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
    }
  }
}
FieldTypeRequiredDescription
idstringYesUnique step identifier
typestringYesMust be "approval"
namestringYesHuman-readable name
config.approversstring[]YesList of approver identifiers (email, user ID, or template)
config.reasonstringYesReason shown to approvers explaining what needs approval
config.timeoutMsnumberNoTimeout in milliseconds (default: 86400000 / 24 hours)
config.notifyChannelsobjectNoChannels 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
    }
  }
}
FieldTypeDescription
slackstringSlack channel ID or name for notifications
slackThreadTsstringThread timestamp to reply in (for threaded conversations)
emailbooleanWhether 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"
  }
}
FieldTypeDefaultDescription
inputTypestring"buttons"Input type: "buttons", "text", or "both"
inputLabelstring-Label for text input field
inputPlaceholderstring-Placeholder text for text input
approveLabelstring"Approve"Custom label for approve/submit button
rejectLabelstring"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."
  }
}
FieldDescription
decision.approvedWhether the request was approved (true) or rejected (false)
decision.approvedByIdentifier of the user who made the decision
decision.approvedAtISO 8601 timestamp of the decision
decision.reasonText 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:

  1. The step completes with a timeout status
  2. 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:

ScenarioTimeoutMilliseconds
Urgent approval1 hour3600000
Same-day approval8 hours28800000
Standard approval24 hours86400000
Extended review72 hours259200000

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

  1. Check approvers list - Ensure at least one valid approver is specified
  2. Verify channel access - Bot must have access to notification channels
  3. Check timeout - Approval may have expired

Slack Modal Not Opening

  1. Verify inputType - Must be "text" or "both" for modal
  2. Check connection - Slack connection must be valid
  3. Ensure bot permissions - Bot needs appropriate scopes

Loop Iterations Not Waiting

Each iteration within a loop creates its own approval waitpoint. Ensure:

  • Loop concurrency is set to 1 for sequential processing
  • Each iteration waits for user response before continuing

Next Steps