Skip to content

Instantly share code, notes, and snippets.

@AbstractUmbra
Last active February 19, 2024 18:08
  • Star 179 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save AbstractUmbra/a9c188797ae194e592efe05fa129c57f to your computer and use it in GitHub Desktop.
discord.py 2.0+ slash command info and examples

This gist has now been 'deprecated' and has moved...

... to my blog style space for easier contribution by third parties and to provide what I believe to be an easier reading experience. Please field all enquiries and issues to the source repository.

Slash Commands and you

This short example will cover how to make slash commands within an ext.commands.Bot's extension and Cog ecosystem.

This will cover most slash situations within discord.py

I added how this command would be invoked in the doc strings.

This does not account for the fact you still need to sync Bot.tree somewhere, please remember to do this.

N.B: Ping Umbra#0009 in the d.py server if you want to suggest edits/improvements.

Syncing the command tree in discord.py

This seems to be a common "gotcha" with users new to the library. As such I have been asked to cover it a bit more here.

CommandTree.sync is how we make Discord aware of our command definitions. This means that we use an API request to send them a copy of what our commands look like and act like, so they can display it to your users in the client. If you do not sync your tree, the commands will not show up, or update them if you make changes locally.

I cover some more items relating to this in the next file below. But for now let's cover the basics and what you can do here.

Common caveats

In my time as Helper, I see people syncing their CommandTree in the on_ready_event or in the new setup_hook entrypoint method. I do not advise this personally, it can lead to footguns if you aren't prepared. For examples, if you sync your tree before you load your extensions (which have your application commands), then you're effectively syncing an empty tree to Discord, which will wipe your commands. If you sync your tree and then make changes, you rely on the autosync and forget to sync changes, resulting in errors and failing commands.

This is why it is strongly recommended to sync on demand with a command (ideally with a message command) and know when to do such things. I cover that later too.

I'll add more things when I can think of them.

Sync examples and details

What I've been recommending to people, is to maintain a sync command as a Message Command (@bot.command()) or some sort of message invoke. Bots without the message content intent will still receive content of messages that mention them or are in DMs.

It's not like you can use a slash command to sync... when that command isn't synced.

Syncing gotchas

There is a ratelimit on syncing global commands which add commands. Updating commands (currently) has no ratelimit.

It is still recommended to sync your commands to a test guild before syncing them globally. Discord.py has added a helper method to do just that: CommandTree.copy_global_to

This util is used as follows:

# you have a defined Bot, with a tree and have commands added to it that are global.

guild = ctx.guild or discord.Object(id=...)  # you can use a full discord.Guild as the method accepts a Snowflake
Bot.tree.copy_global_to(guild=guild)

All this method does is copy your defined global commands (so ones without a guild or guilds kwarg, or without the @app_commands.guilds() decorator) to the specified guild within the CommandTree. When you use this method you must sync afterward still, you can refer to when_to_sync.md for details there.

Sync command example


from typing import Literal, Optional
from discord.ext import commands
from discord.ext.commands import Greedy, Context # or a subclass of yours

@bot.command()
@commands.guild_only()
@commands.is_owner()
async def sync(
  ctx: Context, guilds: Greedy[discord.Object], spec: Optional[Literal["~", "*", "^"]] = None) -> None:
    if not guilds:
        if spec == "~":
            synced = await ctx.bot.tree.sync(guild=ctx.guild)
        elif spec == "*":
            ctx.bot.tree.copy_global_to(guild=ctx.guild)
            synced = await ctx.bot.tree.sync(guild=ctx.guild)
        elif spec == "^":
            ctx.bot.tree.clear_commands(guild=ctx.guild)
            await ctx.bot.tree.sync(guild=ctx.guild)
            synced = []
        else:
            synced = await ctx.bot.tree.sync()

        await ctx.send(
            f"Synced {len(synced)} commands {'globally' if spec is None else 'to the current guild.'}"
        )
        return

    ret = 0
    for guild in guilds:
        try:
            await ctx.bot.tree.sync(guild=guild)
        except discord.HTTPException:
            pass
        else:
            ret += 1

    await ctx.send(f"Synced the tree to {ret}/{len(guilds)}.")

Syncing your commands

At the time of writing this, it's still mostly unclear to most users when they should be syncing their commands to Discord.

Let's try and write up some bullet points:-

