Wiki 9Minecraft Wiki 9Minecraft

Custom Commands in the Scripting API

Build chat-driven commands and developer tools using ScriptEvents and beforeEvents.

4Sections
4Code examples
3Known errors

Minecraft Bedrock Edition does not yet expose a native API to register true slash commands that appear in the vanilla autocomplete menu. Instead, add-on developers build custom commands using two patterns: the official `/scriptevent` command for stable production packs, and `chatSend` event interception under the Beta APIs toggle for prefix-based commands like `!spawn` or `-help`. You reach for these patterns when you need admin tools, debug triggers, or player-accessible chat shortcuts.

The Native Alternative: Script Events

In stable versions of the `@minecraft/server` module, the official way to pass arbitrary text arguments from the chat window, a command block, or an NPC into your JavaScript code is the `/scriptevent` command. While it does not let you create a top-level `/mycommand` alias, it works without enabling Beta APIs and integrates cleanly with vanilla permission levels and selector syntax.

When a player or command block runs `/scriptevent mypack:kit starter`, the game fires the `system.afterEvents.scriptEventReceive` event. Your script filters for the `mypack:` namespace, reads the message payload, and executes the appropriate game logic. Because this is an `afterEvent`, the world is not in a restricted read-only state, meaning you can immediately spawn entities, give items, or teleport players without wrapping your code in `system.run()`.

PropertyWhat it doesNotes
idThe namespaced identifier passed to `/scriptevent` (e.g., `mypack:kit`).Must contain a colon and cannot use the `minecraft:` namespace.
messageEverything typed after the ID string.Limited to 256 characters per execution.
sourceEntityThe entity or player that ran the command.Undefined if triggered by a command block or server console.
sourceBlockThe block that triggered the event.Defined when a repeating, impulse, or chain command block runs the command.
BP/scripts/main.js
import { system, ItemStack } from "@minecraft/server";

system.afterEvents.scriptEventReceive.subscribe((event) => {
    const { id, message, sourceEntity } = event;

    if (id === "mypack:kit" && sourceEntity?.typeId === "minecraft:player") {
        const player = sourceEntity;
        const args = message.trim().split(/\s+/);
        const kitName = args[0]?.toLowerCase();

        if (kitName === "starter") {
            const inventory = player.getComponent("minecraft:inventory")?.container;
            if (inventory) {
                inventory.addItem(new ItemStack("minecraft:stone_pickaxe", 1));
                inventory.addItem(new ItemStack("minecraft:bread", 16));
                player.sendMessage("§aReceived the starter kit!");
            }
        } else {
            player.sendMessage(`§cUnknown kit: ${kitName}`);
        }
    }
}, { namespaces: ["mypack"] });

Chat Interception with Beta APIs

If you want players to type short commands like `!home` or `!tpa` without needing operator permissions to run `/scriptevent`, you must intercept the `world.beforeEvents.chatSend` event. When a player sends a chat message starting with your chosen prefix, you set `event.cancel = true` so the command string is not broadcast to the rest of the server.

It is important to note that `world.beforeEvents.chatSend` is currently part of the `-beta` track of `@minecraft/server`. To use it, your behavior pack's `manifest.json` must depend on a beta version string (such as `1.14.0-beta` or whichever beta matches your current game version) and the world must have the Beta APIs experimental toggle enabled.

BP/manifest.json
{
  "format_version": 2,
  "header": {
    "name": "Chat Command Pack",
    "description": "Custom prefix commands using chatSend",
    "uuid": "a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d",
    "version": [1, 0, 0],
    "min_engine_version": [1, 21, 0]
  },
  "modules": [
    {
      "type": "script",
      "language": "javascript",
      "uuid": "f1e2d3c4-b5a6-4f5e-8d9c-0b1a2f3e4d5c",
      "entry": "scripts/main.js",
      "version": [1, 0, 0]
    }
  ],
  "dependencies": [
    {
      "module_name": "@minecraft/server",
      "version": "1.14.0-beta"
    }
  ]
}

Stepping Out of Read-Only Mode

Because `chatSend` is a `beforeEvent`, it fires while the game engine is mid-calculation. During any `beforeEvent` callback, the world is in a strict read-only state. You can read properties like `sender.location` or `sender.getTags()`, and you can call `sender.sendMessage()`, but any attempt to modify the world—such as teleporting the player, spawning an entity, or adding an item—will throw a read-only privilege error.

To mutate the world in response to a chat command, wrap your execution logic inside `system.run()`. This schedules your function to run on the next available tick window after the read-only lock has been lifted, while still allowing you to cancel the chat message synchronously inside the event handler.

