When to Use
- Incident alerts: DevOps monitoring detects a P1 failure and your workflow sends a formatted alert with severity, service name, and runbook link directly to
#incidents.
- Order confirmations: An e-commerce workflow sends an order summary to a customer's DM immediately after checkout, including order number, items, and estimated delivery.
- Approval notifications: An approval workflow notifies the original requestor via DM when their request is approved or rejected, including the approver name and decision reason.
- Daily digest posts: A scheduled workflow aggregates nightly metrics (sales, active users, open tickets) and posts a formatted summary to
#analytics every morning at 8 AM.
- Cross-team coordination: An HR onboarding workflow sends a welcome message to
#engineering, #design, and #general when a new hire joins, with their role, start date, and Slack handle.
Configuration
Connection
| Field | Required | Description |
botToken | Required | Bot Token in xoxb-... format. Must have chat:write scope. Store in BizFirst Credentials Manager — never inline. |
Operation
| Field | Required | Description |
channel | Required | Destination: channel ID (C0123456789), channel name (#general), or user ID (U0123456789) for a DM. Channel IDs are preferred — names can break if the channel is renamed. |
text | Required | Message text. Supports mrkdwn: *bold*, _italic_, `code`, >blockquote, <URL|link text>, <@USERID> mentions. Used as fallback text when blocksJson is also provided. |
threadTs | Optional | Post as a threaded reply. Pass the ts timestamp of the parent message (e.g. 1716700000.123456). Omit to post a top-level message. |
username | Optional | Override the bot's display name for this single message. Requires chat:write.customize scope. |
iconEmoji | Optional | Override the bot's icon with an emoji for this message, e.g. :robot_face:. Requires chat:write.customize scope. |
blocksJson | Optional | Block Kit JSON array string for rich interactive layouts. When provided, Slack renders the blocks instead of the plain text. The text field still serves as the notification/fallback text. Validate at api.slack.com/tools/block-kit-builder before deploying. |
Sample Configuration
{
"resource": "message",
"operation": "send",
"botToken": "{{ $credentials.slackBotToken }}",
"channel": "C04X9ABCDEF",
"text": "*Alert:* Service `payments-api` is down in `us-east-1`. Severity: *P1*. <https://runbooks.internal/payments-api-down|View Runbook>",
"username": "AlertBot",
"iconEmoji": ":rotating_light:",
"blocksJson": "[{\"type\":\"section\",\"text\":{\"type\":\"mrkdwn\",\"text\":\"*:rotating_light: P1 Incident*\\nService: `payments-api`\\nRegion: `us-east-1`\\nDetected: {{ $now }}\"}},{\"type\":\"actions\",\"elements\":[{\"type\":\"button\",\"text\":{\"type\":\"plain_text\",\"text\":\"View Runbook\"},\"url\":\"https://runbooks.internal/payments-api-down\",\"style\":\"danger\"}]}]"
}
Validation Errors
| Error Code | Cause |
channel_not_found | The specified channel ID or name does not exist or is not accessible to the bot token. |
not_in_channel | The bot is not a member of the specified channel. Use channel/join first or invite the bot manually. |
invalid_blocks | The blocksJson string is malformed or contains unsupported block types. Validate at api.slack.com/tools/block-kit-builder. |
msg_too_long | Message text exceeds Slack's 40,000 character limit. |
VAL_MISSING_CHANNEL | The channel field is empty or not provided. |
VAL_MISSING_TEXT | The text field is empty or not provided. |
Output
Success Port
Fires after the message is confirmed delivered by Slack. The messageTs value is the unique timestamp/ID of the new message and is needed for update, delete, reply, and permalink operations.
| Field | Type | Description |
messageTs | string | Slack timestamp of the sent message (e.g. 1716700412.087200). Acts as the message ID — retain this for downstream update/delete/reply operations. |
channel | string | Channel ID where the message was posted (resolved to ID even if a name was provided). |
userId | string | User ID of the bot that sent the message. |
text | string | The text content of the sent message as stored by Slack. |
status | string | "ok" on success. |
errorCode | string | Empty on success. |
payload | object | Full raw Slack API response for advanced downstream use. |
Error Port
Fires when Slack rejects the request (invalid channel, permission error, malformed blocks, etc.) or when the network call fails. Always connect the error port to a logging or alerting node in production workflows.
| Field | Type | Description |
status | string | "error" |
errorCode | string | Slack error code, e.g. channel_not_found, not_in_channel. |
payload | object | Full raw Slack error response. |
Sample Output
{
"messageTs": "1716700412.087200",
"channel": "C04X9ABCDEF",
"userId": "U0LAN0Z89",
"text": "*Alert:* Service `payments-api` is down in `us-east-1`.",
"status": "ok",
"errorCode": "",
"payload": {
"ok": true,
"channel": "C04X9ABCDEF",
"ts": "1716700412.087200",
"message": {
"type": "message",
"subtype": "bot_message",
"text": "*Alert:* Service `payments-api` is down in `us-east-1`.",
"ts": "1716700412.087200",
"bot_id": "B0LAN0Z89"
}
}
}
Expression Reference
| Field | Expression | Notes |
| Message timestamp | {{ $output.sendAlert.messageTs }} | Use this to update, delete, reply to, or get the permalink of this message in a later node. |
| Destination channel | {{ $output.sendAlert.channel }} | Resolved channel ID — use this when chaining to a message/update or message/delete node. |
| Operation status | {{ $output.sendAlert.status }} | "ok" on success. Use in an IfCondition node to branch on success/failure. |
| Error code | {{ $output.sendAlert.errorCode }} | Non-empty string if the error port fired. Log this for diagnostics. |
| Bot user ID | {{ $output.sendAlert.userId }} | The bot's Slack user ID. Useful for filtering the bot's own messages in channel/getHistory. |
| Full response | {{ $output.sendAlert.payload.message.ts }} | Access raw Slack API response fields when the top-level fields do not expose what you need. |
Node Policies & GuardRails
| Policy Area | Recommendation |
| Credential storage | Store the Bot Token in BizFirst Credentials Manager and reference it via {{ $credentials.slackBotToken }}. Never hard-code the xoxb- string directly in node configuration. |
| Rate limits | chat.postMessage is Tier 3: 50+ req/min with burst. In high-volume loops, introduce a Delay node (1 second) between sends to avoid Slack's per-channel 1 msg/sec soft limit and to prevent unintentional floods. |
| Thread hygiene | Use threadTs for follow-up messages relating to the same event. Posting every update as a top-level message clutters the channel and makes tracking harder. |
| Block Kit validation | Validate all blocksJson payloads at api.slack.com/tools/block-kit-builder before deploying to production. Invalid blocks silently fall back to text in some clients but cause hard errors in others. |
| PII and secrets | Never post personally identifiable information, passwords, API keys, or internal infrastructure details in message text. Ephemeral messages (message/sendEphemeral) should be used for sensitive per-user feedback. |
| Error port handling | Always connect the error port. A disconnected error port means channel access failures, bot permission errors, and rate limit responses are silently swallowed. |
| Channel targeting | Use channel IDs rather than names in production. Channel names can be changed by workspace admins and will cause channel_not_found errors at runtime. |
Examples
Incident Alert with Block Kit
{
"resource": "message",
"operation": "send",
"botToken": "{{ $credentials.slackBotToken }}",
"channel": "C04INCIDENTS",
"text": "P1 Incident: payments-api is down in us-east-1",
"iconEmoji": ":rotating_light:",
"blocksJson": "[{\"type\":\"header\",\"text\":{\"type\":\"plain_text\",\"text\":\":rotating_light: P1 Incident\"}},{\"type\":\"section\",\"fields\":[{\"type\":\"mrkdwn\",\"text\":\"*Service:*\\n`{{ $input.serviceName }}`\"},{\"type\":\"mrkdwn\",\"text\":\"*Region:*\\n`{{ $input.region }}`\"},{\"type\":\"mrkdwn\",\"text\":\"*Severity:*\\n{{ $input.severity }}\"},{\"type\":\"mrkdwn\",\"text\":\"*Detected:*\\n{{ $now }}\"}]},{\"type\":\"actions\",\"elements\":[{\"type\":\"button\",\"text\":{\"type\":\"plain_text\",\"text\":\"View Runbook\"},\"url\":\"{{ $input.runbookUrl }}\",\"style\":\"danger\"}]}]"
}
Posts a structured incident card to #incidents with service details and a runbook button. The messageTs from the success port is stored and used by a later message/update node to change the card status to "Resolved" when the incident closes.
DM Order Confirmation
{
"resource": "message",
"operation": "send",
"botToken": "{{ $credentials.slackBotToken }}",
"channel": "{{ $input.customerSlackUserId }}",
"text": "Your order #{{ $input.orderId }} has been confirmed. Estimated delivery: {{ $input.deliveryDate }}."
}
Sends a plain-text DM directly to a customer's Slack user ID. The user ID is resolved earlier in the workflow from the customer's profile in the CRM via the user/get node using their email address.
Threaded Follow-up Reply
{
"resource": "message",
"operation": "send",
"botToken": "{{ $credentials.slackBotToken }}",
"channel": "{{ $output.postDigest.channel }}",
"text": "_Update at {{ $now }}:_ All 42 overnight jobs completed. Total records processed: *{{ $input.recordCount }}*.",
"threadTs": "{{ $output.postDigest.messageTs }}"
}
Replies in the thread of a previously sent digest message. $output.postDigest.messageTs references the timestamp returned by an earlier message/send node named postDigest. This keeps all status updates inside a single thread rather than flooding the channel.