Skip to content

Creating slash commands

Slash commands are the new way of defining commands, even though there are limitations with them, we do have some advantages such as being easier to fill in, choices and auto-completion.

If you wish to handle application commands yourself, you can disable them by disabling BApplicationConfig#enable.

Defining the command method

Tip

Make sure you read the common command requirements first!

In addition to the common requirements, the first parameter must be GlobalSlashEvent for global commands or GuildSlashEvent for guild commands, or guild-only global commands (default).

Annotated commands

Annotated command methods must be annotated with @JDASlashCommand, where you can set the scope, name, description, etc..., while the declaring class must extend ApplicationCommand.

Why do I need to extend ApplicationCommand?

As a limitation of annotated commands, you are required to extend this class as it allows the framework to ask your commands for stuff, like what guilds a command should be pushed to, getting a value generator for one of their options, and also getting choices.

Example

@Command
class SlashPingKotlin : ApplicationCommand() {
    // Default scope is global, guild-only (GUILD_NO_DM)
    @JDASlashCommand(name = "ping", description = "Pong!")
    suspend fun onSlashPing(event: GuildSlashEvent) {
        event.deferReply(true).queue()

        val ping = event.jda.getRestPing().await()
        event.hook.editOriginal("Pong! $ping ms").await()
    }
}
@Command
public class SlashPing extends ApplicationCommand {
    // Default scope is global, guild-only (GUILD_NO_DM)
    @JDASlashCommand(name = "ping", description = "Pong!")
    public void onSlashPing(GuildSlashEvent event) {
        event.deferReply(true).queue();

        event.getJDA().getRestPing().queue(ping -> {
            event.getHook().editOriginal("Pong! " + ping + " ms").queue();
        });
    }
}

Subcommands

To make a subcommand, set the name and subcommand on the annotation.

You will also need to add a @TopLevelSlashCommandData, it must only be used once per top-level command, this allows you to set top-level attributes.

Example

@Command
class SlashTag : ApplicationCommand() {
    // Data for /tag create
    @JDASlashCommand(name = "tag", subcommand = "create", description = "Creates a tag")
    // Data for /tag
    @TopLevelSlashCommandData(description = "Manage tags")
    fun onSlashTagCreate(event: GuildSlashEvent) {
        // ...
    }

    // Data for /tag delete
    @JDASlashCommand(name = "tag", subcommand = "delete", description = "Deletes a tag")
    fun onSlashTagDelete(event: GuildSlashEvent) {
        // ...
    }
}
@Command
public class SlashTag extends ApplicationCommand {
    // Data for /tag create
    @JDASlashCommand(name = "tag", subcommand = "create", description = "Creates a tag")
    // Data for /tag
    @TopLevelSlashCommandData(description = "Manage tags")
    public void onSlashTagCreate(GuildSlashEvent event) {
        // ...
    }

    // Data for /tag delete
    @JDASlashCommand(name = "tag", subcommand = "delete", description = "Deletes a tag")
    public void onSlashTagDelete(GuildSlashEvent event) {
        // ...
    }
}

Note

You cannot have both subcommands and top-level commands (i.e., an annotation with only name set).

However, you can have both subcommand groups and subcommands groups containing subcommands.

Adding options

Options can be added with a parameter annotated with @SlashOption.

All supported types are documented under ParameterResolver, and other types can be added.

Example

@Command
class SlashSayKotlin : ApplicationCommand() {
    @JDASlashCommand(name = "say", description = "Says something")
    suspend fun onSlashSay(event: GuildSlashEvent, @SlashOption(description = "What to say") content: String) {
        event.reply(content).await()
    }
}
@Command
public class SlashSay extends ApplicationCommand {
    @JDASlashCommand(name = "say", description = "Says something")
    public void onSlashSay(GuildSlashEvent event, @SlashOption(description = "What to say") String content) {
        event.reply(content).queue();
    }
}

Inferred option names

Display names of options can be set on the annotation, but can also be deduced from the parameter name, this is natively supported in Kotlin, but for Java, you will need to enable parameter names on the Java compiler.

Using choices

