MINARA

Slack

Bot-token mode unlocks the full Slack Web API (streaming, files, reactions, ephemeral, scheduled). Webhook mode is a one-URL fallback that still supports plain text, Block Kit blocks, and threaded replies.

🟢 Runtime-ready — two modes picked automatically from which env vars are set. Bot-token mode is recommended for any non-trivial use; webhook mode is kept as a one-URL fallback that still covers text / Block Kit blocks / threaded replies but not streaming, attachments, reactions, ephemeral delivery, scheduled delivery, or the metadata field.

Pick a mode

ModeEnv varsStreamingAttachmentsThreadsReactionsBlocks (Block Kit)Ephemeral / Scheduled / MetadataInboundBest for
Bot token (recommended)SLACK_BOT_TOKEN + SLACK_CHANNEL_IDFull-capability integration, multi-channel routing, inbound listener
Webhook (no-Slack-app fallback)SLACK_WEBHOOK_URLPlain text, Block Kit, threaded replies — no Slack app approval needed

What webhook mode can and can't do: Per Slack's Incoming Webhooks docs, a webhook URL accepts text, blocks, thread_ts, mrkdwn, unfurl_links, and unfurl_media in the JSON body. It does NOT expose the edit endpoint (no streaming), files.upload (no attachments), reactions.add, chat.postEphemeral, chat.scheduleMessage, or metadata — those are bot-token-only Slack Web API methods. This isn't a Minara limitation; no Slack SDK can work around it.

Priority when both env sets exist: Minara picks bot mode first (SLACK_BOT_TOKEN + SLACK_CHANNEL_ID). Webhook is only used when bot credentials are absent or incomplete. Drop either bot env var to fall back to webhook mode.

1. Create the bot

  1. Open api.slack.com/appsCreate New AppFrom scratch → name it "Minara" → pick your workspace
  2. Under OAuth & Permissions, add these Bot Token Scopes:
    • chat:write — required for chat.postMessage / chat.postEphemeral / chat.scheduleMessage
    • chat:write.public — required to post to channels the bot hasn't been invited to
    • files:write — required for attachment uploads via files.v2
    • reactions:write — required for the add_reaction tool
  3. Click Install to Workspace at the top and copy the Bot User OAuth Token (starts with xoxb-)

2. Find the channel id

In the Slack client: click the channel name → scroll to the bottom → copy the Channel ID (e.g. C0123ABC).

3. Configure Minara

