Skip to content

Annotated slash commands

Annotated commands are quick to use but are more limited and may look complex with multiple subcommands, for slash commands, the command method must be annotated with @JDASlashCommand.

@Command
class SlashPing {
    // 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 {
    // 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();
        });
    }
}

Command configuration

You can configure properties which applies to different layers of a command, we will use /ban temp users in our example:

Properties shared by nested commands

Some properties exist on all layers, you must to use the appropriate annotation for each layer. With our previous example, you can set the description for all levels with:

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 {
    // 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 {
    // 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 SlashSay {
    @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 {
    @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 implement SlashOptionChoiceProvider 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 SlashConvert : SlashOptionChoiceProvider {
    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 emptyList()
    }

    @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 implements SlashOptionChoiceProvider {
    @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 List.of();
    }

    @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 {
    @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 {
    @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 implementing ApplicationGeneratedValueSupplierProvider.

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

Example

@Command
class SlashCreateTime : ApplicationGeneratedValueSupplierProvider {
    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 }
            }
        }

        error("Unknown generated option: $optionName")
    }

    @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 implements ApplicationGeneratedValueSupplierProvider {
    @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;
            }
        }

        throw new IllegalArgumentException("Unknown generated option: " + optionName);
    }

    @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();
    }
}

Rate limiting

This lets you reject application commands if the user tries to use them too often.

Using an (anonymous) rate limiter

Use @RateLimit or @Cooldown to define one on an application command.

@Command
class SlashRateLimit {

    // A rate limit for this slash command only
    @RateLimit(
        // Apply to each user, regardless of channel/guild
        scope = RateLimitScope.USER,
        // Delete the message telling the user about the remaining rate limit after it expires
        deleteOnRefill = true,
        // At least one of those needs to be empty for the command to be rejected
        bandwidths = [
            // 5 uses, 5 tokens gets added gradually over an hour (so, 1 token every 12 minutes)
            Bandwidth(
                capacity = 5, // 5 uses
                Refill(
                    type = RefillType.GREEDY,       // Gradually
                    tokens = 5,                     // gives 5 tokens
                    period = 1,                     // every 1
                    periodUnit = ChronoUnit.HOURS   // hour
                )
            ),
            // 2 uses, 2 tokens gets added at once after 2 minutes
            // This is to prevent the user from spamming the command
            Bandwidth(
                capacity = 2, // 2 uses
                Refill(
                    type = RefillType.INTERVAL,     // At once,
                    tokens = 2,                     // give 2 tokens
                    period = 2,                     // every 2
                    periodUnit = ChronoUnit.MINUTES // minutes
                )
            ),
        ]
    )
    @JDASlashCommand(name = "rate_limit")
    suspend fun onSlashRateLimit(event: GuildSlashEvent) {
        event.reply("Hello world!").await()
    }
}
@Command
public class SlashRateLimit {

    // A rate limit for this slash command only
    @RateLimit(
            // Apply to each user, regardless of channel/guild
            scope = RateLimitScope.USER,
            // Delete the message telling the user about the remaining rate limit after it expires
            deleteOnRefill = true,
            // At least one of those needs to be empty for the command to be rejected
            bandwidths = {
                    // 5 uses, 5 tokens gets added gradually over an hour (so, 1 token every 12 minutes)
                    @Bandwidth(
                            capacity = 5, // 5 uses
                            refill = @Refill(
                                    type = RefillType.GREEDY,       // Gradually
                                    tokens = 5,                     // gives 5 tokens
                                    period = 1,                     // every 1
                                    periodUnit = ChronoUnit.HOURS   // hour
                            )
                    ),
                    // 2 uses, 2 tokens gets added at once after 2 minutes
                    // This is to prevent the user from spamming the command
                    @Bandwidth(
                            capacity = 2, // 2 uses
                            refill = @Refill(
                                    type = RefillType.INTERVAL,     // At once,
                                    tokens = 2,                     // give 2 tokens
                                    period = 2,                     // every 2
                                    periodUnit = ChronoUnit.MINUTES // minutes
                            )
                    ),
            }
    )
    @JDASlashCommand(name = "rate_limit")
    public void onSlashRateLimit(GuildSlashEvent event) {
        event.reply("Hello world!").queue();
    }
}

Using an existing rate limiter

Nothing as simple as using @RateLimitReference with the group of a rate limiter defined in a RateLimitProvider.

Learn how to create a rate limiter with "Defining a rate limit"

@BService
class WikiRateLimitProvider : RateLimitProvider {

    override fun declareRateLimit(manager: RateLimitManager) {
        // Lets the user use the command 5 times in an hour, but also 2 times in 2 minutes to prevent spam
        val bucketConfiguration = Buckets.spikeProtected(
            capacity = 5,               // 5 uses
            duration = 1.hours,         // Gives 5 tokens gradually, during an hour (1 token every 12 minutes)
            spikeCapacity = 2,          // 2 uses
            spikeDuration = 2.minutes   // Give 2 tokens every 2 minutes
        )

        val rateLimiter = RateLimiter.createDefault(
            // Apply to each user, regardless of channel/guild
            RateLimitScope.USER,
            // Delete the message telling the user about the remaining rate limit after it expires
            deleteOnRefill = true,
            // Give our constant bucket configuration
            configurationSupplier = bucketConfiguration.toSupplier()
        )

        // Register, any command can use it
        manager.rateLimit(RATE_LIMIT_GROUP, rateLimiter)
    }

