NapNodes

← Discord bot guides

Discord bots · updated 24 October 2026

Slash Commands in Your Discord Bot: Options, Subcommands and Permissions

A /hello that replies is the first step; a real bot needs commands that ask for data (/announce text: …), that are grouped (/music play, /music stop), that only moderators can use and that don't fail when they take a while. Here's each piece, in discord.js and in discord.py, with the same example in both.

Before you start

We start from a bot that already runs and replies to /hello: if you don't have one yet, it's fifteen minutes with the discord.py guide or the discord.js one. The examples below use discord.js 14 (Node.js 20 or newer) and discord.py 2 (Python 3.10 or newer).

Each command has a name of 1 to 32 characters, lowercase and without spaces (hyphens are fine), and a description of 1 to 100. A bot can have up to 100 global slash commands and each command up to 25 options; if that's not enough, group them into subcommands.

Rather not set it up yourself? Discord bot hosting from €1.50 a month, no contract and a full refund within 72 hours. Or create a free Discord bot, no card needed.

Options: make the command ask for data

An option is a field Discord fills in with the right type: text, whole number, decimal number, yes/no, a user, a channel or a role. Discord won't let anyone send the command without the required ones or with a number outside the range you set, so your code doesn't have to check it. With choices (up to 25), the user picks from a list instead of typing.

The example: /dice, with the number of sides (from 2 to 100, 6 if they don't enter anything), and /announce, with a required text and a colour to pick.

index.js (discord.js)
const { SlashCommandBuilder } = require("discord.js");

const commands = [
  new SlashCommandBuilder()
    .setName("dice")
    .setDescription("Roll a die")
    .addIntegerOption((o) =>
      o.setName("sides").setDescription("Number of sides").setMinValue(2).setMaxValue(100)),
  new SlashCommandBuilder()
    .setName("announce")
    .setDescription("Post an announcement")
    .addStringOption((o) => o.setName("text").setDescription("What it says").setRequired(true))
    .addStringOption((o) =>
      o.setName("colour").setDescription("Announcement colour").addChoices(
        { name: "Red", value: "red" },
        { name: "Green", value: "green" },
      )),
];

// In InteractionCreate:
if (i.commandName === "dice") {
  const sides = i.options.getInteger("sides") ?? 6;
  await i.reply(`🎲 You rolled a ${1 + Math.floor(Math.random() * sides)}`);
}
if (i.commandName === "announce") {
  const text = i.options.getString("text", true);
  const colour = i.options.getString("colour") ?? "green";
  await i.reply(`${colour === "red" ? "🔴" : "🟢"} ${text}`);
}
app.py (discord.py)
import random

import discord
from discord import app_commands


@tree.command(name="dice", description="Roll a die")
@app_commands.describe(sides="Number of sides")
async def dice(interaction: discord.Interaction, sides: app_commands.Range[int, 2, 100] = 6):
    await interaction.response.send_message(f"🎲 You rolled a {random.randint(1, sides)}")


@tree.command(name="announce", description="Post an announcement")
@app_commands.describe(text="What it says", colour="Announcement colour")
@app_commands.choices(colour=[
    app_commands.Choice(name="Red", value="red"),
    app_commands.Choice(name="Green", value="green"),
])
async def announce(interaction: discord.Interaction, text: str, colour: str = "green"):
    emoji = "🔴" if colour == "red" else "🟢"
    await interaction.response.send_message(f"{emoji} {text}")

Subcommands: /music play, /music stop

When several commands are about the same thing, instead of /play-music and /stop-music you make one command with subcommands: on Discord they show up together when you type /music, and each subcommand has its own options. A command with subcommands can't be used on its own any more: plain /music doesn't exist.

index.js (discord.js)
new SlashCommandBuilder()
  .setName("music")
  .setDescription("Control the music")
  .addSubcommand((s) =>
    s.setName("play").setDescription("Play a song")
      .addStringOption((o) => o.setName("song").setDescription("Name or link").setRequired(true)))
  .addSubcommand((s) => s.setName("stop").setDescription("Stop the music"));

// In InteractionCreate:
if (i.commandName === "music") {
  const sub = i.options.getSubcommand(); // "play" or "stop"
}
app.py (discord.py)
music = app_commands.Group(name="music", description="Control the music")


@music.command(name="play", description="Play a song")
async def play(interaction: discord.Interaction, song: str):
    await interaction.response.send_message(f"Playing {song}")


@music.command(name="stop", description="Stop the music")
async def stop(interaction: discord.Interaction):
    await interaction.response.send_message("Music stopped")


tree.add_command(music)

Moderator-only commands

The safe way is to tell Discord which permission is needed to see the command: anyone without it doesn't even see it. In the example, /announce is only for people who can manage messages. Each server's admins can change it later in “Server Settings” → “Integrations” → your bot, without touching your code (for example, to give it to a specific role).

That permission decides who sees the command, not what your bot can do: if the command bans, the bot needs “Ban Members” (you tick it when inviting it, in “OAuth2” → “URL Generator”) and its role has to be above the person's.

index.js (discord.js)
const { PermissionFlagsBits } = require("discord.js");

new SlashCommandBuilder()
  .setName("announce")
  .setDescription("Post an announcement")
  .setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages);
app.py (discord.py)
@tree.command(name="announce", description="Post an announcement")
@app_commands.default_permissions(manage_messages=True)
async def announce(interaction: discord.Interaction, text: str):
    ...

Slow replies, and replies only the user can see

Discord expects the first reply within 3 seconds. If your command queries a website, a database or a game server, it can go over, and the user sees “The application did not respond” even though your bot finishes fine. The fix is to say first that you're on it (it shows “… is thinking”) and reply afterwards: from that notice you have 15 minutes.

For messages that only matter to whoever runs the command (an error, a confirmation, their account details), use an ephemeral reply: only they see it, with “Only you can see this”, and it doesn't clutter the channel.

index.js (discord.js)
const { MessageFlags } = require("discord.js");

await i.deferReply(); // “… is thinking”
const data = await somethingSlow();
await i.editReply(`Done: ${data}`);

// Only the person who ran the command sees it:
await i.reply({ content: "You have no pending announcements.", flags: MessageFlags.Ephemeral });
app.py (discord.py)
await interaction.response.defer()  # “… is thinking”
data = await something_slow()
await interaction.followup.send(f"Done: {data}")

# Only the person who ran the command sees it:
await interaction.response.send_message("You have no pending announcements.", ephemeral=True)

Autocomplete: suggestions while they type

Choices are for short, fixed lists. If the list is long or changes (songs, players, shop items), turn on autocomplete: while the user types, Discord asks your bot and it replies with up to 25 suggestions that match what's been typed. Careful: the user can ignore them and type something else, so check it when the command runs.

index.js (discord.js)
const FRUITS = ["apple", "banana", "melon", "orange", "pear"];

// When defining the option: .addStringOption((o) => o.setName("fruit").setDescription("Which one").setAutocomplete(true))

client.on(Events.InteractionCreate, async (i) => {
  if (i.isAutocomplete()) {
    const typed = i.options.getFocused().toLowerCase();
    const matches = FRUITS.filter((f) => f.startsWith(typed)).slice(0, 25);
    await i.respond(matches.map((f) => ({ name: f, value: f })));
    return;
  }
  // … the commands go here
});
app.py (discord.py)
FRUITS = ["apple", "banana", "melon", "orange", "pear"]


@tree.command(name="fruit", description="Pick a fruit")
async def fruit(interaction: discord.Interaction, fruit: str):
    await interaction.response.send_message(f"You picked {fruit}")


@fruit.autocomplete("fruit")
async def suggest_fruit(interaction: discord.Interaction, current: str):
    matches = [f for f in FRUITS if f.startswith(current.lower())][:25]
    return [app_commands.Choice(name=f, value=f) for f in matches]

Global or per server: when changes show up

Global commands show up on every server the bot is in; per-server ones, only on that server. While you're coding, register them on your test server: the change shows up straight away. When they're ready, switch them to global. If after registering you don't see a command or still see the old version, reload Discord (Ctrl+R on the computer, or close and reopen the app): it caches the list.

You don't need to register on every startup: only when you change a command (name, description or options). If you only change what it does inside, restarting the bot is enough. And Discord limits how many times a day commands can be created, so a bot that restarts in a loop and recreates them can end up unable to register them for a while. With set() (discord.js) or sync() (discord.py) you send the whole list: whatever isn't in your code any more disappears from Discord.

index.js (discord.js)
const TEST_SERVER = "123456789012345678"; // right-click your server → “Copy Server ID”

// Only on your test server (straight away):
await c.application.commands.set(commands.map((x) => x.toJSON()), TEST_SERVER);
// Global (on every server the bot is in):
await c.application.commands.set(commands.map((x) => x.toJSON()));
app.py (discord.py)
TEST_SERVER = discord.Object(id=123456789012345678)  # “Copy Server ID”

# Only on your test server (straight away):
tree.copy_global_to(guild=TEST_SERVER)
await tree.sync(guild=TEST_SERVER)
# Global (on every server the bot is in):
await tree.sync()

If the commands don't show up

  1. Check the bot's console

    A name with capitals or spaces, an empty description or one over 100 characters, or a required option after an optional one make Discord reject the whole list, and the error shows up at startup.

  2. Check that they get registered

    That the set() or sync() line really runs (for example, that it's inside “ready” and the bot gets as far as saying “Logged in as…”).

  3. Reload Discord

    Ctrl+R on the computer; on your phone, close the app completely and open it again.

  4. Invite it again

    If you invited it long ago without ticking “applications.commands”, use the link from “OAuth2” → “URL Generator” again with “bot” and “applications.commands” ticked: there's no need to kick it first.

  5. Check the permissions

    If the command has a default permission, anyone without it doesn't see it: try it with an admin account or check “Server Settings” → “Integrations”.

With your bot on NapNodes

For the commands to always work, the bot has to stay online even when you switch your computer off. On NapNodes you upload the code in “Files” (or pull it from your GitHub repository in “Settings”) and, after changing something, click “Restart”: errors when registering the commands show up in the “Console”. The whole process is in the Discord bot hosting guide.

The free bot has 256 MB and is renewed every week with one click; the Mini (512 MB) and Plus (1 GB) plans stay online all the time with nothing to renew (Discord bot pricing).

Frequently asked questions

Why does it say “The application did not respond”?

Because your bot took more than 3 seconds to give the first reply, or the command failed before replying (check the console). If it has to wait for something, use deferReply() in discord.js or interaction.response.defer() in discord.py and reply afterwards: you have 15 minutes.

Why don't my new commands show up?

It's almost always Discord's cache: reload it with Ctrl+R. If it still doesn't show up, check the bot's console in case Discord rejected the list, and register the commands on your test server, where they show up straight away.

How do I make a command moderator-only?

With a default permission: setDefaultMemberPermissions() in discord.js or @app_commands.default_permissions() in discord.py. Anyone without that permission doesn't see it, and admins can adjust it in “Server Settings” → “Integrations”.

How many slash commands can a bot have?

Up to 100 global ones, each with up to 25 options. If you need more, group them into subcommands (/music play, /music stop).

Do I have to register the commands every time the bot starts?

No: only when you change their name, description or options. If you only change what they do inside, restarting the bot is enough.

Shall we set it up for you?

Discord bot hosting from €1.50 a month, never oversold: your RAM is yours alone. And if you'd rather start without paying, there's a free Discord bot, no card needed.

Keep reading

All Discord bot guides, sorted →