How to Build a Slack Bot from Scratch
·APIScout Team
slack apislack botchatbottutorialapi integration
How to Build a Slack Bot from Scratch
Slack bots automate workflows, answer questions, and connect tools. This guide uses Bolt (Slack's official framework) to build a bot that handles slash commands, responds to messages, shows interactive modals, and runs on your server.
What You'll Build
- Slash command handler (
/weather,/status) - Message listener (responds to keywords)
- Interactive buttons and modals
- Scheduled messages
- App Home tab with dashboard
Prerequisites: Node.js 18+, Slack workspace where you can install apps.
1. Setup
Create a Slack App
- Go to api.slack.com/apps
- Click "Create New App" → "From scratch"
- Name it and select your workspace
- Under OAuth & Permissions, add these bot token scopes:
chat:write— send messagescommands— handle slash commandsapp_mentions:read— respond to @mentionsim:history— direct messages
- Install the app to your workspace
- Copy the Bot Token (
xoxb-...) and Signing Secret
Install Bolt
npm install @slack/bolt
Initialize
// app.ts
import { App } from '@slack/bolt';
const app = new App({
token: process.env.SLACK_BOT_TOKEN,
signingSecret: process.env.SLACK_SIGNING_SECRET,
socketMode: true, // For development (no public URL needed)
appToken: process.env.SLACK_APP_TOKEN, // Required for socket mode
});
(async () => {
await app.start(3000);
console.log('⚡️ Slack bot is running!');
})();
Environment Variables
SLACK_BOT_TOKEN=xoxb-...
SLACK_SIGNING_SECRET=your_signing_secret
SLACK_APP_TOKEN=xapp-... # For Socket Mode
2. Slash Commands
Register Command
In your Slack App settings → Slash Commands → Create New Command:
- Command:
/status - Request URL:
https://your-server.com/slack/events(or use Socket Mode)
Handle Command
app.command('/status', async ({ command, ack, respond }) => {
await ack(); // Acknowledge within 3 seconds
// Do your work (fetch data, check systems, etc.)
const status = await checkSystemStatus();
await respond({
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: `*System Status* :white_check_mark:\n• API: ${status.api}\n• Database: ${status.db}\n• Cache: ${status.cache}`,
},
},
{
type: 'context',
elements: [
{
type: 'mrkdwn',
text: `Last checked: ${new Date().toLocaleTimeString()}`,
},
],
},
],
});
});
3. Message Listeners
Respond to Keywords
// Listen for messages containing "help"
app.message(/help/i, async ({ message, say }) => {
await say({
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: 'Here\'s what I can do:',
},
},
{
type: 'section',
text: {
type: 'mrkdwn',
text: '• `/status` — Check system status\n• `/deploy` — Trigger deployment\n• Just mention me with a question!',
},
},
],
});
});
// Respond to @mentions
app.event('app_mention', async ({ event, say }) => {
await say({
text: `Hey <@${event.user}>! How can I help?`,
thread_ts: event.ts, // Reply in thread
});
});
4. Interactive Messages
Buttons
// Send a message with buttons
app.command('/deploy', async ({ command, ack, respond }) => {
await ack();
await respond({
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: `Deploy *${command.text || 'main'}* to production?`,
},
},
{
type: 'actions',
elements: [
{
type: 'button',
text: { type: 'plain_text', text: '✅ Deploy' },
style: 'primary',
action_id: 'deploy_confirm',
value: command.text || 'main',
},
{
type: 'button',
text: { type: 'plain_text', text: '❌ Cancel' },
style: 'danger',
action_id: 'deploy_cancel',
},
],
},
],
});
});
// Handle button clicks
app.action('deploy_confirm', async ({ action, ack, respond }) => {
await ack();
const branch = action.value;
await respond({
replace_original: true,
text: `🚀 Deploying *${branch}*... This may take a few minutes.`,
});
// Trigger actual deployment
await triggerDeploy(branch);
await respond({
text: `✅ *${branch}* deployed successfully!`,
});
});
app.action('deploy_cancel', async ({ ack, respond }) => {
await ack();
await respond({
replace_original: true,
text: '❌ Deployment cancelled.',
});
});
5. Modals
Open a Modal
app.command('/feedback', async ({ command, ack, client }) => {
await ack();
await client.views.open({
trigger_id: command.trigger_id,
view: {
type: 'modal',
callback_id: 'feedback_modal',
title: { type: 'plain_text', text: 'Send Feedback' },
submit: { type: 'plain_text', text: 'Submit' },
blocks: [
{
type: 'input',
block_id: 'feedback_type',
element: {
type: 'static_select',
action_id: 'type_select',
options: [
{ text: { type: 'plain_text', text: 'Bug Report' }, value: 'bug' },
{ text: { type: 'plain_text', text: 'Feature Request' }, value: 'feature' },
{ text: { type: 'plain_text', text: 'General' }, value: 'general' },
],
},
label: { type: 'plain_text', text: 'Type' },
},
{
type: 'input',
block_id: 'feedback_text',
element: {
type: 'plain_text_input',
action_id: 'text_input',
multiline: true,
placeholder: { type: 'plain_text', text: 'Describe your feedback...' },
},
label: { type: 'plain_text', text: 'Feedback' },
},
],
},
});
});
// Handle modal submission
app.view('feedback_modal', async ({ view, ack, client }) => {
await ack();
const type = view.state.values.feedback_type.type_select.selected_option?.value;
const text = view.state.values.feedback_text.text_input.value;
const userId = view.user.id;
// Process feedback (save to database, create ticket, etc.)
await saveFeedback({ type, text, userId });
// Notify the user
await client.chat.postMessage({
channel: userId,
text: `Thanks for your ${type} feedback! We'll review it shortly.`,
});
});
6. Scheduled Messages
// Send a daily standup reminder
import cron from 'node-cron';
cron.schedule('0 9 * * 1-5', async () => {
await app.client.chat.postMessage({
channel: '#engineering',
text: '🌅 *Daily Standup*\nWhat did you work on yesterday? What are you working on today? Any blockers?',
});
});
// Schedule a one-time message
await app.client.chat.scheduleMessage({
channel: '#general',
post_at: Math.floor(Date.now() / 1000) + 3600, // 1 hour from now
text: 'Reminder: Team meeting in 15 minutes!',
});
7. App Home Tab
app.event('app_home_opened', async ({ event, client }) => {
await client.views.publish({
user_id: event.user,
view: {
type: 'home',
blocks: [
{
type: 'header',
text: { type: 'plain_text', text: '🏠 Dashboard' },
},
{
type: 'section',
text: {
type: 'mrkdwn',
text: '*Quick Actions*',
},
},
{
type: 'actions',
elements: [
{
type: 'button',
text: { type: 'plain_text', text: '📊 System Status' },
action_id: 'home_status',
},
{
type: 'button',
text: { type: 'plain_text', text: '🚀 Deploy' },
action_id: 'home_deploy',
},
],
},
{ type: 'divider' },
{
type: 'section',
text: {
type: 'mrkdwn',
text: '*Recent Deployments*\n• `main` → production — 2 hours ago ✅\n• `feature/auth` → staging — 5 hours ago ✅',
},
},
],
},
});
});
Production Deployment
Switch from Socket Mode to HTTP
For production, use HTTP mode instead of Socket Mode:
const app = new App({
token: process.env.SLACK_BOT_TOKEN,
signingSecret: process.env.SLACK_SIGNING_SECRET,
// Remove socketMode and appToken
});
await app.start(process.env.PORT || 3000);
Set your Request URL in Slack App settings to: https://your-server.com/slack/events
Production Checklist
| Item | Notes |
|---|---|
| Use HTTP mode (not Socket Mode) | Socket Mode is dev only |
| Set Request URL to your production server | Required for events and commands |
| Enable retry on failure | Slack retries failed deliveries 3 times |
| Respond within 3 seconds | Use ack() immediately, then process async |
| Handle rate limits | 1 message per second per channel |
| Log all errors | Slack doesn't show your server errors |
Common Mistakes
| Mistake | Impact | Fix |
|---|---|---|
Not calling ack() within 3 seconds | Command appears to fail | Always ack first, process after |
| Sending too many messages | Rate limited (429) | Queue messages, respect 1/sec limit |
| Not handling errors | Bot silently fails | Wrap all handlers in try/catch |
| Hardcoding channel IDs | Breaks in different workspaces | Use channel names or let users choose |
| Not using threads for replies | Clutters channels | Reply with thread_ts |
Building communication tools? Compare Slack API vs Discord API on APIScout — bot platforms, features, and developer experience.