When to sync

  • When you add a new command.
  • When you remove a command.
  • When a command's name or description changes.
  • When the callback's parameters change.
    • This includes parameter names, types or descriptions.
    • Also when you add or remove a parameter.
  • If you change a global to a guild command, or vice versa.
    • NOTE: If you do this, you will need to sync both global and to that guild to reflect the change.

These are currently the only times you should re-sync.

import discord
from discord import app_commands
from discord.ext import commands
class MyCog(commands.Cog):
def __init__(self, bot: commands.Bot) -> None:
self.bot = bot
@app_commands.command(name="command-1")
async def my_command(self, interaction: discord.Interaction) -> None:
""" /command-1 """
await interaction.response.send_message("Hello from command 1!", ephemeral=True)
@app_commands.command(name="command-2")
@app_commands.guilds(discord.Object(id=...), ...)
async def my_private_command(self, interaction: discord.Interaction) -> None:
""" /command-2 """
await interaction.response.send_message("Hello from private command!", ephemeral=True)
async def setup(bot: commands.Bot) -> None:
await bot.add_cog(MyCog(bot))
# for simplicity, these commands are all global. You can add `guild=` or `guilds=` to `Bot.add_cog` in `setup` to add them to a guild.
import discord
from discord import app_commands
from discord.ext import commands
class MyCog(commands.GroupCog, name="parent"):
def __init__(self, bot: commands.Bot) -> None:
self.bot = bot
super().__init__() # this is now required in this context.
@app_commands.command(name="sub-1")
async def my_sub_command_1(self, interaction: discord.Interaction) -> None:
""" /parent sub-1 """
await interaction.response.send_message("Hello from sub command 1", ephemeral=True)
@app_commands.command(name="sub-2")
async def my_sub_command_2(self, interaction: discord.Interaction) -> None:
""" /parent sub-2 """
await interaction.response.send_message("Hello from sub command 2", ephemeral=True)
async def setup(bot: commands.Bot) -> None:
await bot.add_cog(MyCog(bot))
# or if you want guild/guilds only...
await bot.add_cog(MyCog(bot), guilds=[discord.Object(id=...)])
import discord
from discord import app_commands
from discord.ext import commands
class MyCog(commands.Cog):
def __init__(self, bot: commands.Bot) -> None:
self.bot = bot
group = app_commands.Group(name="parent", description="...")
# Above, we declare a command Group, in discord terms this is a parent command
# We define it within the class scope (not an instance scope) so we can use it as a decorator.
# This does have namespace caveats but i don't believe they're worth outlining in our needs.
@app_commands.command(name="top-command")
async def my_top_command(self, interaction: discord.Interaction) -> None:
""" /top-command """
await interaction.response.send_message("Hello from top level command!", ephemeral=True)
@group.command(name="sub-command") # we use the declared group to make a command.
async def my_sub_command(self, interaction: discord.Interaction) -> None:
""" /parent sub-command """
await interaction.response.send_message("Hello from the sub command!", ephemeral=True)
async def setup(bot: commands.Bot) -> None:
await bot.add_cog(MyCog(bot))
import asyncio
from discord.ext import commands
from discord import app_commands
# define Bot with **needed** parameters
bot = commands.Bot(command_prefix="some_prefix", intents=some_intents_definition)
# You can now use `@bot.tree.command()` as a decorator:
@bot.tree.command()
async def my_command(interaction: discord.Interaction) -> None:
await interaction.response.send_message("Hello from my command!")
### NOTE: the above is a global command, see the `main()` func below:
# we can even use Groups
group = app_commands.Group(name="some-parent", description="description")
@group.command()
async def my_subcommand(interaction: discord.Interaction) -> None:
await interaction.response.send_message("hello from the subcommand!")
bot.tree.add_command(group, guild=discord.Object(id=...))
async def main():
async with bot:
# do you setup stuff if you need it here, then:
bot.tree.copy_global_to(guild=discord.Object(id=...)) # we copy the global commands we have to a guild, this is optional
await bot.start(MY_TOKEN)
# We still need to sync this tree somehow, but you can make a command as discussed already.
# discord.py recently added full hybrid commands. They work as follows:
## Note: as I don't see a reason not to, I will present an example using a commands.Cog.
## IMPORTANT: hybrid commands only work if the signature is compatible with app commands.
# this means that all parameters must have a type annotation, even if it is just `str`.
# this also means that you must use `Transformers` not `Coverters` in these cases.
import discord
from discord.ext import commands
class MyCog(commands.Cog):
def __init__(self, bot: commands.Bot) -> None:
self.bot: commands.Bot = bot
@commands.hybrid_command(name="ping")
async def ping_command(self, ctx: commands.Context) -> None:
"""
This command is actually used as an app command AND a message command.
This means it is invoked with `?ping` and `/ping` (once synced, of course).
"""
await ctx.send("Hello!")
# we use ctx.send and this will handle both the message command and app command of sending.
# added note: you can check if this command is invoked as an app command by checking the `ctx.interaction` attribute.
@commands.hybrid_group(name="parent")
async def parent_command(self, ctx: commands.Context) -> None:
"""
We even have the use of parents. This will work as usual for ext.commands but will be un-invokable for app commands.
This is a discord limitation as groups are un-invokable.
"""
... # nothing we want to do in here, I guess!
@parent_command.command(name="sub")
async def sub_command(self, ctx: commands.Context, argument: str) -> None:
"""
This subcommand can now be invoked with `?parent sub <arg>` or `/parent sub <arg>` (once synced).
"""
await ctx.send(f"Hello, you sent {argument}!")
async def setup(bot: commands.Bot) -> None:
await bot.add_cog(MyCog(bot))
# This an App command group.
# You can nest these down a few levels, like so:-
# /group subcommand (up to 25)
# /group subcommand group
# /group subcommand group subcommand (up to 25).
# this example shows one way to do this, subclassing
# the other is constructing an instance of app_commands.Group()
# that one is shown in "free_function_commands-py"
import discord
from discord import app_commands
# the @app_commands.guilds and @app_commands.default_permissions decorators (also including checks) can be used above the class.
# these will apply to ALL subcommands, subcommands cannot have invidual perms!
@app_commands.guild_only()
class Group(app_commands.Group):
# subcommand of Group
@app_commands.command()
async def my_subcommand(self, interaction: discord.Interaction) -> None:
await interaction.response.send_message("hello from the subcommand!")
# nested group command
group2 = app_commands.Group(name="group2", description="This a nested group!")
# subcommand of group2
@group2.command()
async def my_second_group_command(self, interaction: discord.Interaction) -> None:
await interaction.response.send_message("hello from the second subcommand!")
# unlike commands.GroupCog, you need to add this class to your tree yourself.
tree.add_command(Group())
@Tyskiep99
Copy link