You must override getOptionChoices in order to return a list of choices, be careful to check against the command path as well as the option's display name.

Example

@Command
class SlashConvertKotlin : ApplicationCommand() {
    override fun getOptionChoices(guild: Guild?, commandPath: CommandPath, optionName: String): List<Choice> {
        if (commandPath.name == "convert") {
            if (optionName == "from" || optionName == "to") {
                return listOf(TimeUnit.SECONDS, TimeUnit.MINUTES, TimeUnit.HOURS, TimeUnit.DAYS)
                    // The Resolvers class helps us by providing resolvers for any enum type.
                    // We're just using the helper method to change an enum value to a more natural name.
                    .map { Choice(Resolvers.toHumanName(it), it.name) }
            }
        }

        return super.getOptionChoices(guild, commandPath, optionName)
    }

    @JDASlashCommand(name = "convert", description = "Convert time to another unit")
    suspend fun onSlashConvert(
        event: GuildSlashEvent,
        @SlashOption(description = "The time to convert") time: Long,
        @SlashOption(description = "The unit to convert from") from: TimeUnit,
        @SlashOption(description = "The unit to convert to") to: TimeUnit
    ) {
        event.reply("${to.convert(time, from)} ${to.name.lowercase()}").await()
    }
}
@Command
public class SlashConvert extends ApplicationCommand {
    @NotNull
    @Override
    public List<Choice> getOptionChoices(@Nullable Guild guild, @NotNull CommandPath commandPath, @NotNull String optionName) {
        if (commandPath.getName().equals("convert")) {
            if (optionName.equals("from") || optionName.equals("to")) {
                return Stream.of(TimeUnit.SECONDS, TimeUnit.MINUTES, TimeUnit.HOURS, TimeUnit.DAYS)
                        // The Resolvers class helps us by providing resolvers for any enum type.
                        // We're just using the helper method to change an enum value to a more natural name.
                        .map(u -> new Choice(Resolvers.toHumanName(u), u.name()))
                        .toList();
            }
        }

        return super.getOptionChoices(guild, commandPath, optionName);
    }

    @JDASlashCommand(name = "convert", description = "Convert time to another unit")
    public void onSlashConvert(
            GuildSlashEvent event,
            @SlashOption(description = "The time to convert") long time,
            @SlashOption(description = "The unit to convert from") TimeUnit from,
            @SlashOption(description = "The unit to convert to") TimeUnit to
    ) {
        event.reply(to.convert(time, from) + " " + to.name().toLowerCase()).queue();
    }
}

As you can see, despite the short choice list, the method is quite lengthy and causes duplications with multiple commands. This issue is solved with predefined choices.

Using autocomplete

Learn how to create an autocomplete handler here

Enabling autocompletion for an option is done by referencing an existing handler, in the autocomplete property of your @SlashOption.

Example

Using the autocomplete handler we made "Creating autocomplete handlers":

@Command
class SlashWord : ApplicationCommand() {
    @JDASlashCommand(name = "word", description = "Autocompletes a word")
    suspend fun onSlashWord(
        event: GuildSlashEvent,
        @SlashOption(description = "The word", autocomplete = SlashWordAutocomplete.WORD_AUTOCOMPLETE_NAME) word: String,
    ) {
        event.reply_("Your word was $word", ephemeral = true).await()
    }
}
@Command
public class SlashWord extends ApplicationCommand {
    @JDASlashCommand(name = "word", description = "Autocompletes a word")
    public void onSlashWord(GuildSlashEvent event,
                            @SlashOption(description = "The word", autocomplete = SlashWordAutocomplete.WORD_AUTOCOMPLETE_NAME) String word) {
        event.reply("Your word was " + word).setEphemeral(true).queue();
    }
}

Generated values

Generated values are parameters that get their values from a lambda everytime a command is run.

You must give one by overriding ApplicationCommand#getGeneratedValueSupplier, similarly to adding choices.

As always, make sure to check against the command path as well as the option's display name.

Example

