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()`.
| Property | What it does | Notes |
|---|---|---|
| id | The namespaced identifier passed to `/scriptevent` (e.g., `mypack:kit`). | Must contain a colon and cannot use the `minecraft:` namespace. |
| message | Everything typed after the ID string. | Limited to 256 characters per execution. |
| sourceEntity | The entity or player that ran the command. | Undefined if triggered by a command block or server console. |
| sourceBlock | The block that triggered the event. | Defined when a repeating, impulse, or chain command block runs the command. |
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.
{
"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.
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.
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 see | Why | The 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.chatSend | Your `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.
