How to Build a Slack Bot: A Step-by-Step Guide

A Slack bot is an automated program that runs inside a Slack workspace, responding to messages, executing commands, and connecting external services to your team's conversations. Bots handle everything from answering questions and posting notifications to running deployments and managing workflows, all without leaving Slack.

What You Will Build

This tutorial walks you through building a Slack bot from scratch using the Bolt framework. By the end, you will have a working bot that can:

  • Respond to messages and mentions in channels.
  • Handle slash commands like /status or /deploy.
  • Send notifications to Slack channels when external events happen.
  • Use Block Kit to send rich, interactive messages.

Prerequisites

Before you start, make sure you have:

  • Node.js 18+ installed.
  • A Slack workspace where you have permission to install apps. Create a free workspace at slack.com if you need a sandbox for development.
  • ngrok installed for local development tunneling.
  • A code editor (VS Code recommended).

Step 1: Create a Slack App

Go to api.slack.com/apps and click Create New App. Choose From scratch, give your app a name like "My Bot," and select your development workspace.

After creation, you will land on the app configuration page. Take note of the Signing Secret under Basic Information. You will need this later.

Add Bot Scopes

Navigate to OAuth & Permissions in the sidebar. Under Bot Token Scopes, add:

  • chat:write — Send messages as the bot.
  • app_mentions:read — Detect when someone mentions the bot.
  • commands — Register slash commands.
  • channels:history — Read messages in public channels the bot is in.
  • im:history — Read direct messages sent to the bot.

Click Install to Workspace and authorize the app. Copy the Bot User OAuth Token (starts with xoxb-). Store it somewhere safe.

Step 2: Set Up Your Project

Create a new directory and initialize a Node.js project:

mkdir my-slack-bot && cd my-slack-bot
npm init -y
npm install @slack/bolt dotenv

Create a .env file for your credentials:

SLACK_BOT_TOKEN=xoxb-your-token-here
SLACK_SIGNING_SECRET=your-signing-secret-here
SLACK_APP_TOKEN=xoxp-your-app-token-here

Never commit this file to version control.

Step 3: Write the Bot

Create app.js with the basic Bolt setup:

const { App } = require('@slack/bolt');
require('dotenv').config();

const app = new App({
  token: process.env.SLACK_BOT_TOKEN,
  signingSecret: process.env.SLACK_SIGNING_SECRET,
  socketMode: true,
  appToken: process.env.SLACK_APP_TOKEN,
});

// Respond when someone mentions the bot
app.event('app_mention', async ({ event, say }) => {
  await say(`Hey <@${event.user}>! How can I help?`);
});

// Respond to direct messages
app.message('hello', async ({ message, say }) => {
  await say({
    blocks: [
      {
        type: 'section',
        text: {
          type: 'mrkdwn',
          text: `Hey there <@${message.user}>! :wave:`,
        },
        accessory: {
          type: 'button',
          text: { type: 'plain_text', text: 'Learn More' },
          action_id: 'learn_more_click',
        },
      },
    ],
  });
});

(async () => {
  await app.start();
  console.log('Bot is running');
})();

Enable Socket Mode

Socket Mode lets you develop locally without exposing a public URL. Go to your app settings, navigate to Socket Mode, and enable it. Generate an App-Level Token with the connections:write scope. Add this token to your .env file as SLACK_APP_TOKEN.

Run your bot:

node app.js

Mention your bot in any channel and it will respond.

Step 4: Add Slash Commands

Slash commands give users a clean way to interact with your bot. Go to Slash Commands in your app settings and click Create New Command.

Create a /status command:

  • Command: /status
  • Short Description: Check system status
  • Usage Hint: [service-name]

Handle the command in your bot code:

app.command('/status', async ({ command, ack, respond }) => {
  await ack();

  const service = command.text || 'all services';

  await respond({
    blocks: [
      {
        type: 'section',
        text: {
          type: 'mrkdwn',
          text: `:white_check_mark: *${service}* — All systems operational`,
        },
      },
      {
        type: 'context',
        elements: [
          {
            type: 'mrkdwn',
            text: `Checked by <@${command.user_id}> at <!date^${Math.floor(Date.now() / 1000)}^{date_short_pretty} {time}|just now>`,
          },
        ],
      },
    ],
  });
});

Always call ack() within 3 seconds of receiving a slash command. Slack expects a quick acknowledgment before you process the request.

Step 5: Handle Interactive Components

When your bot sends messages with buttons, menus, or other Block Kit components, you need action handlers to respond to clicks.

app.action('learn_more_click', async ({ body, ack, say }) => {
  await ack();
  await say(`<@${body.user.id}> clicked Learn More.`);
});