    companion object {
        // The name of the rate limit, so you can reference it in your commands/components
        const val RATE_LIMIT_GROUP = "Wiki"
    }
}
@Command
class SlashRateLimitExisting {

    // A rate limit for this slash command only
    @RateLimitReference(WikiRateLimitProvider.RATE_LIMIT_GROUP)
    @JDASlashCommand(name = "rate_limit_existing")
    suspend fun onSlashRateLimit(event: GuildSlashEvent) {
        event.reply("Hello world!").await()
    }
}
@BService
public class WikiRateLimitProvider implements RateLimitProvider {
    // The name of the rate limit, so you can reference it in your commands/components
    public static final String RATE_LIMIT_GROUP = "Wiki";

    @Override
    public void declareRateLimit(@NotNull RateLimitManager rateLimitManager) {
        // Lets the user use the command 5 times in an hour, but also 2 times in 2 minutes to prevent spam
        final var bucketConfiguration = Buckets.createSpikeProtected(
                5,                      // 5 uses
                Duration.ofHours(1),    // Gives 5 tokens gradually, during an hour (1 token every 12 minutes)
                2,                      // 2 uses
                Duration.ofMinutes(2)   // Give 2 tokens every 2 minutes
        );

        final var rateLimiter = RateLimiter.createDefault(
                // Apply to each user, regardless of channel/guild
                RateLimitScope.USER,
                // Give our constant bucket configuration
                BucketConfigurationSupplier.constant(bucketConfiguration),
                // Delete the message telling the user about the remaining rate limit after it expires
                true
        );

        // Register
        rateLimitManager.rateLimit(RATE_LIMIT_GROUP, rateLimiter);
    }
}
@Command
public class SlashRateLimitExisting {

    // A rate limit for this slash command only
    @RateLimitReference(WikiRateLimitProvider.RATE_LIMIT_GROUP)
    @JDASlashCommand(name = "rate_limit_existing")
    public void onSlashRateLimit(GuildSlashEvent event) {
        event.reply("Hello world!").queue();
    }
}

Cooldown

A cooldown is a rate limit, but with fewer parameters, it can be used as @Cooldown(5, ChronoUnit.SECONDS /* also scope and deleteOnRefill */).

Filtering commands

You can use @DeclarationFilter if you wish to declare a guild command conditionally.

Note

There is no equivalent for declarative commands as you can check and return early with your own code.

Creating the filter

Create a service implementing CommandDeclarationFilter, and make the method return true if the command can be declared. All filters must return true for the command to be declared.

Example

@BService
class BotOwnerIsGuildOwnerDeclarationFilter(
    private val botOwners: BotOwners, // Provided by the framework
) : CommandDeclarationFilter {

    override fun filter(guild: Guild, path: CommandPath, commandId: String?): Boolean {
        // Only allow this command to be in guilds owned by the bot owner
        return guild.ownerIdLong in botOwners.ownerIds
    }
}
@BService
public class BotOwnerIsGuildOwnerDeclarationFilter implements CommandDeclarationFilter {
    private final BotOwners botOwners; // Provided by the framework

    public BotOwnerIsGuildOwnerDeclarationFilter(BotOwners botOwners) {
        this.botOwners = botOwners;
    }

    @Override
    public boolean filter(@NotNull Guild guild, @NotNull CommandPath commandPath, @Nullable String s) {
        // Only allow this command to be in guilds owned by the bot owner
        return botOwners.getOwnerIds().contains(guild.getOwnerIdLong());
    }
}

Using the filter

Add a @DeclarationFilter on your command and reference your filter inside it.

Example

@Command
class SlashBotOwnerIsGuildOwner {

    // All filters must return 'true' for the command to be declared
    @DeclarationFilter(BotOwnerIsGuildOwnerDeclarationFilter::class)
    // The command needs to be registered on guilds for it to be filtered
    @TopLevelSlashCommandData(scope = CommandScope.GUILD)
    @JDASlashCommand(name = "bot_owner_is_guild_owner")
    suspend fun onSlashBotOwnerIsGuildOwner(event: GuildSlashEvent) {
        event.reply_("You are the owner of this bot and guild!", ephemeral = true).await()
    }
}
@Command
public class SlashBotOwnerIsGuildOwner {

    // All filters must return 'true' for the command to be declared
    @DeclarationFilter(BotOwnerIsGuildOwnerDeclarationFilter.class)
    // The command needs to be registered on guilds for it to be filtered
    @TopLevelSlashCommandData(scope = CommandScope.GUILD)
    @JDASlashCommand(name = "bot_owner_is_guild_owner")
    public void onSlashBotOwnerIsGuildOwner(GuildSlashEvent event) {
        event.reply("You are the owner of this bot and guild!")
                .setEphemeral(true)
                .queue();
    }
}