Get your free server today! View Plans →
Home Plans Blog About Contact Panel Join Discord
Discord Bots

Moving your Discord bot to slash commands

A practical guide to switching your Discord bot from message prefix commands to slash commands, with working examples in both discord.py and discord.js.

Moving your Discord bot to slash commands

If your bot still listens for messages that start with ! or ?, you've probably noticed Discord pushing people toward slash commands for a while now. Moving over isn't as scary as it looks. This guide walks through why the change happened and how to register slash commands in both discord.py and discord.js without breaking your bot.

Why slash commands took over

Old prefix commands worked by reading the content of every message and checking if it started with your prefix. That meant your bot had to see what people were typing. To read message text now, you need the message content intent, and for bigger bots that intent has to be approved by Discord. So the old way got harder to keep running.

Slash commands fixed a few real problems at once. When someone types /, Discord shows them a list of your commands with descriptions, so people can actually find what your bot does. There's no guessing the right prefix or spelling. Discord validates the input before it ever reaches your code, which cuts down on people sending garbage arguments. And because Discord handles the interaction, your bot doesn't need to scan every message in a busy channel.

Honestly, the discoverability alone is worth it. We've seen plenty of community servers where half the members never knew a bot had cool features, simply because nobody told them the prefix. With slash commands, the features are right there the moment you type a slash.

Global commands versus guild commands

Before you write a single command, you need to understand where it lives. Discord splits slash commands into two kinds, and the difference will save you a lot of confusion.

Global commands are available in every server your bot is in, plus in DMs. They sound great, but there's a catch. Global commands can take up to an hour to show up or update across Discord. So you change a command, restart your bot, and then sit there wondering why nothing happened. It did happen. You just have to wait.

Guild commands are registered to one specific server using its guild ID. These update almost instantly, usually within a couple of seconds. That makes them perfect for testing and for bots that only ever live in one or two servers.

So the simple rule is this. While you're building and testing, register commands to your own test server as guild commands. Once everything works, switch to global so the whole world can use them. A quick warning: if you register the same command both globally and to a guild, users in that guild will see it twice. Pick one.

Registering commands with discord.py app_commands

In modern discord.py (version 2.0 and up) slash commands live under app_commands, and you sync them with a command tree. The key word here is sync. Writing the command isn't enough. You have to tell Discord about it by calling tree.sync().

Here's a small bot with one slash command, set up to sync to a single test guild for fast updates.

import discord
from discord import app_commands

GUILD_ID = 123456789012345678  # your test server id

intents = discord.Intents.default()
client = discord.Client(intents=intents)
tree = app_commands.CommandTree(client)

@tree.command(name="ping", description="Check if the bot is awake", guild=discord.Object(id=GUILD_ID))
async def ping(interaction: discord.Interaction):
    await interaction.response.send_message("Pong!")

@client.event
async def on_ready():
    await tree.sync(guild=discord.Object(id=GUILD_ID))
    print(f"Logged in as {client.user}")

client.run("YOUR_TOKEN")

Notice you no longer need the message content intent for this at all, since you're not reading message text. When you're ready to go global, drop the guild= argument from both the command decorator and the sync call. Then be patient while the global update propagates.

One thing that trips people up: don't call tree.sync() on every startup if you go global, because Discord rate limits syncs. Sync when you actually change commands, not every single boot.

Registering commands with discord.js and SlashCommandBuilder

discord.js does it a little differently. You build each command with SlashCommandBuilder, then push them to Discord through the REST API. The registration step is usually its own script that you run once, separate from your main bot file.

Here's a deploy script that registers commands to a single guild.

const { REST, Routes, SlashCommandBuilder } = require('discord.js');

const commands = [
  new SlashCommandBuilder().setName('ping').setDescription('Check if the bot is awake'),
].map(command => command.toJSON());

const rest = new REST({ version: '10' }).setToken(process.env.TOKEN);

(async () => {
  await rest.put(
    Routes.applicationGuildCommands(CLIENT_ID, GUILD_ID),
    { body: commands },
  );
  console.log('Guild commands registered.');
})();

The important bit is Routes.applicationGuildCommands(CLIENT_ID, GUILD_ID). That's the route that registers to one server, so the commands appear right away. When you want them everywhere, swap that line for Routes.applicationCommands(CLIENT_ID), which is the global route. Same wait time applies as with discord.py.

Then in your main bot file, you handle the interaction when someone runs the command.

client.on('interactionCreate', async interaction => {
  if (!interaction.isChatInputCommand()) return;
  if (interaction.commandName === 'ping') {
    await interaction.reply('Pong!');
  }
});

