Slack is where work happens — decisions in DMs, culture in #random. If your AI agent isn’t on Slack, it’s missing 90% of your team’s conversations. But Slack bot setup has gotten more complex: Socket Mode vs HTTP, Events API vs deprecated RTM, channel scopes vs user scopes, and Enterprise Grid on top.
This guide walks you through creating a Slack app from scratch, connecting it to OpenClaw, and configuring it to respond only when @-mentioned (not spamming every channel). Works for both standard Workspaces and Enterprise Grid.
Why Slack Bots Are Different (And More Complicated Now)
- ●The old days: RTM API — WebSocket, receive all events. Simple but deprecated.
- ●The new way: Events API — Slack POSTs events to your endpoint. Scalable but needs a public URL.
- ●The compromise: Socket Mode — Slack opens a WebSocket to your app, no public endpoint needed.
Scope complexity: Slack distinguishes between user tokens vs bot tokens, channel scopes (public) vs group scopes (private) vs IM/MPIM (DMs). Miss one and a feature silently breaks. Enterprise Grid adds multi-workspace deployment and org-level installation.
Step 1: Create Slack App
Go to api.slack.com/apps, sign in, and click Create New App → From scratch. Name it “OpenClaw Bot” and pick your workspace.
Note Your App Credentials
In Basic Information, copy:
- ●App ID — e.g. A123456789
- ●Client ID and Client Secret
- ●Signing Secret — used to verify requests (HTTP mode)
Step 2: Configure OAuth Scopes
In OAuth & Permissions → Bot Token Scopes, add the minimal set for OpenClaw:
- ●channels:history, channels:read — read & view public channels
- ●chat:write, chat:write.public — send messages
- ●users:read — get user info
- ●app_mentions:read — receive @bot mentions
- ●im:history, im:read, im:write — DMs (optional)
- ●groups:history, groups:read — private channels (optional)
- ●reactions:read, reactions:write — emoji reactions (optional)
Step 3: Choose Connection Mode
Option A: Socket Mode (recommended for most)
- ●Pros: no public endpoint, works behind firewalls, easy local dev
- ●Cons: persistent connection, dropped events on disconnect, not ideal for 100+ workspaces
- ●Best for: single workspace, internal tool, or development
Option B: HTTP Events (traditional)
- ●Pros: truly scalable, stateless, industry standard
- ●Cons: requires public HTTPS endpoint, URL verification, more setup
- ●Best for: production SaaS or multi-workspace apps
For this guide we’ll use Socket Mode. Enable it under Socket Mode, then generate an App-Level Token with the connections:write scope. Copy it — it starts with xapp-.
Step 4: Subscribe to Events
In Event Subscriptions, toggle Enable Events on (no Request URL needed for Socket Mode). Under Subscribe to bot events add:
- ●app_mention — bot is @-mentioned
- ●message.im — DMs to the bot
- ●message.channels — all public channel messages (filter in code)
- ●message.groups — private channel messages (if group scopes added)
message.channels entirely and only subscribe to app_mention.Step 5: Install App to Workspace
- 1.In OAuth & Permissions, click Install to Workspace
- 2.Review requested scopes, click Allow
- 3.Copy the Bot User OAuth Token (starts with xoxb-)
- 4.Invite the bot to channels with /invite @OpenClaw Bot
chat:write.public).Step 6: Configure OpenClaw
Add Slack credentials to your .env:
SLACK_ENABLED=true SLACK_BOT_TOKEN=xoxb-... SLACK_APP_TOKEN=xapp-... SLACK_SIGNING_SECRET=... SLACK_MODE=socket SLACK_RESPOND_TO_MENTIONS_ONLY=true
Create the Slack Adapter
// adapters/slack.js
const { App } = require('@slack/bolt');
class SlackAdapter {
constructor(config) { this.config = config; this.app = null; }
async initialize() {
this.app = new App({
token: this.config.botToken,
appToken: this.config.appToken,
socketMode: this.config.mode === 'socket',
signingSecret: this.config.signingSecret,
});
this.app.event('app_mention', async ({ event, say }) => {
const text = event.text.replace(/<@[A-Z0-9]+>/g, '').trim();
const response = await this.processWithOpenClaw(event.user, text, event.channel);
await say({ text: response, thread_ts: event.ts });
});
this.app.message(async ({ message, say }) => {
if (message.channel_type === 'im') {
const response = await this.processWithOpenClaw(message.user, message.text, message.channel);
await say(response);
}
});
await this.app.start();
console.log('[Slack] Bot is running!');
}
}
module.exports = SlackAdapter;Install Bolt: npm install @slack/bolt. Enable the channel in config/channels.yml:
channels:
slack:
enabled: true
adapter: adapters/slack.js
botToken: ${SLACK_BOT_TOKEN}
appToken: ${SLACK_APP_TOKEN}
signingSecret: ${SLACK_SIGNING_SECRET}
mode: socket
respond_to_mentions_only: true
reply_in_threads: true
rate_limit:
messages_per_second: 1Step 7: Test Your Bot
Run npm start. You should see [Slack] Bot is running! and the bot status switches to Active (green) in Slack. In an invited channel:
@OpenClaw Bot what's 2+2?
It should reply in-thread. DM the bot directly to test IM handling. If nothing happens, verify: bot is invited, im:history scope is enabled for DMs, and OpenClaw logs show the event arriving.
Responding Only When @-Mentioned
With message.channels subscribed, your bot receives every channel message. Without filtering, it would respond to everything. Two clean approaches:
// Approach 1: only subscribe to app_mention (cleanest)
this.app.event('app_mention', async ({ event, say }) => {
await handleMention(event, say);
});
// Approach 2: filter inside message.channels
this.app.message(async ({ message, say }) => {
const botId = await getBotUserId();
if (!message.text.includes(`<@${botId}>`)) return;
await handleMention(message, say);
});Alternative: Slash Commands
Create /ask under Slash Commands, then handle it in code:
this.app.command('/ask', async ({ command, ack, say }) => {
await ack();
const response = await this.processWithOpenClaw(command.user_id, command.text);
await say(response);
});Channel Scopes vs User Scopes
Bot Token Scopes (recommended): bot acts as a bot user, limited permissions, safer. User Token Scopes: act as a specific user — needed only for things like reading a user’s DMs outside the bot or posting as the user. 99% of bots use bot tokens exclusively.
Enterprise Grid Considerations
- ●Multiple workspaces under one org instead of a single workspace
- ●App can be installed org-wide (requires admin approval)
- ●Each workspace still gets its own bot token — you route by team_id
const tokens = {
'T123WORKSPACE1': 'xoxb-token-for-workspace-1',
'T456WORKSPACE2': 'xoxb-token-for-workspace-2',
};
const workspaceId = event.team_id;
const token = tokens[workspaceId];Socket Mode vs HTTP: Deep Dive
Socket Mode: your app connects to wss://wss-primary.slack.com/, Slack pushes events via WebSocket, Bolt SDK auto-reconnects on disconnect. HTTP Mode: Slack POSTs JSON to your public endpoint; you must verify the X-Slack-Signature header, respond within 3 seconds with HTTP 200, and handle the one-time URL verification challenge:
if (body.type === 'url_verification') {
res.send({ challenge: body.challenge });
}Slack Block Kit (Rich Messages)
Plain text is fine, but Slack’s Block Kit lets you send formatted sections, dividers, fields, and interactive buttons:
await say({
blocks: [
{ type: 'section', text: { type: 'mrkdwn', text: '*OpenClaw Response:*nHere is what I found:' } },
{ type: 'divider' },
{ type: 'actions', elements: [
{ type: 'button', text: { type: 'plain_text', text: 'Yes' }, action_id: 'confirm_yes', style: 'primary' },
{ type: 'button', text: { type: 'plain_text', text: 'No' }, action_id: 'confirm_no', style: 'danger' },
]},
],
});
this.app.action('confirm_yes', async ({ ack, say }) => { await ack(); await say('Confirmed!'); });Common Slack Bot Issues (And Fixes)
- ●Bot doesn’t respond to mentions — not invited to channel, missing app_mentions:read, or app_mention event not subscribed
- ●Bot responds to ALL messages — you subscribed to message.channels without filtering for the bot mention
- ●Socket disconnects frequently — wrap the process in PM2 so it stays alive; Bolt auto-reconnects
- ●Rate limit errors — Slack allows ~1 message/second per channel; add a per-channel debounce
- ●Can’t read DMs — missing im:history scope; add scope, reinstall, send a new DM
Security Considerations
For HTTP mode, verify every request using X-Slack-Signature and X-Slack-Request-Timestamp (reject anything older than 5 minutes) — Bolt does this automatically. Keep your bot token in .env, never log it, and rotate it from OAuth & Permissions if exposed.
Scope Minimization
Only request scopes you actually need — users see the full list at install time. Start with channels:history, chat:write, users:read, app_mentions:read and add more later only as features demand them.
The PaioClaw Alternative
End-to-end Slack DIY: ~45–60 minutes plus ongoing maintenance for scope changes, token rotation, and multi-workspace handling. With PaioClaw:
- 1.Connect Slack workspace via OAuth
- 2.Pick response mode — mentions-only, DMs, or all messages
- 3.Deploy
Total time: ~3 minutes. Pre-configured minimal scopes, automatic mention filtering, rich message templates, and Enterprise Grid multi-workspace support out of the box. Starts FREE, Smart $15/month, Genius $25/month.
The Bottom Line
Slack bot setup is more involved than it used to be, but more powerful. Socket Mode simplifies deployment (no public endpoint), and the scope system is granular but logical once you separate bot vs user tokens. The critical rule: respond only to @-mentions unless you actually want to spam channels — subscribe to app_mention or filter message.channels for the bot’s user ID.
Self-host with this guide if you want to be a Slack API expert. Use PaioClaw if you just want your agent on Slack — and on every other channel — without debugging OAuth scopes for an afternoon.

