Portal Community
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

Configuration

Connection

FieldRequiredDescription
botTokenRequiredBot Token in xoxb-... format. Must have chat:write scope. Store in BizFirst Credentials Manager.

Operation

FieldRequiredDescription
channelRequiredChannel ID, channel name, or user ID (for DM). The message is sent here and the bot listens for replies in this conversation.
textRequiredMessage text. Guide the responder clearly — include expected reply options (e.g. "Reply approved or rejected"). Supports mrkdwn.
threadTsOptionalPost the wait message in an existing thread. The bot will listen for a reply within that thread.
blocksJsonOptionalBlock 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.
timeoutSecondsOptionalSeconds 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 CodeCause
channel_not_foundThe target channel or user ID does not exist or is not accessible to the bot.
not_in_channelThe bot is not a member of the specified channel.
invalid_blocksThe blocksJson string is malformed.
VAL_MISSING_CHANNELThe channel field is empty or not provided.
VAL_MISSING_TEXTThe 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.

FieldTypeDescription
messageTsstringTimestamp of the sent message. Store this if you need to update or delete the message while it is in the waiting state.
channelstringChannel ID where the message was posted.
statusstring"waiting"

Success Port

Fires when a reply is received before the timeout expires. Execution resumes here with the reply content.

FieldTypeDescription
messageTsstringTimestamp of the original sent message.
replyTextstringThe full text of the reply message as typed by the user, or the button action value if Block Kit buttons were used.
replyUserIdstringSlack user ID of the person who replied.
replyTsstringTimestamp of the reply message.
statusstring"ok"
errorCodestringEmpty on success.
payloadobjectFull 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.

FieldTypeDescription
statusstring"error" or "timeout"
errorCodestringSlack error code or "TIMEOUT" if the wait period expired.
payloadobjectFull 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

FieldExpressionNotes
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 AreaRecommendation
Always set a timeoutIn 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 portConnect 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 portNetwork 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 responsesFree-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 escalationConnect 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 storageStore the Bot Token in BizFirst Credentials Manager. Never expose it in expressions visible in workflow logs.
DM vs channelFor 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.