Slack Integration
The slack-channel plugin connects a Slack app to Codebolt using @slack/bolt in Socket Mode. It supports channel messages (@mention required), DMs, file uploads, and threaded replies.
Prerequisites
- Create a Slack app at api.slack.com/apps
- Enable Socket Mode and generate an App-Level Token (
xapp-...) - Add the following Bot Token scopes:
channels:history,chat:write,files:write,im:history,im:write - Enable the messages.im and app_mention event subscriptions
- Install the app to your workspace and copy the Bot Token (
xoxb-...)
Installation
codebolt plugin install slack-channel
Configuration
| Field | Required | Description |
|---|---|---|
appToken | Yes | App-level token starting with xapp- |
botToken | Yes | Bot user token starting with xoxb- |
agentId | No | Default agent to route messages to |
threadStrategy | No | per-user (default) · per-conversation · per-message · single |
Message routing
Channels: The bot only responds to app_mention events — messages where the bot is @mentioned.
DMs (Direct Messages): All messages are forwarded without requiring a mention.
User in #general: @MyBot summarise the PR
→ routes via app_mention event
User in DM: summarise the PR
→ routes via message event (no mention needed)
Thread support
The plugin preserves Slack's thread_ts so agent replies stay in the same thread:
// Reply arrives from agent
plugin.gateway.onReply(async (reply) => {
await app.client.chat.postMessage({
token: botToken,
channel: reply.replyTo.externalThreadId,
text: reply.text,
thread_ts: reply.metadata?.thread_ts, // reply in thread if original was threaded
});
});
File handling
Incoming files are forwarded as attachment metadata. Outgoing files from the agent are uploaded via files.uploadV2:
await app.client.files.uploadV2({
token: botToken,
channel_id: channelId,
file: fileBuffer,
filename: attachment.name,
});
Message chunking
Slack limits message blocks to 4,000 characters. The plugin splits long replies automatically.
Minimal custom implementation
import { App } from '@slack/bolt';
export async function connect(plugin, config) {
const app = new App({
appToken: config.appToken,
token: config.botToken,
socketMode: true,
});
// Get the bot user ID for mention filtering
const auth = await app.client.auth.test({ token: config.botToken });
const botUserId = auth.user_id as string;
await plugin.gateway.registerChannel({
channelId: 'my-slack',
name: 'Slack',
platform: 'slack',
agentId: config.agentId,
threadStrategy: config.threadStrategy ?? 'per-user',
});
const handleMessage = async ({ message, say }) => {
if (message.bot_id || message.subtype) return;
await plugin.gateway.routeMessage({
source: 'channel',
sourceId: plugin.pluginId,
threadStrategy: config.threadStrategy ?? 'per-user',
agentId: config.agentId,
userId: message.user,
externalThreadId: message.channel,
text: message.text ?? '',
metadata: { thread_ts: message.thread_ts ?? message.ts },
replyTo: {
channelId: 'my-slack',
externalThreadId: message.channel,
userId: message.user,
},
});
};
app.message(handleMessage);
app.event('app_mention', ({ event }) => handleMessage({ message: event, say: null }));
plugin.gateway.onReply(async (reply) => {
const chunks = reply.text.match(/.{1,4000}/gs) ?? [reply.text];
for (const chunk of chunks) {
await app.client.chat.postMessage({
token: config.botToken,
channel: reply.replyTo.externalThreadId!,
text: chunk,
thread_ts: reply.metadata?.thread_ts,
});
}
});
await app.start();
}