Moltis
Features

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:

  1. Go to api.slack.com/apps and click Create New App

  2. Choose From scratch, name the app, and select your workspace

  3. Navigate to OAuth & Permissions and add these Bot Token Scopes:

    • app_mentions:read — read @mentions
    • chat:write — send messages
    • files:write — upload files generated by the agent
    • im:history — read DM history
    • reactions:write — add acknowledgment reactions (👀/✅/❌; see Acknowledgment Reactions)
    • reactions:read — read reactions (only for inbound reaction triggers)
  4. Under Event Subscriptions > Subscribe to bot events, add:

    • app_mention — channel messages that @mention the bot
    • message.im — direct messages to the bot (paired with im:history)
    • reaction_added — only if using inbound reaction triggers (paired with reactions:read)
  5. 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 typeBot Token ScopeBot event
    Public channelschannels:historymessage.channels
    Private channelsgroups:historymessage.groups
    Multiparty direct messages (MPIMs)mpim:historymessage.mpim

    Both 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, so group_policy, mention_mode, and channel_allowlist apply to them.

  6. Click Install to Workspace and copy the Bot User OAuth Token (xoxb-...)

  7. For Socket Mode (recommended):

    • Go to Socket Mode and enable it
    • Generate an App-Level Token (xapp-...) with the connections:write scope
  8. 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

FieldRequiredDefaultDescription
bot_tokenyesBot user OAuth token (xoxb-...)
app_tokenyes*App-level token for Socket Mode (xapp-...). *Required for socket_mode.
api_base_urlno"https://slack.com/api"Slack Web API base URL. Override only for Slack-compatible proxies, mock servers, or gateways.
connection_modeno"socket_mode"Connection method: "socket_mode" or "events_api"
signing_secretno*Signing secret for Events API request verification. *Required for events_api.
dm_policyno"allowlist"Who can DM the bot: "open", "allowlist", or "disabled"
group_policyno"open"Who can talk to the bot in channels: "open", "allowlist", or "disabled"
mention_modeno"mention"When the bot responds in channels: "always", "mention", or "none"
allowlistno[]Slack user IDs allowed to DM the bot (when dm_policy = "allowlist")
channel_allowlistno[]Slack channel IDs allowed to interact with the bot
otp_self_approvalnotrueEnable OTP self-approval for non-allowlisted DM users
otp_cooldown_secsno300Cooldown after failed OTP attempts
modelnoOverride the default model for this channel
model_providernoProvider for the overridden model
stream_modeno"edit_in_place"Streaming mode: "edit_in_place", "native", or "off"
edit_throttle_msno500Minimum milliseconds between streaming edit updates
thread_repliesnotrueReply in threads
ack_reactionsnotrueAcknowledge inbound messages with emoji reactions (👀 → ✅/❌). Only applied when the bot is directly addressed (DM or @mention).
reaction_triggersnofalseRoute inbound user reactions into the agent as messages (e.g. react ✅ to approve).
reaction_trigger_emojisno[]When reaction_triggers is on, only these emoji shortcodes trigger the agent. Empty = any emoji.
rich_blocksnofalseRender replies as Block Kit blocks (headings, dividers, code) with a plain-text fallback. Disables streaming, since streamed text cannot carry blocks.
channel_overridesno{}Per-channel model/provider overrides (see below)
user_overridesno{}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 settingURL
Event Subscriptionshttps://your-host/api/channels/slack/<id>/events
Interactivity & Shortcutshttps://your-host/api/channels/slack/<id>/interactions
Slash Commandshttps://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

ValueBehavior
"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

ValueBehavior
"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

ValueBehavior
"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:

ModeBehavior
"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 in allowlist
  • 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)