Make sure SLACK_WEBHOOK_URL is unset in your project .env file (it's fine to leave configured too — bot mode wins priority — but cleaner to drop it so the active mode is obvious). Then:

SLACK_BOT_TOKEN=xoxb-...
SLACK_CHANNEL_ID=C0123ABC

4. Test

minara auth messaging test slack

Setup — Webhook mode (fallback)

Prefer bot-token mode. Use webhook only when you can't get a Slack app approved (personal workspaces / restricted enterprise plans) or need absolute minimum setup for a one-shot text alert channel.

1. Create an Incoming Webhook

  1. api.slack.com/appsCreate New AppFrom scratch → name it "Minara" → pick your workspace
  2. Left nav → Incoming Webhooks → toggle the feature On
  3. Add New Webhook to Workspace → pick the channel → Allow
  4. Copy the webhook URL (https://hooks.slack.com/services/T00/B00/xxx)

2. Configure Minara

SLACK_WEBHOOK_URL=https://hooks.slack.com/services/T00/B00/xxx

The channel is baked into the URL — SLACK_CHANNEL_ID is ignored and the channel override on send_message is rejected with a clear error. Webhook mode DOES support threads, Block Kit blocks, mrkdwn, and unfurl toggles; it does NOT support attachments, ephemeral delivery, scheduled delivery, or metadata.

Streaming behaviour (bot mode only)

Slack's chat.update is on their Tier-3 rate limit (~50 / minute). Minara throttles edits at 1 200 ms — comfortably under the ceiling and fast enough to feel responsive. Max message length is 40 000 characters; you'll almost never hit it in practice.

Rich messaging — Block Kit, ephemeral, scheduled, metadata (bot mode)

send_message on bot-mode Slack accepts a provider_options.slack object that routes directly to the corresponding Slack Web API fields / endpoints. The core text / channel / thread fields compose with provider_options.slack on the same call.

attachments is the one exception. Attachments route through Slack's Files v2 flow (files.getUploadURLExternalfiles.completeUploadExternal), which only accepts initial_comment (populated from text) and thread_ts — nothing else. Passing blocks / mrkdwn / unfurl_* / metadata / ephemeral_user / schedule_at together with attachments is rejected at the tool boundary. Workaround: post the rich message first (get the messageId), then upload the file into that thread as a follow-up.

Block Kit rich formatting

Slack's Block Kit is the standard rich-message format — headers, sections, dividers, context, fields, images. blocks is an array of block objects that goes straight into chat.postMessage's blocks param. text stays as the plain-text fallback (mobile notifications, screen readers, accessibility tools).

send_message({
  provider: "slack",
  text: "BTC -5.1% on 1h",  // fallback — shown when blocks can't render
  provider_options: {
    slack: {
      blocks: [
        { type: "header", text: { type: "plain_text", text: "Price alert" } },
        { type: "section", text: { type: "mrkdwn", text: "*BTC* dropped *5.1%* in the last hour" } },
        { type: "divider" },
        {
          type: "context",
          elements: [
            { type: "mrkdwn", text: "_Source: Minara · 1h · $68,450_" },
          ],
        },
      ],
    },
  },
})

Iterate on block layouts in Slack's Block Kit Builder; paste the resulting JSON into blocks directly.

Ephemeral messages — only visible to one user

Set ephemeral_user to a Slack user id (e.g. U012ABC) to route the send to chat.postEphemeral. The message shows up only for that user and disappears when they reload Slack. Useful for per-user confirmations or responses to slash commands in channels.

send_message({
  provider: "slack",
  text: "Your position is under 1% of portfolio — auto-trade skipped.",
  provider_options: { slack: { ephemeral_user: "U012ABC" } },
})

chat.postEphemeral argument support is a strict subset of chat.postMessage. Per Slack's documented arg list, the ephemeral endpoint does NOT accept mrkdwn, unfurl_links, unfurl_media, or metadata — only text / blocks / thread_ts / attachments / standard auth args. Passing any of the missing fields alongside ephemeral_user is rejected at the tool boundary with a specific error; it's not a silent drop. Ephemeral sends are also incompatible with Minara's attachments (Slack's files.v2 flow doesn't expose an ephemeral hook) and with schedule_at (you can't schedule an ephemeral).

Scheduled messages

Set schedule_at to a unix-seconds timestamp to route the send to chat.scheduleMessage. Slack caps scheduling at 120 days in the future; Minara enforces the same at the tool boundary.

send_message({
  provider: "slack",
  text: "Weekly review — check the dashboard before stand-up",
  provider_options: {
    slack: { schedule_at: Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60 },
  },
})

The returned message_id is the Slack scheduled_message_id — pass it to chat.deleteScheduledMessage via a future tool if you need to cancel. Mutually exclusive with ephemeral_user.

metadata is not supported on scheduled messages. Slack's chat.scheduleMessage docs state that scheduled messages with a metadata parameter "will not post". Minara rejects the combination at the tool boundary so you don't get a scheduled_message_id for a send that will silently never deliver.

Control Slack's default parsing behaviour per-message:

  • mrkdwn: false — disable markdown expansion on text (send literal *not-bold*).
  • unfurl_links: false — suppress in-message link previews (useful for high-frequency alerts where each one would get a big preview card).
  • unfurl_media: false — suppress rich media previews.

metadata — machine-readable context

Bundle a structured JSON payload with the message (Slack max 8KB). Not shown in the UI; useful when your inbound handler wants the raw data the LLM reasoned over, not just the rendered text.

send_message({
  provider: "slack",
  text: "BTC dropped 5%",
  provider_options: {
    slack: {
      metadata: {
        event_type: "price_alert",
        event_payload: { symbol: "BTC", pct: -5.1, ts: Date.now() },
      },
    },
  },
})

Overriding the channel (bot mode only)

send_message({
  provider: "slack",
  channel: "C9876XYZ",
  text: "Critical: position liquidation imminent",
})

The bot must be a member of the override channel, or have the chat:write.public scope.

Troubleshooting

"Webhook URL is disabled"

  • Slack disabled the webhook because the app was removed from the workspace, or the webhook was manually revoked in app settings
  • Recreate the webhook and replace SLACK_WEBHOOK_URL

"channel_not_found" (bot mode)

  • Bot isn't a member of the channel. Invite /invite @your-bot in the target channel, or add the chat:write.public scope

"not_authed" / "invalid_auth"

  • SLACK_BOT_TOKEN missing or wrong. Bot tokens start with xoxb-; user tokens (xoxp-) won't work — Slack API rejects them for chat.postMessage

"Streaming not working"

  • You're probably in webhook mode. Check SLACK_BOT_TOKEN + SLACK_CHANNEL_ID are both set

"invalid_blocks" / "missing_scope" (when using Block Kit)

  • Slack's API validates Block Kit against its own JSON schema — Minara doesn't replicate that schema check. Iterate with Block Kit Builder to find the exact offending block
  • missing_scope usually means you're missing files:write (for attachments) or reactions:write (for add_reaction) — re-install the app after adding scopes

Reference

On this page