@Command
class SlashCreateTimeKotlin : ApplicationCommand() {
    override fun getGeneratedValueSupplier(
        guild: Guild?,
        commandId: String?,
        commandPath: CommandPath,
        optionName: String,
        parameterType: ParameterType
    ): ApplicationGeneratedValueSupplier {
        if (commandPath.name == "create_time") {
            if (optionName == "timestamp") {
                // Create a snapshot of the instant the command was created
                val now = Instant.now()
                // Give back the instant snapshot, as this will be called every time the command runs
                return ApplicationGeneratedValueSupplier { now }
            }
        }

        return super.getGeneratedValueSupplier(guild, commandId, commandPath, optionName, parameterType)
    }

    @JDASlashCommand(name = "create_time", description = "Shows the creation time of this command")
    suspend fun onSlashCreateTime(
        event: GuildSlashEvent,
        @GeneratedOption timestamp: Instant
    ) {
        event.reply("I was created on ${TimeFormat.DATE_TIME_SHORT.format(timestamp)}").await()
    }
}
@Command
public class SlashCreateTime extends ApplicationCommand {
    @NotNull
    @Override
    public ApplicationGeneratedValueSupplier getGeneratedValueSupplier(
            @Nullable Guild guild,
            @Nullable String commandId,
            @NotNull CommandPath commandPath,
            @NotNull String optionName,
            @NotNull ParameterType parameterType
    ) {
        if (commandPath.getName().equals("create_time")) {
            if (optionName.equals("timestamp")) {
                // Create a snapshot of the instant the command was created
                final Instant now = Instant.now();
                // Give back the instant snapshot, as this will be called every time the command runs
                return event -> now;
            }
        }

        return super.getGeneratedValueSupplier(guild, commandId, commandPath, optionName, parameterType);
    }

    @JDASlashCommand(name = "create_time", description = "Shows the creation time of this command")
    public void onSlashTimeIn(
            GuildSlashEvent event,
            @GeneratedOption Instant timestamp
    ) {
        event.reply("I was created on " + TimeFormat.DATE_TIME_SHORT.format(timestamp)).queue();
    }
}

DSL commands (Kotlin)

Commands can be DSL-declared by either implementing:

  • GlobalApplicationCommandProvider (for global / guild-only global application commands), or,
  • GuildApplicationCommandProvider (for guild-specific application commands)

You can then use the slashCommand method on the manager, give it the command name, the command method, and then configure your command.

Tip

You are allowed to not add any command at all, for example, if the guild in GuildApplicationCommandManager isn't a guild you want the command to appear in.

Example

@Command
class SlashPingKotlinDsl : GlobalApplicationCommandProvider {
    suspend fun onSlashPing(event: GuildSlashEvent) {
        event.deferReply(true).queue()

        val ping = event.jda.getRestPing().await()
        event.hook.editOriginal("Pong! $ping ms").await()
    }

    override fun declareGlobalApplicationCommands(manager: GlobalApplicationCommandManager) {
        // Default scope is global, guild-only (GUILD_NO_DM)
        manager.slashCommand("ping", function = ::onSlashPing) {
            description = "Pong!"
        }
    }
}

Subcommands

As top-level commands cannot be made alongside subcommands, the top-level function must be null.

You can then add a subcommand by using subcommand, where each subcommand is its own function.

Example

@Command
class SlashTagDsl : GlobalApplicationCommandProvider {
    fun onSlashTagCreate(event: GuildSlashEvent) {
        // ...
    }

    fun onSlashTagDelete(event: GuildSlashEvent) {
        // ...
    }

    override fun declareGlobalApplicationCommands(manager: GlobalApplicationCommandManager) {
        // Pass a null function as this is not a top-level command
        manager.slashCommand("tag", function = null) {
            description = "Manage tags"

            subcommand("create", ::onSlashTagCreate) {
                description = "Creates a tag"
            }

            subcommand("delete", ::onSlashTagDelete) {
                description = "Deletes a tag"
            }
        }
    }
}

Info

You can still create both subcommands, and subcommand groups containg subcommands.

Adding options

Options can be added with a parameter and declaring it using option in your command builder, where the declaredName is the name of your parameter, the block will let you change the description, choices, etc.

All supported types are documented under ParameterResolver, and other types can be added.

