Skip to content

June 23, 2026

Building Custom Slack Slash Commands — A Slack API Implementation Guide

CommunitySlackEngineeringAI

Slash Commands Turn Slack Into Your Team’s Control Panel

As mapped out in What You Can Do with Slack Bots and APIs, Slack automation spans messaging, interactions, and workflows. The most accessible entry point is the slash command.

Type /help and an FAQ appears. Run /report @user and a moderator gets a notification. Execute /standup and the bot collects everyone’s update. Slash commands let members trigger operations without learning a special UI — they just type a command. For community operations, they are as important as they are in Discord.

How It Works

User types /hello and sends it
 → Slack POSTs to your Request URL (application/x-www-form-urlencoded)
 → Your server validates the Slack signature
 → You return a response within 3 seconds (or send ack())
 → If needed, post additional content asynchronously via response_url or Bolt's say()

Unlike Discord, Slack sends command arguments as a raw text string. To receive structured input, you either parse the string yourself or open a modal to present form fields.

Implementation: Building with Bolt for JavaScript (Node.js)

Slack’s official Bolt framework handles signature verification, event routing, and response boilerplate so you can focus on your command logic.

Install

npm install @slack/bolt

Minimal Slash Command

// src/index.ts
import { App } from '@slack/bolt';

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

// respond to /hello
app.command('/hello', async ({ command, ack, respond }) => {
  await ack(); // acknowledge within 3 seconds

  await respond({
    text: `Hello, <@${command.user_id}>!`,
    response_type: 'in_channel', // visible to everyone; use 'ephemeral' for sender only
  });
});

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

response_type: 'in_channel' posts the reply so the whole channel can see it. 'ephemeral' shows the reply only to the person who ran the command. Notifications and confirmations → ephemeral. Shared information → in_channel.

Example: A Moderation Notification Command

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

  const text = command.text.trim();

  // notify the moderator channel
  await client.chat.postMessage({
    channel: process.env.MODERATOR_CHANNEL_ID!,
    blocks: [
      {
        type: 'section',
        text: {
          type: 'mrkdwn',
          text: `🚨 *New Report*\n*From:* <@${command.user_id}>\n*Details:* ${text || '(no details provided)'}`,
        },
      },
      {
        type: 'context',
        elements: [
          {
            type: 'mrkdwn',
            text: `Channel: <#${command.channel_id}> | Received: <!date^${Math.floor(Date.now() / 1000)}^{date_pretty} {time}|${new Date().toISOString()}>`,
          },
        ],
      },
    ],
  });

  // confirm to the reporter (ephemeral)
  await respond({
    text: 'Your report has been received. A moderator will review it.',
    response_type: 'ephemeral',
  });
});

Using a Modal for Structured Input

Free-text arguments lead to inconsistent reports. A modal forces structured input through form fields.

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

  await client.views.open({
    trigger_id: command.trigger_id,
    view: {
      type: 'modal',
      callback_id: 'report_modal',
      title: { type: 'plain_text', text: 'Report a Member' },
      submit: { type: 'plain_text', text: 'Submit' },
      blocks: [
        {
          type: 'input',
          block_id: 'user_block',
          element: {
            type: 'users_select',
            action_id: 'user',
            placeholder: { type: 'plain_text', text: 'Select the member to report' },
          },
          label: { type: 'plain_text', text: 'Member' },
        },
        {
          type: 'input',
          block_id: 'reason_block',
          element: {
            type: 'plain_text_input',
            action_id: 'reason',
            multiline: true,
            placeholder: { type: 'plain_text', text: 'Describe the situation in detail' },
          },
          label: { type: 'plain_text', text: 'Reason / Situation' },
          optional: true,
        },
      ],
    },
  });
});

// handle modal submission
app.view('report_modal', async ({ ack, view, body, client }) => {
  await ack();

  const userId = view.state.values.user_block.user.selected_user;
  const reason = view.state.values.reason_block.reason.value ?? '(no reason given)';

  await client.chat.postMessage({
    channel: process.env.MODERATOR_CHANNEL_ID!,
    text: `🚨 Report: <@${userId}> | Reason: ${reason} | Reported by: <@${body.user.id}>`,
  });
});

Vibe-Coding Slash Commands

Here is an effective prompt for generating this kind of implementation with AI coding tools (Claude, etc.):

I want to build a Slack slash command in TypeScript.
Framework: Bolt for JavaScript + Socket Mode

Command: /feedback
Behavior:
  - Open a modal (single textarea: feedback content, required)
  - On modal submit, post to #feedback channel using Block Kit
    (sender name, content, timestamp)
  - Send the submitter an ephemeral "Received" confirmation

Please also generate package.json scripts and .env variable names.

Always verify the generated code for:

  • SLACK_SIGNING_SECRET validation (prevents spoofing)
  • ack() within 3 seconds (async pattern for slow operations)
  • Error handling (channel not found, insufficient permissions, etc.)

If you don’t understand a part of the code, ask the AI to explain it before deploying. Understanding what the code does is especially important for anything touching member data or moderation.

Common Command Patterns

CommandUse CaseResponse Type
/helpFAQ and usage guideEphemeral
/reportMember reportsEphemeral confirm + notify moderators
/announceAdmin posts announcementsin_channel
/standupCollect standup updates (Modal)Post summary to channel
/tag @userAssign tags / rolesEphemeral confirm
/digestSummary of recent posts (LLM)Ephemeral

For the broader philosophy of expressing community operations as code, see Expressing Community Operations as Code — 3 Elements Implemented in Slack.

Summary

  • Slack slash commands are configured at api.slack.com with a command name and Request URL
  • The Bolt framework handles signature verification, routing, and response boilerplate
  • ack() must be called within 3 seconds; heavy processing must go async via respond() or chat.postMessage
  • Modals let you collect structured input, which is cleaner than parsing free text
  • For vibe-coding, specify command name, behavior, library, and .env variable names explicitly

Once slash commands are working, the next automation targets are scheduled messages, reaction triggers, and LLM integration.


Looking for help designing and implementing Slack bots for your community? See Rokuse’s Community Development Services.

Contact · Rokuse LLC

Continue this conversation about your community.

If a moment in this article made you wonder "what about ours?", send that exact question. It does not have to be polished — we will work the entry point out together.

Frequently asked questions

Q. Where do I configure Slack slash commands?
A. Create an app at api.slack.com/apps and add a new command under "Slash Commands." You set the command name (e.g. /hello), the Request URL (where Slack POSTs when the command is run), and a description. Enter your server or Cloudflare Workers URL as the Request URL.
Q. Is it true that slash commands must respond within 3 seconds?
A. Yes. Slack requires a response within 3 seconds of command execution. If your processing takes longer, immediately return a 200 OK with an empty body (or call ack()), then post the actual result asynchronously via response_url or Bolt's say(). Heavy tasks like LLM calls or database queries almost always require this async pattern.
Q. Can I pass arguments to a slash command?
A. Yes. Text typed after the command name arrives in the text parameter (e.g. /report @tanaka rule violation sends "@tanaka rule violation" as text). Unlike Discord, Slack's standard slash commands don't have typed arguments, so you either parse the text string yourself or open a modal to collect structured input. Bolt v4 added Suggestions support for autocomplete in some components.
Q. Can I add a slash command to an existing Slack bot app?
A. Yes. Go to api.slack.com/apps, select your app, and add the command under "Slash Commands." After adding, you may need to reinstall the app to the workspace (re-authorize) or have an admin approve it. You also need to add the corresponding handler in your implementation.