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
metadatafield.
Pick a mode
| Mode | Env vars | Streaming | Attachments | Threads | Reactions | Blocks (Block Kit) | Ephemeral / Scheduled / Metadata | Inbound | Best for |
|---|---|---|---|---|---|---|---|---|---|
| Bot token (recommended) | SLACK_BOT_TOKEN + SLACK_CHANNEL_ID | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | Full-capability integration, multi-channel routing, inbound listener |
| Webhook (no-Slack-app fallback) | SLACK_WEBHOOK_URL | ❌ | ❌ | ✅ | ❌ | ✅ | ❌ | ❌ | Plain 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.
Setup — Bot-token mode (recommended)
1. Create the bot
- Open api.slack.com/apps → Create New App → From scratch → name it "Minara" → pick your workspace
- Under OAuth & Permissions, add these Bot Token Scopes:
chat:write— required forchat.postMessage/chat.postEphemeral/chat.scheduleMessagechat:write.public— required to post to channels the bot hasn't been invited tofiles:write— required for attachment uploads viafiles.v2reactions:write— required for theadd_reactiontool
- 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=C0123ABC4. Test
minara auth messaging test slackSetup — 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
- api.slack.com/apps → Create New App → From scratch → name it "Minara" → pick your workspace
- Left nav → Incoming Webhooks → toggle the feature On
- Add New Webhook to Workspace → pick the channel → Allow
- 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/xxxThe 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.getUploadURLExternal →
files.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.
mrkdwn / unfurl_links / unfurl_media
Control Slack's default parsing behaviour per-message:
mrkdwn: false— disable markdown expansion ontext(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-botin the target channel, or add thechat:write.publicscope
"not_authed" / "invalid_auth"
SLACK_BOT_TOKENmissing or wrong. Bot tokens start withxoxb-; user tokens (xoxp-) won't work — Slack API rejects them forchat.postMessage
"Streaming not working"
- You're probably in webhook mode. Check
SLACK_BOT_TOKEN+SLACK_CHANNEL_IDare 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_scopeusually means you're missingfiles:write(for attachments) orreactions:write(foradd_reaction) — re-install the app after adding scopes
Reference
- Env vars:
SLACK_WEBHOOK_URL,SLACK_BOT_TOKEN,SLACK_CHANNEL_ID - Source:
apps/agent/src/messaging/slack.ts - Slack Web API index: api.slack.com/methods
- Block Kit reference: api.slack.com/block-kit
- Incoming Webhooks: api.slack.com/messaging/webhooks