Example

@Command
class SlashSayKotlinDsl : GlobalApplicationCommandProvider {
    suspend fun onSlashSay(event: GuildSlashEvent, content: String) {
        event.reply(content).await()
    }

    override fun declareGlobalApplicationCommands(manager: GlobalApplicationCommandManager) {
        manager.slashCommand("say", function = ::onSlashSay) {
            description = "Says something"

            option("content") {
                description = "What to say"
            }
        }
    }
}

Tip

You can override the option name by setting optionName in the option declaration:

option("content", optionName = "sentence") {
    ...
}

Using choices

Adding choices is very straight forward, you only have to give a list of choices to the choice property.

Example

@Command
class SlashConvertKotlinDsl : GlobalApplicationCommandProvider {
    suspend fun onSlashConvert(event: GuildSlashEvent, time: Long, from: TimeUnit, to: TimeUnit) {
        event.reply("${to.convert(time, from)} ${to.name.lowercase()}").await()
    }

    override fun declareGlobalApplicationCommands(manager: GlobalApplicationCommandManager) {
        manager.slashCommand("convert", function = ::onSlashConvert) {
            description = "Convert time to another unit"

            option("time") {
                description = "The time to convert"
            }

            option("from") {
                description = "The unit to convert from"

                choices = listOf(TimeUnit.SECONDS, TimeUnit.MINUTES, TimeUnit.HOURS, TimeUnit.DAYS)
                    // The Resolvers class helps us by providing resolvers for any enum type.
                    // We're just using the helper method to change an enum value to a more natural name.
                    .map { Choice(Resolvers.toHumanName(it), it.name) }
            }

            option("to") {
                description = "The unit to convert to"

                choices = listOf(TimeUnit.SECONDS, TimeUnit.MINUTES, TimeUnit.HOURS, TimeUnit.DAYS)
                    // The Resolvers class helps us by providing resolvers for any enum type.
                    // We're just using the helper method to change an enum value to a more natural name.
                    .map { Choice(Resolvers.toHumanName(it), it.name) }
            }
        }
    }
}

As you can see, despite the short choice list, this causes duplications with multiple commands. This issue is solved with predefined choices.

Using autocomplete

Learn how to create an autocomplete handler here

Enabling autocompletion for an option is done by referencing an existing handler, either using autocompleteByFunction or autocompleteByName.

Tip

I recommend using autocompleteByFunction as it avoids typing the name twice.

Example

Using the autocomplete handler we made "Creating autocomplete handlers":

@Command
class SlashWordDsl : GlobalApplicationCommandProvider {
    suspend fun onSlashWord(event: GuildSlashEvent, word: String) {
        event.reply_("Your word was $word", ephemeral = true).await()
    }

    override fun declareGlobalApplicationCommands(manager: GlobalApplicationCommandManager) {
        manager.slashCommand("word", function = ::onSlashWord) {
            description = "Autocompletes a word"

            option("word") {
                description = "The word"

                // Use an existing autocomplete declaration
                autocompleteByFunction(SlashWordAutocompleteDsl::onWordAutocomplete)
            }
        }
    }
}

Generated values

Generated values are a command parameter that gets their values computed by the given block everytime the command run.

Contrary to the annotated commands, no checks are required, as this is tied to the currently built command.

Example

@Command
class SlashCreateTimeKotlinDsl : GlobalApplicationCommandProvider {
    suspend fun onSlashCreateTime(event: GuildSlashEvent, timestamp: Instant) {
        event.reply("I was created on ${TimeFormat.DATE_TIME_SHORT.format(timestamp)}").await()
    }

    override fun declareGlobalApplicationCommands(manager: GlobalApplicationCommandManager) {
        manager.slashCommand("create_time", function = ::onSlashCreateTime) {
            description = "Shows the creation time of this command"

            // Create a snapshot of the instant the command was created
            val now = Instant.now()
            generatedOption("timestamp") {
                // Give back the instant snapshot, as this will be called every time the command is run
                return@generatedOption now
            }
        }
    }
}

Default description

