Portal Community

Node Configuration

{
  "nodeType": "SendSlackMessage",
  "name": "notifyEngineering",
  "config": {
    "credentialId": 12,
    "channelId": "#engineering-alerts",
    "text": "Deployment $output.triggerDeploy.version completed successfully.",
    "blocks": null,
    "username": "BizFirstAI Bot",
    "iconEmoji": ":robot_face:",
    "threadTs": null,
    "retryCount": 3,
    "retryDelayMs": 1000
  }
}

Configuration Fields

FieldTypeRequiredDescription
credentialIdintYesReferences a Slack bot token stored via ICredentialResolver
channelIdstring / exprYesChannel ID (C01234) or name (#channel). Expressions allowed.
textstring / exprYes*Plain text message body. Required if blocks is null.
blocksarray / exprNoBlock Kit JSON array. When set, overrides text as the primary payload.
usernamestringNoOverride bot display name (requires chat:write.customize scope).
iconEmojistringNoOverride bot icon (requires chat:write.customize scope).
threadTsstring / exprNoReply in thread — set to parent message ts.
retryCountintNoNumber of retry attempts on transient failure. Default: 2.
retryDelayMsintNoInitial retry delay in milliseconds. Doubles on each attempt.

Block Kit Payload

Pass a Block Kit JSON array via blocks to send rich Slack messages. The array can be a static value or a JavaScript expression building blocks from workflow data:

// Expression-built blocks
"blocks": `[
  {
    type: 'section',
    text: {
      type: 'mrkdwn',
      text: '*Approval Required:* ' + $output.createInvoice.invoiceNumber
    }
  },
  {
    type: 'section',
    fields: $output.createInvoice.lineItems.slice(0, 5).map(item => ({
      type: 'mrkdwn',
      text: '*' + item.description + '*\n$' + item.amount.toFixed(2)
    }))
  },
  {
    type: 'actions',
    elements: [
      { type: 'button', text: { type: 'plain_text', text: 'Review' }, url: $context.workDeskUrl }
    ]
  }
]`

SlackChannelAdapter — Executor

public class SlackChannelAdapter : IMessagingChannelAdapter
{
    private readonly ICredentialResolver _credentials;
    private readonly IHttpClientFactory _httpFactory;

    public string ChannelType => "Slack";

    public async Task<MessagingResult> SendAsync(
        MessagingMessage message,
        MessagingConfig config,
        CancellationToken ct)
    {
        var token = await _credentials.GetPasswordAsync(config.CredentialId, ct);
        var client = _httpFactory.CreateClient("SlackApi");
        client.DefaultRequestHeaders.Authorization =
            new AuthenticationHeaderValue("Bearer", token);

        var payload = new
        {
            channel = config.ChannelId,
            text = message.Text,
            blocks = message.Blocks,
            username = config.Username,
            icon_emoji = config.IconEmoji,
            thread_ts = config.ThreadTs
        };

        var response = await client.PostAsJsonAsync(
            "https://slack.com/api/chat.postMessage", payload, ct);

        var result = await response.Content.ReadFromJsonAsync<SlackApiResponse>(ct);
        if (!result.Ok)
            throw new MessagingException($"Slack error: {result.Error}");

        return new MessagingResult
        {
            MessageId = result.Ts,
            Channel = "slack",
            Recipient = config.ChannelId,
            SentAt = DateTimeOffset.UtcNow
        };
    }
}

Node Output

{
  "messageId": "1716638400.123456",
  "channel": "slack",
  "sentAt": "2026-05-25T10:00:00Z",
  "status": "delivered",
  "recipient": "#engineering-alerts"
}
Threading: To reply in a thread, capture the messageId (ts) from the first message's output and pass it as threadTs in subsequent nodes. This keeps related notifications grouped in a single Slack thread.
Required OAuth Scopes: Ensure the bot token has chat:write at minimum. Add chat:write.customize to override username/icon. The token is stored via ICredentialResolver — never in node config.