Here's how to connect your Node.js agent to Slack using Socket Mode - no Express, no HTTP server, just WebSockets and callbacks.
npm install @slack/socket-mode @slack/web-apiThat's it. Two packages:
@slack/socket-mode- Receives events via WebSocket@slack/web-api- Sends messages back to Slack
You need TWO tokens:
- Go to https://api.slack.com/apps
- Create app → "From scratch"
- Click "OAuth & Permissions" in sidebar
- Add Bot Token Scopes (all 16):
app_mentions:read channels:history channels:join channels:read chat:write files:read files:write groups:history groups:read im:history im:read im:write mpim:history mpim:read mpim:write users:read - Click "Install to Workspace" at top
- Copy the Bot User OAuth Token (starts with
xoxb-)
- In same app, click "Basic Information" in sidebar
- Scroll to "App-Level Tokens"
- Click "Generate Token and Scopes"
- Name it whatever (e.g., "socket-token")
- Add scope:
connections:write - Click "Generate"
- Copy the token (starts with
xapp-)
- Go to https://api.slack.com/apps → select your app
- Click "Socket Mode" in sidebar
- Toggle "Enable Socket Mode" to ON
- This routes your app's interactions and events over WebSockets instead of public HTTP endpoints
- Done - no webhook URL needed!
Note: Socket Mode is intended for internal apps in development or behind a firewall. Not for apps distributed via Slack Marketplace.
- Go to https://api.slack.com/apps → select your app
- Click "App Home" in sidebar
- Scroll to "Show Tabs" section
- Check "Allow users to send Slash commands and messages from the messages tab"
- Save
- Go to https://api.slack.com/apps → select your app
- Click "Event Subscriptions" in sidebar
- Toggle "Enable Events" to ON
- Important: No Request URL needed (Socket Mode handles this)
- Expand "Subscribe to bot events"
- Click "Add Bot User Event" and add:
app_mention(required - to see when bot is mentioned)message.channels(required - to log all channel messages for context)message.groups(optional - to see private channel messages)message.im(required - to see DMs)
- Click "Save Changes" at bottom
Create .env file:
SLACK_BOT_TOKEN=xoxb-your-bot-token-here
SLACK_APP_TOKEN=xapp-your-app-token-hereAdd to .gitignore:
echo ".env" >> .gitignorerequire('dotenv').config();
const { SocketModeClient } = require('@slack/socket-mode');
const { WebClient } = require('@slack/web-api');
const socketClient = new SocketModeClient({
appToken: process.env.SLACK_APP_TOKEN
});
const webClient = new WebClient(process.env.SLACK_BOT_TOKEN);
// Listen for app mentions (@mom do something)
socketClient.on('app_mention', async ({ event, ack }) => {
try {
// Acknowledge receipt
await ack();
console.log('Mentioned:', event.text);
console.log('Channel:', event.channel);
console.log('User:', event.user);
// Process with your agent
const response = await yourAgentFunction(event.text);
// Send response
await webClient.chat.postMessage({
channel: event.channel,
text: response
});
} catch (error) {
console.error('Error:', error);
}
});
// Start the connection
(async () => {
await socketClient.start();
console.log('⚡️ Bot connected and listening!');
})();
// Your existing agent logic
async function yourAgentFunction(text) {
// Your code here
return "I processed: " + text;
}That's it. No web server. Just run it:
node bot.jsIf you want to see every message in channels/DMs the bot is in:
// Listen to all Slack events
socketClient.on('slack_event', async ({ event, body, ack }) => {
await ack();
console.log('Event type:', event.type);
console.log('Event data:', event);
if (event.type === 'message' && event.subtype === undefined) {
// Regular message (not bot message, not edited, etc.)
console.log('Message:', event.text);
console.log('Channel:', event.channel);
console.log('User:', event.user);
// Your logic here
}
});await webClient.chat.postMessage({
channel: 'C12345', // or channel ID from event
text: 'Hello!'
});// Open DM channel with user
const result = await webClient.conversations.open({
users: 'U12345' // user ID
});
// Send message to that DM
await webClient.chat.postMessage({
channel: result.channel.id,
text: 'Hey there!'
});const channels = await webClient.conversations.list({
types: 'public_channel,private_channel'
});
console.log(channels.channels);const members = await webClient.conversations.members({
channel: 'C12345'
});
console.log(members.members); // Array of user IDsconst user = await webClient.users.info({
user: 'U12345'
});
console.log(user.user.name);
console.log(user.user.real_name);await webClient.conversations.join({
channel: 'C12345'
});await webClient.files.uploadV2({
channel_id: 'C12345',
file: fs.createReadStream('./file.pdf'),
filename: 'document.pdf',
title: 'My Document'
});require('dotenv').config();
const { SocketModeClient } = require('@slack/socket-mode');
const { WebClient } = require('@slack/web-api');
const socketClient = new SocketModeClient({
appToken: process.env.SLACK_APP_TOKEN
});
const webClient = new WebClient(process.env.SLACK_BOT_TOKEN);
// Your existing agent/AI/whatever
class MyAgent {
async process(message, context) {
// Your complex logic here
// context has: user, channel, etc.
return `Processed: ${message}`;
}
}
const agent = new MyAgent();
// Handle mentions
socketClient.on('app_mention', async ({ event, ack }) => {
await ack();
try {
// Remove the @mention from text
const text = event.text.replace(/<@[A-Z0-9]+>/g, '').trim();
// Process with your agent
const response = await agent.process(text, {
user: event.user,
channel: event.channel
});
// Send response
await webClient.chat.postMessage({
channel: event.channel,
text: response
});
} catch (error) {
console.error('Error processing mention:', error);
// Send error message
await webClient.chat.postMessage({
channel: event.channel,
text: 'Sorry, something went wrong!'
});
}
});
// Start
(async () => {
await socketClient.start();
console.log('⚡️ Agent connected to Slack!');
})();You subscribed to these in step 4:
app_mention- Someone @mentioned the botmessage- Any message in a channel/DM the bot is in
Event object structure:
{
type: 'app_mention' or 'message',
text: 'the message text',
user: 'U12345', // who sent it
channel: 'C12345', // where it was sent
ts: '1234567890.123456' // timestamp
}✅ No web server needed - just run your script
✅ No public URL needed - works behind firewall
✅ No ngrok - works on localhost
✅ Auto-reconnect - SDK handles connection drops
✅ Event-driven - just listen to callbacks
❌ Can't distribute to Slack App Directory (only for your workspace)
❌ Script must be running to receive messages (unlike webhooks)
❌ Max 10 concurrent connections per app
- You MUST call
ack()on every event or Slack will retry - Bot token (
xoxb-) is for sending messages - App token (
xapp-) is for receiving events via WebSocket - Connection is persistent - your script stays running
- No URL validation needed (unlike HTTP webhooks)
- Check you're using the right tokens
- Bot token for WebClient, App token for SocketModeClient
- Make sure you added all 16 bot scopes
- Reinstall the app after adding scopes
- Check Socket Mode is enabled
- Check you subscribed to events in "Event Subscriptions"
- Make sure bot is in the channel (or use
channels:join)
- Must subscribe to
app_mentionevent - Bot must be installed to workspace
- Check
await ack()is called
That's it. No HTTP server bullshit. Just WebSockets and callbacks.