You can avoid setting the (non-localized) descriptions of your commands and options by putting them in a localization file, using the root locale (i.e., no locale suffix), and have your localization bundle registered with BApplicationConfigBuilder#addLocalizations.

The same commands as before, but without the descriptions
@Command
class SlashSayDefaultDescriptionKotlin : ApplicationCommand() {
    @JDASlashCommand(name = "say_default_description")
    suspend fun onSlashSayDefaultDescription(event: GuildSlashEvent, @SlashOption content: String) {
        event.reply(content).await()
    }
}
@Command
class SlashSayDefaultDescriptionKotlinDsl : GlobalApplicationCommandProvider {
    suspend fun onSlashSayDefaultDescription(event: GuildSlashEvent, content: String) {
        event.reply(content).await()
    }

    override fun declareGlobalApplicationCommands(manager: GlobalApplicationCommandManager) {
        manager.slashCommand("say_default_description", function = ::onSlashSayDefaultDescription) {
            option("content")
        }
    }
}
@Command
public class SlashSayDefaultDescription extends ApplicationCommand {
    @JDASlashCommand(name = "say_default_description")
    public void onSlashSayDefaultDescription(GuildSlashEvent event, @SlashOption String content) {
        event.reply(content).queue();
    }
}

Adding the root localization bundle

For the given resource bundle:

src/main/resources/bc_localization/Commands.json
{
  "say_default_description": {
    "description": "Says something",
    "options": {
      "content.description": "What to say"
    }
  }
}

You can add the bundle by calling BApplicationConfigBuilder#addLocalizations("Commands").

Using predefined choices

If your choices stay the same for every command, you can improve re-usability and avoid extra code by using choices on the resolver's level, that is, the resolver will return the choices used for every option of their type.

All you now need to do is enable usePredefinedChoices on your option.

Example

Here, the resolver for TimeUnit is already defined and will be explained in Adding option resolvers.

@Command
class SlashConvertSimplifiedKotlin : ApplicationCommand() {
    @JDASlashCommand(name = "convert_simplified", description = "Convert time to another unit")
    suspend fun onSlashConvertSimplified(
        event: GuildSlashEvent,
        @SlashOption(description = "The time to convert") time: Long,
        @SlashOption(description = "The unit to convert from", usePredefinedChoices = true) from: TimeUnit,
        @SlashOption(description = "The unit to convert to", usePredefinedChoices = true) to: TimeUnit
    ) {
        event.reply("${to.convert(time, from)} ${to.name.lowercase()}").await()
    }
}
@Command
class SlashConvertSimplifiedKotlinDsl : GlobalApplicationCommandProvider {
    suspend fun onSlashConvertSimplified(event: GuildSlashEvent, time: Long, from: TimeUnit, to: TimeUnit) {
        event.reply("${to.convert(time, from)} ${to.name.lowercase()}").await()
    }

    override fun declareGlobalApplicationCommands(manager: GlobalApplicationCommandManager) {
        manager.slashCommand("convert_simplified", function = ::onSlashConvertSimplified) {
            description = "Convert time to another unit"

            option("time") {
                description = "The time to convert"
            }

            option("from") {
                description = "The unit to convert from"

                usePredefinedChoices = true
            }

            option("to") {
                description = "The unit to convert to"

                usePredefinedChoices = true
            }
        }
    }
}
@Command
public class SlashConvertSimplified extends ApplicationCommand {
    @JDASlashCommand(name = "convert_simplified", description = "Convert time to another unit")
    public void onSlashTimeInSimplified(
            GuildSlashEvent event,
            @SlashOption(description = "The time to convert") long time,
            @SlashOption(description = "The unit to convert from", usePredefinedChoices = true) TimeUnit from,
            @SlashOption(description = "The unit to convert to", usePredefinedChoices = true) TimeUnit to
    ) {
        event.reply(to.convert(time, from) + " " + to.toString().toLowerCase()).queue();
    }
}

Update logs

You can optionally get more info on what changed in your application commands, by enabling the TRACE logs on io.github.freya022.botcommands.internal.commands.application.diff.DiffLogger, or any package it is in.

Examples

You can take a look at more examples here.