Register your own slash commands with typed parameters, enums and permission levels.
Plugins add slash commands through the custom command registry, the same API Mojang’s scripting uses. A registered command shows up in the client’s command list with auto-complete, in /help, and can be run from the terminal.
Register a command
Section titled “Register a command”Commands can only be registered during startup. Subscribe to system.beforeEvents.startup and use the customCommandRegistry it gives you:
import { system, CommandPermissionLevel, CustomCommandStatus } from "@minecraft/server";
system.beforeEvents.startup.subscribe(({ customCommandRegistry }) => { customCommandRegistry.registerCommand( { name: "example:ping", description: "Replies with pong", permissionLevel: CommandPermissionLevel.Any, }, () => { return { status: CustomCommandStatus.Success, message: "Pong!" }; }, );});The name must have a namespace, written namespace:name. The command then answers to both the short and the full form: /ping and /example:ping.
Registration throws a CustomCommandError if:
- it happens outside the startup event,
- the name has no namespace,
- a command with that name is already registered, by Basalt or by another plugin.
Because Basalt’s own commands are registered first, a plugin cannot replace a built-in command such as /give.
The callback
Section titled “The callback”The callback receives the command’s origin, followed by one argument per parameter, in the order they were declared. It returns a result:
{ status: CustomCommandStatus.Success, message: "Shown to whoever ran it" }message is optional. With CustomCommandStatus.Failure the message is shown as an error.
The origin tells you who ran the command:
| Field | Meaning |
|---|---|
sourceType |
CustomCommandSource.Entity for a player, CustomCommandSource.Server for the terminal. |
sourceEntity |
The player who ran it, or undefined from the terminal. |
Always handle the case where there is no player:
import { system, CommandPermissionLevel, CustomCommandStatus, Player } from "@minecraft/server";
system.beforeEvents.startup.subscribe(({ customCommandRegistry }) => { customCommandRegistry.registerCommand( { name: "example:heal", description: "Restores your health", permissionLevel: CommandPermissionLevel.GameDirectors, }, (origin) => { const player = origin.sourceEntity; if (!(player instanceof Player)) { return { status: CustomCommandStatus.Failure, message: "Only players can use this." }; } system.run(() => { player.getComponent("minecraft:health")?.resetToMaxValue(); }); return { status: CustomCommandStatus.Success, message: "Healed." }; }, );});Keep the callback short
Section titled “Keep the callback short”Treat a command callback like a before event: inside it, a player is read as the plugin last saw them, and a first read can throw. Return the result straight away and do the real work in system.run, as the example above does. BDS requires the same, since it does not allow world changes inside a command callback.
The result’s message is sent to the player when the callback returns, so a slow callback means a slow reply.
Anything asynchronous, such as showing a form or making an HTTP request, must start from system.run too:
system.run(() => void openShop(player));return { status: CustomCommandStatus.Success };Parameters
Section titled “Parameters”Declare parameters in mandatoryParameters and optionalParameters. Each has a name, which the client shows as a hint, and a type.
import { system, CommandPermissionLevel, CustomCommandParamType, CustomCommandStatus, Player,} from "@minecraft/server";
system.beforeEvents.startup.subscribe(({ customCommandRegistry }) => { customCommandRegistry.registerCommand( { name: "example:announce", description: "Sends a title to players", permissionLevel: CommandPermissionLevel.GameDirectors, mandatoryParameters: [ { name: "targets", type: CustomCommandParamType.PlayerSelector }, { name: "text", type: CustomCommandParamType.String }, ], optionalParameters: [ { name: "seconds", type: CustomCommandParamType.Integer }, ], }, (origin, targets: Player[], text: string, seconds?: number) => { const stay = (seconds ?? 3) * 20; system.run(() => { for (const player of targets) { player.onScreenDisplay.setTitle(text, { fadeInDuration: 10, stayDuration: stay, fadeOutDuration: 10, }); } }); return { status: CustomCommandStatus.Success, message: `Sent to ${targets.length} players.` }; }, );});Run as /announce @a "Event starting" 5. Text with spaces must be quoted. An optional parameter that was left out arrives as undefined.
CustomCommandParamType |
The callback receives |
|---|---|
Boolean |
true or false |
Integer |
A whole number |
Float |
A number |
String |
A string: one word, or a quoted phrase |
Enum |
One of the enum’s values, as a string |
Location |
A Vector3. Players can use ~ for relative coordinates. |
PlayerSelector |
An array of Player, from a name or a selector such as @a |
EntitySelector |
An array of Player. On Basalt, selectors in custom commands pick players only. |
BlockType, ItemType, EntityType |
The identifier as a string |
Basalt checks the arguments before calling you. If a number is not a number, or no player matches a selector, the player gets the usual error and the callback is not called.
An enum is a fixed list of words that the client auto-completes. Register it before the command that uses it, and name it in the parameter with enumName:
import { system, CommandPermissionLevel, CustomCommandParamType, CustomCommandStatus, ItemStack, Player,} from "@minecraft/server";
const kits: Record<string, string[]> = { starter: ["minecraft:wooden_sword", "minecraft:bread"], miner: ["minecraft:iron_pickaxe", "minecraft:torch"],};
system.beforeEvents.startup.subscribe(({ customCommandRegistry }) => { customCommandRegistry.registerEnum("example:kit", Object.keys(kits)); customCommandRegistry.registerCommand( { name: "example:kit", description: "Gives you a kit", permissionLevel: CommandPermissionLevel.Any, mandatoryParameters: [ { name: "kit", type: CustomCommandParamType.Enum, enumName: "example:kit" }, ], }, (origin, kit: string) => { const player = origin.sourceEntity; if (!(player instanceof Player)) { return { status: CustomCommandStatus.Failure, message: "Only players can take a kit." }; } system.run(() => { const container = player.getComponent("minecraft:inventory")?.container; for (const item of kits[kit]) container?.addItem(new ItemStack(item, 1)); }); return { status: CustomCommandStatus.Success, message: `Here is the ${kit} kit.` }; }, );});Give enum names a namespace, like command names. Each name can be registered once.
Permission levels
Section titled “Permission levels”permissionLevel sets who may run the command.
| Level | Who can run it |
|---|---|
CommandPermissionLevel.Any |
Every player |
CommandPermissionLevel.GameDirectors |
Operators and the terminal |
CommandPermissionLevel.Admin, Host, Owner |
The terminal only |
Players only see, and can only run, commands at or below their level. See Permissions, allowlist and bans for how players become operators.
Running commands from a plugin
Section titled “Running commands from a plugin”A plugin can run commands itself, in two ways with different rights.
dimension.runCommand runs with the terminal’s rights, so it can run anything. ~ is relative to the world spawn. It throws if the command fails.
import { world } from "@minecraft/server";
world.getDimension("overworld").runCommand("time set day");player.runCommand runs the command as if the player had typed it, with the player’s own permission level. A command the player is not allowed to run will not work this way.
import { world } from "@minecraft/server";
for (const player of world.getAllPlayers()) { player.runCommand("me is ready");}Write the command without its leading slash. A player’s command is queued and runs after the call returns.
Intercepting commands
Section titled “Intercepting commands”To watch or block commands that players run, including built-in ones, use server.beforeEvents.playerCommand:
import { server } from "@minecraft/server";
server.beforeEvents.playerCommand.subscribe((event) => { console.log(`${event.player.name} ran: ${event.command}`); if (event.command.startsWith("me ")) event.cancel = true;});event.command is the line the player typed, without its leading slash. It can be changed to run something else instead.
Reloading
Section titled “Reloading”When plugins are reloaded with /reload, their commands are removed and registered again as each plugin starts.