Human-In-The-Loop pattern: This operation is fundamentally different from message/send. After posting the message, the workflow execution is suspended — no compute resources are consumed while waiting. When the target user replies, the BizFirst runtime resumes the workflow from this node's success port with the reply content. This enables durable approval and acknowledgement workflows that can wait hours or days.
When to Use
- Slack-based approvals: A manager receives a budget approval request via DM, types "approved" or "rejected", and the workflow automatically routes the request to the next step based on the reply text.
- Data validation checkpoints: An analyst receives a pipeline data summary and must confirm the numbers look correct before the workflow continues to load production data.
- Incident acknowledgement: An on-call engineer receives an alert page and must reply "ack" to prevent automatic escalation to the next responder.
- Customer confirmation: A customer-facing bot sends an order summary and awaits the customer's "confirm" reply before processing payment and fulfilling the order.
- QA sign-off gate: A test pipeline posts test results to a QA engineer and waits for "LGTM" before proceeding to the deploy stage.
Configuration
Connection
| Field | Required | Description |
botToken | Required | Bot Token in xoxb-... format. Must have chat:write scope. Store in BizFirst Credentials Manager. |
Operation
| Field | Required | Description |
channel | Required | Channel ID, channel name, or user ID (for DM). The message is sent here and the bot listens for replies in this conversation. |
text | Required | Message text. Guide the responder clearly — include expected reply options (e.g. "Reply approved or rejected"). Supports mrkdwn. |
threadTs | Optional | Post the wait message in an existing thread. The bot will listen for a reply within that thread. |
blocksJson | Optional | Block Kit JSON for rich presentation. Use Button blocks to make responses structured and typo-proof. The button action value becomes the reply text captured by the workflow. |
timeoutSeconds | Optional | Seconds to wait before the timeout port fires. Default 0 means indefinite wait. In production, always set a finite timeout — e.g. 86400 (24 hours) — so workflows do not suspend forever if the recipient does not respond. |
Sample Configuration
{
"resource": "message",
"operation": "sendAndWait",
"botToken": "{{ $credentials.slackBotToken }}",
"channel": "{{ $input.managerSlackUserId }}",
"text": "Approval required for purchase order *#{{ $input.poNumber }}* — Amount: *${{ $input.amount }}*\nRequested by: {{ $input.requesterName }}\n\nReply *approved* to proceed or *rejected* to decline.",
"timeoutSeconds": 86400,
"blocksJson": "[{\"type\":\"section\",\"text\":{\"type\":\"mrkdwn\",\"text\":\"*Purchase Order Approval Request*\\nPO: `{{ $input.poNumber }}`\\nAmount: *${{ $input.amount }}*\\nRequester: {{ $input.requesterName }}\"}},{\"type\":\"actions\",\"elements\":[{\"type\":\"button\",\"text\":{\"type\":\"plain_text\",\"text\":\"Approve\"},\"value\":\"approved\",\"style\":\"primary\"},{\"type\":\"button\",\"text\":{\"type\":\"plain_text\",\"text\":\"Reject\"},\"value\":\"rejected\",\"style\":\"danger\"}]}]"
}
Validation Errors
| Error Code | Cause |
channel_not_found | The target channel or user ID does not exist or is not accessible to the bot. |
not_in_channel | The bot is not a member of the specified channel. |
invalid_blocks | The blocksJson string is malformed. |
VAL_MISSING_CHANNEL | The channel field is empty or not provided. |
VAL_MISSING_TEXT | The text field is empty or not provided. |
Output
Waiting Port
Fires immediately after the message is sent, before any reply is received. The workflow execution is suspended at this point. Connect this port to a monitoring or audit log node to track in-flight approvals.
| Field | Type | Description |
messageTs | string | Timestamp of the sent message. Store this if you need to update or delete the message while it is in the waiting state. |
channel | string | Channel ID where the message was posted. |
status | string | "waiting" |
Success Port
Fires when a reply is received before the timeout expires. Execution resumes here with the reply content.
| Field | Type | Description |
messageTs | string | Timestamp of the original sent message. |
replyText | string | The full text of the reply message as typed by the user, or the button action value if Block Kit buttons were used. |
replyUserId | string | Slack user ID of the person who replied. |
replyTs | string | Timestamp of the reply message. |
status | string | "ok" |
errorCode | string | Empty on success. |
payload | object | Full raw Slack event payload for the reply. |
Error Port
Fires when the initial message send fails, when a timeout expires (if timeoutSeconds is set), or when the runtime cannot process the reply event. Always handle this port.
| Field | Type | Description |
status | string | "error" or "timeout" |
errorCode | string | Slack error code or "TIMEOUT" if the wait period expired. |
payload | object | Full error detail. |
Sample Output
{
"messageTs": "1716702000.045100",
"replyText": "approved",
"replyUserId": "U04MANAGER1",
"replyTs": "1716705600.088300",
"status": "ok",
"errorCode": "",
"payload": {
"ok": true,
"channel": "D04MANAGER1",
"original_ts": "1716702000.045100",
"reply": {
"user": "U04MANAGER1",
"text": "approved",
"ts": "1716705600.088300"
}
}
}
Expression Reference
| Field | Expression | Notes |
| Reply content | {{ $output.waitApproval.replyText }} | The human's response. Use in an IfCondition to branch: == "approved" vs == "rejected". |
| Replier's user ID | {{ $output.waitApproval.replyUserId }} | Who responded. Use to look up the responder's profile or write to an audit log. |
| Reply timestamp | {{ $output.waitApproval.replyTs }} | When the reply arrived. Use to calculate decision latency or for audit records. |
| Original message TS | {{ $output.waitApproval.messageTs }} | Update or delete the original approval request message after a decision is made. |
| Operation status | {{ $output.waitApproval.status }} | "ok", "waiting", or "timeout". Route to different branches with Switch node. |
Node Policies & GuardRails
| Policy Area | Recommendation |
| Always set a timeout | In production workflows, always configure timeoutSeconds. A value of 86400 (24 hours) is a safe default for business approval workflows. Without a timeout, a non-responsive approver permanently blocks the workflow execution slot. |
| Monitor the waiting port | Connect the waiting port to a log or database write node to record all in-flight approval requests. This enables ops teams to view, audit, and manually intervene on pending workflows. |
| Handle the error port | Network issues, bot disconnection, or workspace API problems can prevent reply delivery. The error port must route to an escalation or notification node, not be left disconnected. |
| Use buttons for structured responses | Free-text replies are fragile — responders may type "Approve" instead of "approved", breaking downstream IfCondition checks. Use Slack Button blocks so the button value is always a controlled string. |
| Timeout escalation | Connect the error port (with status == "timeout") to an escalation node that notifies a secondary approver or manager. Do not silently fail timed-out approvals. |
| Credential storage | Store the Bot Token in BizFirst Credentials Manager. Never expose it in expressions visible in workflow logs. |
| DM vs channel | For sensitive approval decisions, send to the approver's DM (U... user ID) rather than a channel. This prevents other channel members from seeing the approval request or influencing the decision. |
Examples
Purchase Order Approval via DM
{
"resource": "message",
"operation": "sendAndWait",
"botToken": "{{ $credentials.slackBotToken }}",
"channel": "{{ $output.lookupManager.userId }}",
"text": "Approval needed for PO #{{ $input.poNumber }} — ${{ $input.amount }}. Reply *approved* or *rejected*.",
"timeoutSeconds": 28800
}
Sends a DM to the manager (resolved by the previous user/get node) and waits up to 8 business hours for a reply. The downstream IfCondition node checks {{ $output.waitPO.replyText }} to route to the "Approved" or "Rejected" path.
Incident Acknowledgement Gate
{
"resource": "message",
"operation": "sendAndWait",
"botToken": "{{ $credentials.slackBotToken }}",
"channel": "{{ $input.oncallUserId }}",
"text": ":pager: *P2 Incident* — `{{ $input.serviceName }}` degraded in `{{ $input.region }}`.\nReply *ack* to acknowledge. No reply in 10 minutes escalates to secondary on-call.",
"timeoutSeconds": 600
}
Pages the primary on-call engineer. If no reply arrives within 600 seconds (10 minutes), the error port fires with status == "timeout", which triggers an escalation to the secondary on-call via a separate message/sendAndWait node.
Block Kit Button Approval
{
"resource": "message",
"operation": "sendAndWait",
"botToken": "{{ $credentials.slackBotToken }}",
"channel": "{{ $input.reviewerUserId }}",
"text": "Content review required for article: {{ $input.articleTitle }}",
"timeoutSeconds": 172800,
"blocksJson": "[{\"type\":\"section\",\"text\":{\"type\":\"mrkdwn\",\"text\":\"*Content Review Request*\\nArticle: *{{ $input.articleTitle }}*\\nAuthor: {{ $input.authorName }}\\n<{{ $input.previewUrl }}|Preview article>\"}},{\"type\":\"actions\",\"elements\":[{\"type\":\"button\",\"text\":{\"type\":\"plain_text\",\"text\":\"Approve & Publish\"},\"value\":\"approved\",\"style\":\"primary\"},{\"type\":\"button\",\"text\":{\"type\":\"plain_text\",\"text\":\"Request Revisions\"},\"value\":\"revisions\"},{\"type\":\"button\",\"text\":{\"type\":\"plain_text\",\"text\":\"Reject\"},\"value\":\"rejected\",\"style\":\"danger\"}]}]"
}
Presents a structured review card with three buttons. The button value (approved, revisions, or rejected) is captured as replyText in the success port, enabling a three-way branch in the downstream Switch node.