BP/scripts/main.js
import { world, system } from "@minecraft/server";

const PREFIX = "!";

world.beforeEvents.chatSend.subscribe((event) => {
    const { sender, message } = event;
    if (!message.startsWith(PREFIX)) return;

    // Cancel immediately so the command does not appear in public chat
    event.cancel = true;

    const args = message.slice(PREFIX.length).trim().split(/\s+/);
    const command = args.shift()?.toLowerCase();

    if (command === "up") {
        const distance = parseInt(args[0], 10) || 10;
        
        // Defer world modification to the main tick loop
        system.run(() => {
            const loc = sender.location;
            sender.teleport({ x: loc.x, y: loc.y + distance, z: loc.z });
            sender.sendMessage(`§eWhoosh! Teleported up ${distance} blocks.`);
        });
    } else {
        sender.sendMessage(`§cUnknown command: ${PREFIX}${command}`);
    }
});

Structuring a Scalable Command Registry

As your add-on grows beyond two or three commands, a long chain of `if/else` statements becomes difficult to maintain. A cleaner approach is to store command definitions in a JavaScript `Map` where each entry holds metadata like required tags, description text, and the callback function.

Using tag checks (such as `sender.hasTag("admin")`) gives you a fine-grained permission system that works independently of vanilla operator status. You can grant a trusted builder access to `!up` and `!clearlag` by giving them a tag, without giving them full `/op` access to destructive vanilla commands.

BP/scripts/commands.js
import { world, system } from "@minecraft/server";

const commands = new Map();

export function registerCommand(name, requiredTag, callback) {
    commands.set(name.toLowerCase(), { requiredTag, callback });
}

world.beforeEvents.chatSend.subscribe((event) => {
    const { sender, message } = event;
    if (!message.startsWith("!")) return;

    event.cancel = true;

    // Match quoted strings or standalone words
    const matches = message.slice(1).match(/(?:[^\s"]+|"[^"]*")+/g) || [];
    const args = matches.map(arg => arg.replace(/^"|"$/g, ""));
    const cmdName = args.shift()?.toLowerCase();

    const cmd = commands.get(cmdName);
    if (!cmd) {
        sender.sendMessage(`§cUnknown command: !${cmdName}`);
        return;
    }

    if (cmd.requiredTag && !sender.hasTag(cmd.requiredTag)) {
        sender.sendMessage("§cYou do not have permission to use this command.");
        return;
    }

    system.run(() => {
        cmd.callback(sender, args);
    });
});

// Example registration
registerCommand("broadcast", "admin", (player, args) => {
    const text = args.join(" ");
    if (!text) {
        player.sendMessage("§cUsage: !broadcast <message>");
        return;
    }
    world.sendMessage(`§6[Broadcast] §f${text}`);
});

Which versions this applies to

Applies to Bedrock Edition 1.20.50 and newer. The `/scriptevent` receiver (`system.afterEvents.scriptEventReceive`) is part of stable `@minecraft/server` (1.8.0+) and requires no experimental toggles. Chat interception (`world.beforeEvents.chatSend`) requires the Beta APIs experimental toggle and a `-beta` version dependency in `manifest.json`.

When it does not work

What you seeWhyThe fix
ReferenceError: Native function [Entity::teleport] does not have required privileges.You attempted to modify the world state (teleporting, spawning entities, changing blocks, or altering inventories) directly inside the `world.beforeEvents.chatSend` callback while the game is in read-only mode.Keep `event.cancel = true` directly inside the `chatSend` callback, but wrap all world-modifying code inside a `system.run(() => { ... })` block.
TypeError: Cannot read properties of undefined (reading 'subscribe') on world.beforeEvents.chatSendYour `manifest.json` declares a stable version of `@minecraft/server` (like `1.13.0`), where `chatSend` is not exposed, or the Beta APIs toggle is turned off in the world settings.Change the `@minecraft/server` version in `manifest.json` to the current `-beta` version (e.g., `1.14.0-beta`) and enable the Beta APIs experiment in your world settings, or switch to `/scriptevent` for a stable-only pack.
Running `/scriptevent` in chat prints no errors, but the `scriptEventReceive` callback never triggers.Either you used the reserved `minecraft:` namespace in your command, or the namespace in your command does not match the `namespaces` filter array passed to `subscribe()`.Always use a custom namespace like `/scriptevent mypack:action` and ensure `{ namespaces: ["mypack"] }` matches the prefix before the colon.

Reference, straight from the game files

Read out of the game itself, so the ids match the version you are building for.