Tyskiep99 commented Aug 24, 2022

I've got one more question, please, I have trouble with the Greedy param from @AbstractUmbra example of sync command, it seems it doenst work:

discord.ext.commands.errors.ExtensionFailed: Extension 'eqdkp_ng.cogs.admin' raised an error: TypeError: unhashable type: 'Greedy'

my import in the cog is as follow:

import discord
from discord.ext import commands
# and the params:
guilds: commands.Greedy[discord.Object]

@AbstractUmbra
Copy link
Author

It is an annotation, not an assignment.

You have =, you should have :

@Tyskiep99
Copy link

sorry that was a typo, it is a : I also tried with guilds: commands.Greedy[discord.Guild] but same effect, I use it in a @app_commands.command() in the cog maybe that is why? Is it only reserved to old bot.command ?

@AbstractUmbra
Copy link
Author

Yes, app commands have no capability of consumption arguments. The sync command presented is designed as a Message command only.

@Tyskiep99
Copy link

Thanks a lot for the information.
Altho I have another question sorry to bother but I cant any information anywhere about it,
I have an app command using my models from db to load items for a converter (event: Enum('Events', Event.get_event_dict_enum_from_db())).
And I have another command to add new entry in the db. I dont use bot.tree.copy_global_to (should I?) I also dont use bot.tree.sync anywhere (yet).
The issue I am facing is when I add a new entry for the converter in the db, and after issuing the !sync or !sync ~ command, the list is not updated. I tried to put a .sync after the entry is added in the adding command but without success too.
How would you tackle this? Is it even possible? (the command converter list get updated after a reboot of the bot and a sync) - the bot is intended to be used only within one guild

@AbstractUmbra
Copy link
Author

This would be better discussed in the discord.py server as I'm not fully understanding what you're asking.

You do need to sync your slash commands, but read over this first.

@in-op
Copy link

in-op commented Sep 20, 2022

Are we supposed to call setup ourselves via asyncio? It's not really clear. I'm coming from the old and easy on_message() and I'm totally lost.

