Building AI Slack Bots with NeuroLink
Build intelligent Slack bots with NeuroLink. Commands, conversations, and workspace automation.
You will build an AI-powered Slack bot using the Slack Bolt framework and NeuroLink’s generation API. By the end of this tutorial, you will have a production-ready bot that responds to natural language queries, analyzes sentiment, summarizes threads, and routes requests to the right team members.
Tip: This tutorial builds a custom Slack bot from scratch. NeuroLink does not provide a built-in Slack integration – you will build the webhook handlers and event listeners yourself using Bolt, with NeuroLink handling the AI generation.
Now you will set up the Slack application, install dependencies, and configure the Bolt framework.
External Dependencies
This tutorial requires several npm packages for building Slack bots:
1
2
3
npm install @juspay/neurolink @slack/bolt
npm install dotenv
npm install -D typescript @types/node ts-node nodemon
Required Packages:
@juspay/neurolink- NeuroLink SDK for AI generation@slack/bolt- Slack’s official framework for building appsdotenv- Environment variable managementtypescriptand@types/node- TypeScript supportts-nodeandnodemon- Development tools
Prerequisites
Before we begin, ensure you have:
- Node.js 20.18.1+ installed
- A Slack workspace where you have admin permissions
- NeuroLink account with API access
- Required packages (see External Dependencies section above)
- Basic familiarity with JavaScript and async/await patterns
- A code editor of your choice
Part 1: Setting Up Your Slack Application
Creating the Slack App
First, navigate to the Slack API portal at api.slack.com/apps and click “Create New App.” Select “From scratch” and provide a name for your bot. Choose the workspace where you’ll develop and test.
After creation, you’ll land on the Basic Information page. Keep this tab open as we’ll need several values from here.
Configuring OAuth Scopes
Navigate to “OAuth & Permissions” in the sidebar. Scroll to “Scopes” and add the following Bot Token Scopes:
1
2
3
4
5
6
7
8
9
10
11
app_mentions:read - Allows your bot to read messages that mention it
chat:write - Enables sending messages
channels:history - Read message history in public channels
channels:read - View basic channel information
groups:history - Read message history in private channels
groups:read - View basic private channel information
im:history - Read direct message history
im:read - View basic direct message information
im:write - Send direct messages
users:read - View user information
commands - Add slash commands
These scopes give your bot the permissions it needs to receive messages, respond appropriately, and interact with users.
Setting Up Event Subscriptions
Navigate to “Event Subscriptions” and toggle it on. You’ll need to provide a Request URL later once your server is running. For now, subscribe to the following bot events:
1
2
3
4
app_mention - When someone mentions your bot
message.channels - Messages in public channels
message.groups - Messages in private channels
message.im - Direct messages to your bot
Enabling Socket Mode
For development, Socket Mode allows your bot to receive events without exposing a public URL. Go to “Socket Mode” in the sidebar and enable it. Create an app-level token with the connections:write scope and save this token securely.
Installing the App
Return to “OAuth & Permissions” and click “Install to Workspace.” Authorize the requested permissions. After installation, copy the Bot User OAuth Token that appears. You’ll need both this token and the app-level token for your application.
Part 2: Project Setup and Dependencies
Initializing the Project
Create a new directory for your project and initialize it:
1
2
3
mkdir neurolink-slack-bot
cd neurolink-slack-bot
npm init -y
Installing Dependencies
Install the required packages:
1
2
npm install @slack/bolt @juspay/neurolink dotenv winston
npm install --save-dev nodemon typescript @types/node
The packages serve these purposes:
- @slack/bolt: Slack’s official framework for building apps
- @juspay/neurolink: NeuroLink’s SDK for AI capabilities
- dotenv: Environment variable management
- winston: Logging framework for production
Environment Configuration
Create a .env file in your project root:
SLACK_BOT_TOKEN=xoxb-your-bot-token
SLACK_APP_TOKEN=xapp-your-app-token
OPENAI_API_KEY=your-openai-api-key
LOG_LEVEL=info
Replace the placeholder values with your actual credentials. Never commit this file to version control.
Note: This tutorial uses Socket Mode, which doesn’t require
SLACK_SIGNING_SECRETfor request verification. Socket Mode establishes a WebSocket connection for real-time events, eliminating the need for signature verification. If you were using HTTP mode with webhooks instead, you would need to addSLACK_SIGNING_SECRET=your-signing-secretand implement signature verification in your request handlers.
Project Structure
Organize your project with this structure:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
neurolink-slack-bot/
├── src/
│ ├── index.js
│ ├── neurolink.js
│ ├── handlers/
│ │ ├── commands.js
│ │ ├── messages.js
│ │ └── events.js
│ └── utils/
│ ├── logger.js
│ └── formatters.js
├── .env
├── .gitignore
└── package.json
Part 3: Core Application Setup
Creating the Logger
Start with a robust logging utility in src/utils/logger.js:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const winston = require('winston');
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
)
})
]
});
module.exports = logger;
Initializing NeuroLink
Create the NeuroLink integration in src/neurolink.js:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
const { NeuroLink } = require('@juspay/neurolink');
const logger = require('./utils/logger');
const neurolink = new NeuroLink();
async function generateResponse(prompt, context = {}) {
try {
const result = await neurolink.generate({
input: { text: prompt },
systemPrompt: `You are an intelligent assistant integrated into a Slack workspace.
Be helpful, concise, and professional. Format responses appropriately for Slack
using markdown when helpful. Keep responses focused and actionable.`,
provider: 'openai',
model: 'gpt-4o' // Specify model directly - NeuroLink doesn't use env vars for model selection
});
return result.content;
} catch (error) {
logger.error('NeuroLink API error:', error);
throw new Error('Failed to generate AI response');
}
}
async function analyzeMessage(message, analysisType) {
const prompts = {
sentiment: `Analyze the sentiment of this message and respond with one word: positive, negative, or neutral.\n\nMessage: "${message}"`,
summary: `Summarize this message in one concise sentence:\n\n"${message}"`,
actionItems: `Extract any action items from this message. List them as bullet points. If none exist, respond with "No action items found."\n\n"${message}"`
};
return generateResponse(prompts[analysisType] || prompts.summary);
}
module.exports = { generateResponse, analyzeMessage };
Main Application Entry Point
Create the main application in src/index.js:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
require('dotenv').config();
const { App } = require('@slack/bolt');
const logger = require('./utils/logger');
const { setupCommands } = require('./handlers/commands');
const { setupMessageHandlers } = require('./handlers/messages');
const { setupEventHandlers } = require('./handlers/events');
const app = new App({
token: process.env.SLACK_BOT_TOKEN,
appToken: process.env.SLACK_APP_TOKEN,
socketMode: true
});
// Register all handlers
setupCommands(app);
setupMessageHandlers(app);
setupEventHandlers(app);
// Global error handler
app.error(async (error) => {
logger.error('Slack app error:', error);
});
// Start the application
(async () => {
await app.start();
logger.info('NeuroLink Slack Bot is running!');
})();
Part 4: Implementing Slash Commands
Slash commands provide a clean interface for users to interact with your bot. Create src/handlers/commands.js:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
const { generateResponse, analyzeMessage } = require('../neurolink');
const logger = require('../utils/logger');
function setupCommands(app) {
// Ask the AI a question
app.command('/ask', async ({ command, ack, respond }) => {
await ack();
try {
const question = command.text;
if (!question) {
await respond('Please provide a question. Usage: `/ask your question here`');
return;
}
await respond({
response_type: 'ephemeral',
text: 'Thinking...'
});
const answer = await generateResponse(question);
await respond({
response_type: 'in_channel',
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: `*Question:* ${question}`
}
},
{
type: 'divider'
},
{
type: 'section',
text: {
type: 'mrkdwn',
text: `*Answer:*\n${answer}`
}
},
{
type: 'context',
elements: [
{
type: 'mrkdwn',
text: `Asked by <@${command.user_id}> | Powered by NeuroLink`
}
]
}
]
});
} catch (error) {
logger.error('Error in /ask command:', error);
await respond('Sorry, I encountered an error processing your question. Please try again.');
}
});
// Summarize a message or text
app.command('/summarize', async ({ command, ack, respond }) => {
await ack();
try {
const text = command.text;
if (!text) {
await respond('Please provide text to summarize. Usage: `/summarize your text here`');
return;
}
const summary = await analyzeMessage(text, 'summary');
await respond({
response_type: 'ephemeral',
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: `*Summary:*\n${summary}`
}
}
]
});
} catch (error) {
logger.error('Error in /summarize command:', error);
await respond('Sorry, I could not generate a summary. Please try again.');
}
});
// Extract action items
app.command('/actions', async ({ command, ack, respond }) => {
await ack();
try {
const text = command.text;
if (!text) {
await respond('Please provide text to extract actions from. Usage: `/actions meeting notes or message`');
return;
}
const actions = await analyzeMessage(text, 'actionItems');
await respond({
response_type: 'ephemeral',
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: `*Action Items:*\n${actions}`
}
}
]
});
} catch (error) {
logger.error('Error in /actions command:', error);
await respond('Sorry, I could not extract action items. Please try again.');
}
});
// Help command
app.command('/aihelp', async ({ ack, respond }) => {
await ack();
await respond({
blocks: [
{
type: 'header',
text: {
type: 'plain_text',
text: 'NeuroLink Bot Commands'
}
},
{
type: 'section',
text: {
type: 'mrkdwn',
text: '*Available Commands:*'
}
},
{
type: 'section',
text: {
type: 'mrkdwn',
text: '`/ask [question]` - Ask the AI any question\n`/summarize [text]` - Get a summary of text\n`/actions [text]` - Extract action items\n`/aihelp` - Show this help message'
}
},
{
type: 'section',
text: {
type: 'mrkdwn',
text: '*Tips:*\n• Mention @NeuroLinkBot in any channel for conversational AI\n• DM the bot for private conversations\n• React with :brain: to get AI insights on any message'
}
}
]
});
});
}
module.exports = { setupCommands };
Don’t forget to register these commands in your Slack app settings under “Slash Commands.”
Part 5: Handling Conversations
Conversational interactions make your bot feel more natural. Create src/handlers/messages.js:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
const { generateResponse } = require('../neurolink');
const logger = require('../utils/logger');
// Store conversation history per user
const conversationHistory = new Map();
function getConversationContext(userId) {
if (!conversationHistory.has(userId)) {
conversationHistory.set(userId, []);
}
return conversationHistory.get(userId);
}
function addToConversation(userId, role, content) {
const history = getConversationContext(userId);
history.push({ role, content });
// Keep only last 10 messages for context
if (history.length > 10) {
history.shift();
}
}
function setupMessageHandlers(app) {
// Handle direct messages
app.message(async ({ message, say, client }) => {
// Ignore bot messages
if (message.bot_id || message.subtype) return;
// Only respond to DMs
const conversationInfo = await client.conversations.info({
channel: message.channel
});
if (!conversationInfo.channel.is_im) return;
try {
const userId = message.user;
const userMessage = message.text;
// Add user message to history
addToConversation(userId, 'user', userMessage);
// Get conversation context
const context = getConversationContext(userId);
// Build prompt with context
const contextPrompt = context
.map(msg => `${msg.role}: ${msg.content}`)
.join('\n');
const response = await generateResponse(
`Previous conversation:\n${contextPrompt}\n\nRespond to the latest message naturally and helpfully.`
);
// Add bot response to history
addToConversation(userId, 'assistant', response);
await say(response);
} catch (error) {
logger.error('Error handling DM:', error);
await say('Sorry, I encountered an error. Please try again.');
}
});
// Handle app mentions
app.event('app_mention', async ({ event, say, client }) => {
try {
// Remove the bot mention from the message
const botUserId = (await client.auth.test()).user_id;
const cleanMessage = event.text.replace(`<@${botUserId}>`, '').trim();
if (!cleanMessage) {
await say({
thread_ts: event.ts,
text: "Hi! How can I help you? Ask me anything or use `/aihelp` to see available commands."
});
return;
}
// Get thread context if replying in a thread
let contextMessages = [];
if (event.thread_ts) {
const replies = await client.conversations.replies({
channel: event.channel,
ts: event.thread_ts,
limit: 10
});
contextMessages = replies.messages
.filter(msg => !msg.bot_id)
.map(msg => msg.text)
.join('\n');
}
const prompt = contextMessages
? `Thread context:\n${contextMessages}\n\nQuestion: ${cleanMessage}`
: cleanMessage;
const response = await generateResponse(prompt);
await say({
thread_ts: event.thread_ts || event.ts,
text: response
});
} catch (error) {
logger.error('Error handling mention:', error);
await say({
thread_ts: event.ts,
text: 'Sorry, I encountered an error processing your request.'
});
}
});
}
module.exports = { setupMessageHandlers };
Part 6: Advanced Event Handling
Create src/handlers/events.js for handling reactions and other events:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
const { analyzeMessage, generateResponse } = require('../neurolink');
const logger = require('../utils/logger');
function setupEventHandlers(app) {
// React to :brain: emoji to provide AI insights
app.event('reaction_added', async ({ event, client }) => {
if (event.reaction !== 'brain') return;
try {
// Get the message that was reacted to
const result = await client.conversations.history({
channel: event.item.channel,
latest: event.item.ts,
inclusive: true,
limit: 1
});
if (!result.messages || result.messages.length === 0) return;
const originalMessage = result.messages[0].text;
// Generate insights
const insights = await generateResponse(
`Provide brief, helpful insights about this message. Include any relevant suggestions or observations:\n\n"${originalMessage}"`
);
// Reply in thread
await client.chat.postMessage({
channel: event.item.channel,
thread_ts: event.item.ts,
text: `*AI Insights:*\n${insights}`
});
} catch (error) {
logger.error('Error handling reaction:', error);
}
});
// Handle channel join to introduce the bot
app.event('member_joined_channel', async ({ event, client }) => {
// Only respond if the bot itself joined
const botInfo = await client.auth.test();
if (event.user !== botInfo.user_id) return;
try {
await client.chat.postMessage({
channel: event.channel,
text: `Hello! I'm NeuroLink Bot, your AI assistant. Here's what I can do:\n\n• Ask me questions by mentioning me: @NeuroLinkBot\n• Use \`/ask\` for quick questions\n• Use \`/summarize\` to condense text\n• Use \`/actions\` to extract action items\n• React with :brain: to any message for AI insights\n\nType \`/aihelp\` for more information!`
});
} catch (error) {
logger.error('Error sending welcome message:', error);
}
});
// Handle message shortcuts (from message actions menu)
app.shortcut('analyze_message', async ({ shortcut, ack, client }) => {
await ack();
try {
const messageText = shortcut.message.text;
// Open a modal with analysis options
await client.views.open({
trigger_id: shortcut.trigger_id,
view: {
type: 'modal',
callback_id: 'analysis_modal',
private_metadata: JSON.stringify({
channel: shortcut.channel.id,
message_ts: shortcut.message.ts,
message_text: messageText
}),
title: {
type: 'plain_text',
text: 'Analyze Message'
},
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: `*Message:*\n>${messageText.substring(0, 200)}${messageText.length > 200 ? '...' : ''}`
}
},
{
type: 'input',
block_id: 'analysis_type',
element: {
type: 'static_select',
action_id: 'type_select',
placeholder: {
type: 'plain_text',
text: 'Select analysis type'
},
options: [
{
text: { type: 'plain_text', text: 'Sentiment Analysis' },
value: 'sentiment'
},
{
text: { type: 'plain_text', text: 'Summary' },
value: 'summary'
},
{
text: { type: 'plain_text', text: 'Extract Action Items' },
value: 'actionItems'
}
]
},
label: {
type: 'plain_text',
text: 'Analysis Type'
}
}
],
submit: {
type: 'plain_text',
text: 'Analyze'
}
}
});
} catch (error) {
logger.error('Error opening analysis modal:', error);
}
});
// Handle modal submission
app.view('analysis_modal', async ({ ack, view, client, body }) => {
await ack();
try {
const metadata = JSON.parse(view.private_metadata);
const analysisType = view.state.values.analysis_type.type_select.selected_option.value;
const analysis = await analyzeMessage(metadata.message_text, analysisType);
// Send the analysis as a DM to the user
await client.chat.postMessage({
channel: body.user.id,
text: `*${analysisType.charAt(0).toUpperCase() + analysisType.slice(1)} Analysis:*\n${analysis}`
});
} catch (error) {
logger.error('Error processing analysis:', error);
}
});
}
module.exports = { setupEventHandlers };
Part 7: Message Formatting Utilities
Create src/utils/formatters.js for consistent message formatting:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
function formatCodeBlock(code, language = '') {
return `\`\`\`${language}\n${code}\n\`\`\``;
}
function formatBulletList(items) {
return items.map(item => `• ${item}`).join('\n');
}
function formatNumberedList(items) {
return items.map((item, index) => `${index + 1}. ${item}`).join('\n');
}
function formatQuote(text) {
return text.split('\n').map(line => `> ${line}`).join('\n');
}
function formatUserMention(userId) {
return `<@${userId}>`;
}
function formatChannelLink(channelId) {
return `<#${channelId}>`;
}
function formatTimestamp(timestamp, format = 'date_short_pretty') {
return `<!date^${Math.floor(timestamp)}^{${format}}|${new Date(timestamp * 1000).toLocaleDateString()}>`;
}
function truncateText(text, maxLength = 2000) {
if (text.length <= maxLength) return text;
return text.substring(0, maxLength - 3) + '...';
}
module.exports = {
formatCodeBlock,
formatBulletList,
formatNumberedList,
formatQuote,
formatUserMention,
formatChannelLink,
formatTimestamp,
truncateText
};
Part 8: Deployment and Production Considerations
Preparing for Production
Update your package.json with production scripts:
1
2
3
4
5
6
7
8
{
"scripts": {
"start": "node src/index.js",
"dev": "nodemon src/index.js",
"lint": "eslint src/",
"test": "jest"
}
}
Docker Deployment
Create a Dockerfile for containerized deployment:
1
2
3
4
5
6
7
8
9
10
11
12
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY src/ ./src/
USER node
CMD ["npm", "start"]
And a docker-compose.yml:
1
2
3
4
5
6
7
8
9
10
11
12
version: '3.8'
services:
slack-bot:
build: .
env_file:
- .env
restart: unless-stopped
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
Health Checks and Monitoring
Add a health check endpoint by modifying your main application:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const http = require('http');
// Add after app initialization
const healthServer = http.createServer((req, res) => {
if (req.url === '/health') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ status: 'healthy', timestamp: new Date().toISOString() }));
} else {
res.writeHead(404);
res.end();
}
});
healthServer.listen(3000, () => {
logger.info('Health check server running on port 3000');
});
Rate Limiting
Implement rate limiting to prevent abuse:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const rateLimits = new Map();
function checkRateLimit(userId, limit = 10, windowMs = 60000) {
const now = Date.now();
const userLimits = rateLimits.get(userId) || { count: 0, resetTime: now + windowMs };
if (now > userLimits.resetTime) {
userLimits.count = 0;
userLimits.resetTime = now + windowMs;
}
userLimits.count++;
rateLimits.set(userId, userLimits);
return userLimits.count <= limit;
}
Security Best Practices
- Validate all inputs: Never trust user input. Sanitize and validate before processing.
- Use environment variables: Keep secrets out of code.
- Implement request signing verification: Verify requests come from Slack.
- Limit data retention: Don’t store conversation history indefinitely.
- Audit logging: Log all bot actions for security review.
Part 9: Testing Your Bot
Local Testing
Run your bot in development mode:
1
npm run dev
Test the following scenarios:
- Send a direct message to your bot
- Mention the bot in a channel
- Use each slash command
- React to a message with :brain:
- Test the message shortcut (if configured)
Automated Testing
Create basic tests in tests/handlers.test.js:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const { generateResponse, analyzeMessage } = require('../src/neurolink');
describe('NeuroLink Integration', () => {
test('generates response for simple question', async () => {
const response = await generateResponse('What is 2+2?');
expect(response).toBeDefined();
expect(response.length).toBeGreaterThan(0);
});
test('analyzes sentiment correctly', async () => {
const sentiment = await analyzeMessage('I love this product!', 'sentiment');
expect(sentiment.toLowerCase()).toContain('positive');
});
});
What You Built
You built an AI-powered Slack bot with slash commands, conversation handling, mention responses, and AI-powered reactions using NeuroLink’s generation API. The bot processes natural language queries and returns intelligent responses directly in Slack channels and direct messages.
Remember to monitor your bot’s performance, gather user feedback, and iterate on the experience. The best bots evolve based on how teams actually use them.
For more advanced NeuroLink features, explore the documentation at docs.neurolink.ink. You’ll find capabilities like function calling, structured outputs, and fine-tuning that can make your Slack bot even more powerful.
Happy building!
Ready to transform your Slack workspace with AI? Sign up for NeuroLink at neurolink.ink and start building intelligent integrations today.
Related posts:
