Slack
Moltis can connect to Slack as a bot, letting you chat with your agent from any Slack workspace. The integration supports both Socket Mode (default, no public URL needed) and Events API (webhook-based).
How It Works
┌──────────────────────────────────────────────────────┐
│ Slack API │
│ (Socket Mode / Events API) │
└──────────────────┬───────────────────────────────────┘
│ WebSocket (Socket Mode)
│ or HTTP POST (Events API)
▼
┌──────────────────────────────────────────────────────┐
│ moltis-slack crate │
│ ┌────────────┐ ┌────────────┐ ┌────────────────┐ │
│ │ Handler │ │ Outbound │ │ Plugin │ │
│ │ (inbound) │ │ (replies) │ │ (lifecycle) │ │
│ └────────────┘ └────────────┘ └────────────────┘ │
└──────────────────┬───────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────┐
│ Moltis Gateway │
│ (chat dispatch, tools, memory) │
└──────────────────────────────────────────────────────┘
With Socket Mode (the default), the bot opens an outbound WebSocket connection to Slack — no public URL, port forwarding, or TLS certificate is needed. With Events API mode, Slack sends HTTP POST requests to your server, requiring a publicly reachable endpoint.
Prerequisites
Before configuring Moltis, create a Slack app:
-
Go to api.slack.com/apps and click Create New App
-
Choose From scratch, name the app, and select your workspace
-
Navigate to OAuth & Permissions and add these Bot Token Scopes:
app_mentions:read— read @mentionschat:write— send messagesfiles:write— upload files generated by the agentim:history— read DM historyreactions:write— add acknowledgment reactions (👀/✅/❌; see Acknowledgment Reactions)reactions:read— read reactions (only for inbound reaction triggers)
-
Under Event Subscriptions > Subscribe to bot events, add:
app_mention— channel messages that @mention the botmessage.im— direct messages to the bot (paired withim:history)reaction_added— only if using inbound reaction triggers (paired withreactions:read)
-
If you use
mention_mode = "always", add the matching scope and bot event for every conversation type where the bot should receive all messages:Conversation type Bot Token Scope Bot event Public channels channels:historymessage.channelsPrivate channels groups:historymessage.groupsMultiparty direct messages (MPIMs) mpim:historymessage.mpimBoth entries in a row are required: the history scope permits access, while the
message.*event delivers new messages to Moltis. Private channels and MPIMs are handled as channels by Moltis, sogroup_policy,mention_mode, andchannel_allowlistapply to them. -
Click Install to Workspace and copy the Bot User OAuth Token (
xoxb-...) -
For Socket Mode (recommended):
- Go to Socket Mode and enable it
- Generate an App-Level Token (
xapp-...) with theconnections:writescope
-
For Events API mode:
- Go to Event Subscriptions and enable it
- Set the exact URLs shown in Events API Mode
- Copy the Signing Secret from Basic Information
Configuration
Add a [channels.slack.<account-id>] section to your moltis.toml:
[channels.slack.my-bot]
bot_token = "xoxb-your-bot-token"
app_token = "xapp-your-app-token"
Make sure "slack" is included in channels.offered:
[channels]
offered = ["slack"]
Configuration Fields
| Field | Required | Default | Description |
|---|---|---|---|
bot_token | yes | — | Bot user OAuth token (xoxb-...) |
app_token | yes* | — | App-level token for Socket Mode (xapp-...). *Required for socket_mode. |
api_base_url | no | "https://slack.com/api" | Slack Web API base URL. Override only for Slack-compatible proxies, mock servers, or gateways. |
connection_mode | no | "socket_mode" | Connection method: "socket_mode" or "events_api" |
signing_secret | no* | — | Signing secret for Events API request verification. *Required for events_api. |
dm_policy | no | "allowlist" | Who can DM the bot: "open", "allowlist", or "disabled" |
group_policy | no | "open" | Who can talk to the bot in channels: "open", "allowlist", or "disabled" |
mention_mode | no | "mention" | When the bot responds in channels: "always", "mention", or "none" |
allowlist | no | [] | Slack user IDs allowed to DM the bot (when dm_policy = "allowlist") |
channel_allowlist | no | [] | Slack channel IDs allowed to interact with the bot |
otp_self_approval | no | true | Enable OTP self-approval for non-allowlisted DM users |
otp_cooldown_secs | no | 300 | Cooldown after failed OTP attempts |
model | no | — | Override the default model for this channel |
model_provider | no | — | Provider for the overridden model |
stream_mode | no | "edit_in_place" | Streaming mode: "edit_in_place", "native", or "off" |
edit_throttle_ms | no | 500 | Minimum milliseconds between streaming edit updates |
thread_replies | no | true | Reply in threads |
ack_reactions | no | true | Acknowledge inbound messages with emoji reactions (👀 → ✅/❌). Only applied when the bot is directly addressed (DM or @mention). |
reaction_triggers | no | false | Route inbound user reactions into the agent as messages (e.g. react ✅ to approve). |
reaction_trigger_emojis | no | [] | When reaction_triggers is on, only these emoji shortcodes trigger the agent. Empty = any emoji. |
rich_blocks | no | false | Render replies as Block Kit blocks (headings, dividers, code) with a plain-text fallback. Disables streaming, since streamed text cannot carry blocks. |
channel_overrides | no | {} | Per-channel model/provider overrides (see below) |
user_overrides | no | {} | Per-user model/provider overrides (see below) |
Full Example
[channels]
offered = ["slack"]
[channels.slack.my-bot]
bot_token = "xoxb-..."
app_token = "xapp-..."
api_base_url = "https://slack.com/api"
connection_mode = "socket_mode"
dm_policy = "allowlist"
group_policy = "open"
mention_mode = "mention"
allowlist = ["U0123456789", "U9876543210"]
channel_allowlist = ["C0123456789"]
otp_self_approval = true
otp_cooldown_secs = 300
model = "claude-sonnet-4-20250514"
model_provider = "anthropic"
stream_mode = "edit_in_place"
edit_throttle_ms = 500
thread_replies = true
ack_reactions = true
# Per-channel override: use a different model in a specific Slack channel
[channels.slack.my-bot.channel_overrides.C0123456789]
model = "gpt-4o"
# Per-user override: use a specific model/provider for a Slack user
[channels.slack.my-bot.user_overrides.U0123456789]
model = "claude-sonnet-4-20250514"
model_provider = "anthropic"
Events API Mode
Set these Request URLs in your Slack app (replace <id> with the account ID you
configured in Moltis):
| Slack setting | URL |
|---|---|
| Event Subscriptions | https://your-host/api/channels/slack/<id>/events |
| Interactivity & Shortcuts | https://your-host/api/channels/slack/<id>/interactions |
| Slash Commands | https://your-host/api/channels/slack/<id>/commands |
These endpoints are reachable without a Moltis session — Slack cannot present one — and instead verify Slack’s request signature (HMAC) and timestamp before doing any work.
If you prefer webhook-based delivery instead of Socket Mode:
[channels.slack.my-bot]
bot_token = "xoxb-..."
connection_mode = "events_api"
signing_secret = "abc123..."
This requires your Moltis instance to be reachable from the internet (or use Tailscale Funnel).
Slack-Compatible Proxies
Moltis normally talks to Slack at https://slack.com/api. For testing, local
development, or Slack-compatible gateways, set api_base_url:
[channels.slack.proxy]
bot_token = "xoxb-proxy-token"
app_token = "xapp-proxy-token"
api_base_url = "https://proxy.example/api"
connection_mode = "socket_mode"
For Socket Mode, Moltis calls apps.connections.open on this endpoint and
connects to the WebSocket URL returned by that API.
The endpoint must be a public HTTP(S) URL. Localhost, private-network, link-local, and other non-public IP targets are rejected because Slack API calls carry the bot token. Hostnames are resolved via DNS when the account starts, and every resolved address is checked against the same policy — a DNS name pointing at a private or cloud-metadata address is rejected, and a hostname that fails to resolve is rejected too (validation fails closed).
Allowing internal hosts
If you deliberately front Slack with a proxy on an internal host, an operator can
allow specific hosts back in with the MOLTIS_SLACK_API_BASE_URL_ALLOWLIST
environment variable — a comma-separated list of exact hostnames or IPs:
MOLTIS_SLACK_API_BASE_URL_ALLOWLIST="proxy.internal,127.0.0.1"
Only hosts on this list bypass the private-address guard, and matching is on the
exact host (so localhost and 127.0.0.1 must each be listed if you want both).
The allowlist is intentionally an environment variable, not a web-editable
setting, so this SSRF exception stays under operator control. Cloud metadata
addresses (169.254.169.254, fd00:ec2::254) remain blocked even when
allowlisted, including when an allowlisted hostname resolves to one.
If using a proxy, ensure it supports Slack’s native streaming methods before
setting stream_mode = "native".
Access Control
Slack uses the same gating system as Telegram, Discord, and other channels.
DM Policy
| Value | Behavior |
|---|---|
"allowlist" | Only users listed in allowlist can DM (default). If the allowlist is empty, unknown users receive an OTP challenge when otp_self_approval = true. |
"open" | Anyone in the workspace can DM the bot |
"disabled" | DMs are silently ignored |
When dm_policy = "allowlist" and otp_self_approval = true, unknown DM users
receive a verification prompt. The PIN is visible to the bot owner in the web UI
under Channels → Senders. After a correct PIN reply, Moltis adds the sender’s
Slack user ID, such as U0123456789, to the account allowlist.
Group Policy
| Value | Behavior |
|---|---|
"open" | Bot responds in any channel it’s invited to (default) |
"allowlist" | Only channels listed in channel_allowlist are allowed. An empty channel allowlist denies all channels. |
"disabled" | Channel messages are silently ignored |
Mention Mode
| Value | Behavior |
|---|---|
"mention" | Bot only responds when @mentioned (default) |
"always" | Bot responds to every message in allowed channels |
"none" | Bot never responds in channels (useful for DM-only bots) |
Slack only sends general public-channel messages when the app has both the
channels:history Bot Token Scope and the message.channels bot event. For
private channels, use groups:history with message.groups; for MPIMs, use
mpim:history with message.mpim. The default "mention" mode only needs the
app_mentions:read scope and app_mention event for channel mentions.
Allowlist Matching
Allowlist entries support:
- Exact match (case-insensitive):
"U0123456789" - Glob wildcards:
"U012*","*admin*"
Streaming
Slack supports three streaming modes:
| Mode | Behavior |
|---|---|
"edit_in_place" | Sends a placeholder message and edits it as tokens arrive (default) |
"native" | Uses Slack’s streaming API (chat.startStream/chat.appendStream/chat.stopStream) |
"off" | No streaming — sends the full response as a single message |
The edit_in_place mode throttles updates to edit_throttle_ms milliseconds
(default: 500) to avoid Slack API rate limits.
Native mode calls those three methods directly at api_base_url with the bot
token and requires a thread target. With thread_replies = false, Moltis falls
back to top-level edit-in-place streaming instead. For threaded replies, Moltis
does not probe whether the Slack app, workspace, or compatible proxy supports
the native methods, and a native API failure does not switch an active stream to
edit-in-place mode. Native requests send standard Markdown unchanged through
Slack’s markdown_text field and use edit_throttle_ms. When
rich_blocks = true, rich rendering takes precedence: streaming (including
native streaming) is disabled and the completed response is sent once through
the Block Kit path.
Thread Replies
By default (thread_replies = true), the bot replies in a thread attached to
the user’s message. Set thread_replies = false to have the bot reply directly
in the channel.
Acknowledgment Reactions
Because Slack bots cannot show a typing indicator, Moltis acknowledges messages with emoji reactions so you know your message was received and is being worked on:
- 👀 (
eyes) is added as soon as the message starts processing. - While the agent runs, the reaction swaps to a phase emoji reflecting the current tool — 🌐 web, 💻 shell, ✏️ file edits, 🛠️ other tools — and shows ⏳ if a step runs long. Rapid changes are debounced so the reaction doesn’t flicker.
- ✅ (
white_check_mark) replaces it when the reply is delivered. - ❌ (
x) replaces it if the turn fails. A cancelled turn just removes 👀.
Reactions are only added when the bot is directly addressed — a direct
message, or a channel message that @mentions the bot — never on general channel
chatter. Set ack_reactions = false to disable them.
Each inbound message owns its own reaction for its whole life, including while
it waits behind an in-flight turn: a message queued behind another keeps its 👀
and is resolved when its own turn runs. In collect queue mode one reply
answers several messages, and every one of them receives the terminal reaction.
A reply that fails to deliver is marked ❌ rather than ✅.
The bot needs the reactions:write OAuth scope (and reactions:read if you
also use inbound reaction triggers). Reaction failures are non-fatal and never
block the reply.
Reaction Triggers
With reaction_triggers = true, when a user adds an emoji reaction to a message,
Moltis routes it into the agent as a synthetic message (threaded under the
reacted message so the agent sees the original content as context). This enables
flows like “react ✅ to approve” or “react 👍 to continue”.
- The bot’s own acknowledgment reactions (👀/✅/❌) are always ignored, so triggers never loop.
- Only added reactions trigger (not removals), and only from senders who pass the normal DM/channel access policy.
- Only reactions on the bot’s own messages trigger. Without this, any member could point the agent at an unrelated person’s message just by reacting to it.
- Restrict which emoji count with
reaction_trigger_emojis(shortcodes without colons, e.g.["white_check_mark", "thumbsup"]). Empty means any emoji.
This requires the reactions:read scope and the reaction_added event
subscription.
Troubleshooting
Bot doesn’t respond
- Verify the bot and app tokens are correct
- Check that Socket Mode is enabled in the Slack app settings
- Check
dm_policy— if set to"allowlist", make sure your Slack user ID is inallowlist - Ensure the bot has been invited to channels you want it to respond in
- Look at logs:
RUST_LOG=moltis_slack=debug moltis
Bot doesn’t respond in channels
- Check
mention_mode— if"mention", you must @mention the bot - Check
group_policy— if"disabled", channel messages are ignored - Check
channel_allowlist— if non-empty, the channel must be listed - Ensure the bot is a member of the channel (invite it with
/invite @botname)