Keep your registration logic and your handling logic separate. Run the deploy script when commands change. Run the bot the rest of the time.

Options, choices and ephemeral replies

Slash commands get genuinely nice once you add options. An option is an input the user fills in, and Discord builds the form for you. You can make options required or optional, and you can lock them to a set of choices so people can't type something unexpected.

Here's a discord.py command that takes a string option with a few fixed choices.

@tree.command(name="role", description="Pick a class", guild=discord.Object(id=GUILD_ID))
@app_commands.describe(choice="Which class do you want?")
@app_commands.choices(choice=[
    app_commands.Choice(name="Warrior", value="warrior"),
    app_commands.Choice(name="Mage", value="mage"),
    app_commands.Choice(name="Healer", value="healer"),
])
async def role(interaction: discord.Interaction, choice: app_commands.Choice[str]):
    await interaction.response.send_message(f"You picked {choice.name}", ephemeral=True)

See that ephemeral=True at the end? That makes the reply visible only to the person who ran the command. Nobody else in the channel sees it. This is great for settings, confirmations, error messages, or anything that would just clutter the chat for everyone else. Use it whenever the reply is meant for one person.

In discord.js, options are added with builder methods, and ephemeral replies use a flag.

new SlashCommandBuilder().setName('role').setDescription('Pick a class').addStringOption(option =>
    option.setName('choice').setDescription('Which class do you want?').setRequired(true).addChoices(
        { name: 'Warrior', value: 'warrior' },
        { name: 'Mage', value: 'mage' },
        { name: 'Healer', value: 'healer' },
      ))

// in your handler:
const choice = interaction.options.getString('choice');
await interaction.reply({ content: `You picked ${choice}`, ephemeral: true });

Discord supports other option types too, like integers, users, channels and booleans. Reach for them instead of asking people to type a username as plain text. If you let users pick a real user object, you skip a whole class of typos and lookups.

Test in one server before you go wide

This is the part people skip and then regret. Always register and test as guild commands in a server you control first. Because guild commands update in seconds, you can change a description, fix a typo, tweak an option, and see it immediately. Trying to do that with global commands means waiting up to an hour each time, which turns a five minute fix into an afternoon.

A workflow that holds up well:

It also helps to add a short reply timeout in your head. If your command does something slow, like calling an external API or hitting a database, Discord expects a response within three seconds. If you can't answer that fast, defer the reply first with interaction.response.defer() in discord.py or interaction.deferReply() in discord.js, then send the real answer when it's ready. Forgetting this is the most common reason a working command suddenly shows "This interaction failed".

Wrapping up

Switching to slash commands mostly comes down to two ideas. Register your commands properly, either to a guild for fast testing or globally for everyone, and respond to the interaction within the time Discord gives you. Once those click, the rest is just adding options and tidying up your replies. Start with one command on your test server, get it syncing cleanly, then build out from there. If you're running your bot on Bytte.cloud, the panel keeps it online while you iterate, so you can deploy a change and try it in Discord straight away.

Common questions

Why should I switch from prefix commands to slash commands?

Slash commands are easier for people to discover, since typing a slash shows your bot's commands with descriptions. Discord also validates the input before it reaches your code, and you usually don't need the message content intent anymore because you're not reading raw message text.

What is the difference between global and guild commands?

Guild commands are registered to one specific server and update within a few seconds, which makes them ideal for testing. Global commands work in every server your bot is in but can take up to an hour to appear or update across Discord. Use guild commands while building, then switch to global when you're ready.

Why do my new slash commands not show up right away?

If you registered them globally, Discord can take up to an hour to propagate the change. Nothing is broken, you just have to wait. For instant updates during development, register the commands to a single test guild instead.

What is an ephemeral reply and when should I use it?

An ephemeral reply is only visible to the person who ran the command, not the whole channel. It's great for settings, confirmations, and error messages that would otherwise clutter the chat. In discord.py you set ephemeral=True, and in discord.js you pass ephemeral: true on the reply.

Why does my command sometimes say This interaction failed?

Discord expects a response within about three seconds. If your command does something slow like calling an API or a database, defer the reply first with interaction.response.defer() in discord.py or interaction.deferReply() in discord.js, then send the real answer when it's ready.

PN
Priya Nair
Community Manager at Bytte.cloud

Part of the Bytte.cloud team. We run game servers, bots and websites for a living, and we write these guides from what we see day to day in support and on our own servers.

Want to try this on real hardware?

Bytte.cloud has free plans for game servers, bots and websites. No credit card, set up in seconds.

Start for free See the plans