June 23, 2026
Building Custom Slack Slash Commands — A Slack API Implementation Guide
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_SECRETvalidation (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
| Command | Use Case | Response Type |
|---|---|---|
/help | FAQ and usage guide | Ephemeral |
/report | Member reports | Ephemeral confirm + notify moderators |
/announce | Admin posts announcements | in_channel |
/standup | Collect standup updates (Modal) | Post summary to channel |
/tag @user | Assign tags / roles | Ephemeral confirm |
/digest | Summary 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 viarespond()orchat.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.
Related Articles
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.