For more complex interactions, use modals:

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' },
      blocks: [
        {
          type: 'input',
          block_id: 'feedback_input',
          element: {
            type: 'plain_text_input',
            action_id: 'feedback_text',
            multiline: true,
          },
          label: { type: 'plain_text', text: 'Your Feedback' },
        },
      ],
      submit: { type: 'plain_text', text: 'Submit' },
    },
  });
});

app.view('feedback_modal', async ({ ack, view, body }) => {
  await ack();
  const feedback = view.state.values.feedback_input.feedback_text.value;
  console.log(`Feedback from ${body.user.id}: ${feedback}`);
});

Step 6: Send Notifications from External Events

One of the most useful things a Slack bot can do is send notifications when something happens outside of Slack. Connect your bot to webhooks, CI/CD pipelines, monitoring systems, or any external service.

const express = require('express');
const webhookApp = express();
webhookApp.use(express.json());

webhookApp.post('/webhook/deploy', async (req, res) => {
  const { service, version, status } = req.body;

  await app.client.chat.postMessage({
    token: process.env.SLACK_BOT_TOKEN,
    channel: '#deployments',
    blocks: [
      {
        type: 'section',
        text: {
          type: 'mrkdwn',
          text: status === 'success'
            ? `:rocket: *${service}* deployed successfully — v${version}`
            : `:x: *${service}* deployment failed — v${version}`,
        },
      },
    ],
  });

  res.sendStatus(200);
});

webhookApp.listen(3001);

For production notification workflows across multiple channels (Slack, email, push, in-app), consider using a notification API that handles delivery, user preferences, and retry logic.

Step 7: Deploy to Production

For production deployment, switch from Socket Mode to HTTP endpoints. This gives you better reliability and scalability.

Set Up HTTP Mode

Update your Bolt app configuration:

const app = new App({
  token: process.env.SLACK_BOT_TOKEN,
  signingSecret: process.env.SLACK_SIGNING_SECRET,
});

(async () => {
  await app.start(process.env.PORT || 3000);
  console.log('Bot is running on port 3000');
})();

Configure Request URL

Deploy your bot to a hosting provider (Railway, Render, AWS, or any platform that supports Node.js). Set your app's Request URL in the Slack app settings to point to your deployed endpoint:

  • Event Subscriptions: https://your-bot.example.com/slack/events
  • Interactivity: https://your-bot.example.com/slack/events

Bolt handles both event and interactivity payloads on the same endpoint by default.

Production Checklist

Before going live, verify:

  • Environment variables are set on your hosting platform.
  • HTTPS is enabled (Slack requires it).
  • Error handling covers network failures and API rate limits.
  • Logging captures enough detail to debug issues without exposing tokens.
  • Your bot handles the Slack Events API retry behavior (Slack resends events if it does not get a 200 response within 3 seconds).

Testing Your Bot

Test each capability before deploying:

  1. Mention the bot in a channel — verify it responds.
  2. Send a direct message — verify the interactive message appears with the button.
  3. Run /status — verify the formatted response.
  4. Click the button — verify the action handler fires.
  5. Send a webhook request — verify the deployment notification posts to the correct channel.

Use the Block Kit Builder to prototype message layouts before writing code.

Common Patterns for Slack Bots

Scheduled Messages

Post recurring updates using chat.scheduleMessage:

await app.client.chat.scheduleMessage({
  token: process.env.SLACK_BOT_TOKEN,
  channel: '#standup',
  text: 'Time for standup! What did you work on yesterday?',
  post_at: Math.floor(Date.now() / 1000) + 3600, // 1 hour from now
});

Thread Replies

Keep conversations organized by replying in threads:

app.event('app_mention', async ({ event, say }) => {
  await say({
    text: 'Working on it...',
    thread_ts: event.ts, // Reply in thread
  });
});

Error Handling

Wrap all handlers with proper error handling:

app.error(async (error) => {
  console.error('Unhandled error:', error.message);
});

Next Steps

Once your bot is running, consider these improvements:

  • Add a database to store user preferences or conversation state.
  • Integrate with GitHub to post PR notifications and deployment status.
  • Build an App Home tab using Block Kit to give users a dashboard inside Slack.
  • Connect external notification services like MagicBell to manage notifications across Slack, email, push, and in-app from a single API.
  • Submit to the Slack App Directory if you want to distribute your bot publicly. The review process takes 1-2 weeks.
Ready to Ship?

Add Slack Notifications to Your App

MagicBell makes it easy to send notifications to Slack channels and DMs with rich formatting.

Slack Notification Service