@dbxflame6666
Copy link

and how do you load em to use em??? i cant load em in any wise idk

@Soheab
Copy link

Soheab commented Oct 3, 2022

Hello Umbra, want to suggest some changes for #file-9-subclassing_group-py...

- # this a app command group
+ # this is an App command group
- # you can nest these up 1: 
- # `/group group` = VALID
+ # Valid combinations:
+ # /group subcommand (up to 25)
+ # /group subcommand group
+ # /group subcommand group subcommand (up to 25)
- # /group group group` = INVALID
- # etc... all invalid.

# this example shows one way to do this, subclassing
# the other is constructing an instance of app_commands.Group()
# that one is shown in "free_function_commands-py"

import discord
from discord import app_commands

- # the @app_commands.guilds and others (including checks) can be used above the class
+ # checks like @app_commands.guilds and @app_commands.default_permissions can be used above the class
- # these will apply to ALL subcommand, subcommands cannot have invidual perms!
+ # these will apply to all subcommands, subcommands cannot have individual perms.
@app_commands.guild_only()
class Group(app_commands.Group):

@AbstractUmbra
Copy link
Author

Hello Umbra, want to suggest some changes for #file-9-subclassing_group-py...

- # this a app command group
+ # this is an App command group
- # you can nest these up 1: 
- # `/group group` = VALID
+ # Valid combinations:
+ # /group subcommand (up to 25)
+ # /group subcommand group
+ # /group subcommand group subcommand (up to 25)
- # /group group group` = INVALID
- # etc... all invalid.

# this example shows one way to do this, subclassing
# the other is constructing an instance of app_commands.Group()
# that one is shown in "free_function_commands-py"

import discord
from discord import app_commands

- # the @app_commands.guilds and others (including checks) can be used above the class
+ # checks like @app_commands.guilds and @app_commands.default_permissions can be used above the class
- # these will apply to ALL subcommand, subcommands cannot have invidual perms!
+ # these will apply to all subcommands, subcommands cannot have individual perms.
@app_commands.guild_only()
class Group(app_commands.Group):

Done.

@lpkeates
Copy link

I know this is old, but do you have anything to add an input in the command, like /ban user:<user> reason:[reason] with <> being required and [] is optional?

@DoubleF3lix
Copy link

I know this is old, but do you have anything to add an input in the command, like /ban user:<user> reason:[reason] with <> being required and [] is optional?

You can give it a default value of something like None and check that in your function accordingly.

@KenwoodFox
Copy link

KenwoodFox commented Jan 12, 2023

I know this is old, but do you have anything to add an input in the command, like /ban user:<user> reason:[reason] with <> being required and [] is optional?

You can give it a default value of something like None and check that in your function accordingly.

Im actually having trouble with this too haha! How do you read in "the rest of the message" my old way was to just

*args

But now i cant do that because of typing, *args: tuple doesn't work, nor does str, I'm at my wits end!

@AbstractUmbra
Copy link
Author

@KenwoodFox

The reason for this is not discord.py related. Slash commands themselves have no concept of "consumption args" or varargs like ext.commands implements.

You simply cannot do this this way.

The recommend way is to take a str input and then .split() it with a clear separator, or have N many arguments.
Sad but this is the most recommended ways of doing it.

@KenwoodFox
Copy link

@KenwoodFox

The reason for this is not discord.py related. Slash commands themselves have no concept of "consumption args" or varargs like ext.commands implements.

You simply cannot do this this way.

The recommend way is to take a str input and then .split() it with a clear separator, or have N many arguments. Sad but this is the most recommended ways of doing it.

That makes it really hard for users who don't usually have concepts of wrapping strings with " or using other deliminaters than space :c i might just not migrate these commands that expect full sentence strings until something can be figured out.

@InterStella0
Copy link

Hyperlink is outdated for CommandTree.copy_global_to at Syncing gotchas.

@AbstractUmbra
Copy link
Author

Hyperlink is outdated for CommandTree.copy_global_to at Syncing gotchas.

Thanks! Resolved.

@D-Stacks
Copy link

can anyone tell me why i am unable to use slash commands while dming my bot? i have them in a tree, and sync without specifying any guilds, i.e. globally, i assumed this would sync to dms as well. but it just won't work. I know it is possible in theory to dm slash-commands, but i just see no documentation on how to achieve this with discord.py...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment