How Bedrock Scripting Works
Bedrock's modern scripting engine uses the QuickJS runtime to execute standard ES2020 JavaScript modules packaged inside your behavior pack. Unlike the retired legacy scripting engine from early Bedrock versions, the current GameTest-derived Scripting API is event-driven, strongly typed when paired with official TypeScript declarations, and split into distinct npm-style native modules.
Scripts only exist inside behavior packs (`BP/scripts/`). They do not run inside resource packs; if a script needs to trigger a visual or audio cue on the client, it calls server-side methods that instruct the client to play a sound, spawn a particle, or open a form. Every script execution happens within the context of a world tick, running at 20 ticks per second under normal load.
Core Script Modules
Functionality is divided into native modules that you declare in your pack's `manifest.json` and import at the top of your JavaScript files. Most add-ons only need the core server module, while others add UI or diagnostic capabilities.
| Module Name | What it does | Notes |
|---|---|---|
| @minecraft/server | Exposes worlds, dimensions, entities, blocks, items, players, and world events. | Has both stable releases (e.g., 1.8.0+) and `-beta` versions. |
| @minecraft/server-ui | Builds pop-up forms (`ActionFormData`, `ModalFormData`, `MessageFormData`) for players. | Stable versions available; frequently paired with `@minecraft/server`. |
| @minecraft/server-gametest | Provides automated test registration and simulated player helpers. | Requires the Beta APIs experimental toggle in world settings. |
| @minecraft/server-net | Allows HTTP requests from the server. | Restricted to Bedrock Dedicated Server configurations. |
| @minecraft/server-admin | Manages server variables and configuration secrets. | Restricted to Bedrock Dedicated Server configurations. |
Configuring manifest.json for Scripts
To enable scripting in a behavior pack, your `manifest.json` requires two things beyond a standard data pack: a module entry with its `type` set to `"script"`, and a `dependencies` array listing the exact native modules and versions your code imports.
The `entry` field inside the script module points to the main JavaScript file relative to the behavior pack root, always starting inside the `scripts/` folder. You must generate fresh UUIDs for both the header and the module when setting up your own pack.
{
"format_version": 2,
"header": {
"name": "Starter Script Pack",
"description": "Basic behavior pack configured for the Scripting API.",
"uuid": "a183b410-5162-4c22-9111-111111111111",
"version": [1, 0, 0],
"min_engine_version": [1, 20, 80]
},
"modules": [
{
"description": "Script Module",
"type": "script",
"language": "javascript",
"uuid": "b294c521-6273-4d33-8222-222222222222",
"version": [1, 0, 0],
"entry": "scripts/main.js"
}
],
"dependencies": [
{
"module_name": "@minecraft/server",
"version": "1.10.0"
}
]
}Writing Your First Entry Script
Two top-level objects from `@minecraft/server` anchor almost every script: `world` and `system`. The `world` object gives you access to dimensions (`overworld`, `nether`, `the_end`), active players, and world-level events (`world.beforeEvents` and `world.afterEvents`). The `system` object handles the tick loop, scheduled timeouts (`system.run`, `system.runInterval`, `system.runTimeout`), and script lifecycle events.
The example below listens for an entity or player breaking a block and sends a message to everyone in the world, then sets up a repeating interval that runs once every 100 ticks (5 seconds).
import { world, system } from "@minecraft/server";
world.afterEvents.playerBreakBlock.subscribe((event) => {
const player = event.player;
const blockId = event.brokenBlockPermutation.type.id;
world.sendMessage(`${player.name} broke a ${blockId} block!`);
});
system.runInterval(() => {
for (const player of world.getAllPlayers()) {
player.onScreenDisplay.setActionBar(`Current Tick: ${system.currentTick}`);
}
}, 100);Before Events, After Events, and Read-Only Mode
World events are split into two namespaces: `world.beforeEvents` and `world.afterEvents`. A `before` event fires right as an action is about to happen, before the game state changes. Most `before` events expose a `.cancel = true` property on their event data object, letting you stop a block break, item use, or chat message from occurring.
Because `before` events run mid-tick while the engine is evaluating collisions and state transitions, the world is placed into a restricted read-only mode during their callbacks. If you try to spawn an entity, mutate a block, or modify a component inside a `before` event callback, the engine throws a privilege error. To perform world modifications in response to a `before` event, wrap the mutation inside `system.run(() => { ... })` so it defers execution until normal tick processing resumes. Callbacks inside `world.afterEvents` do not have this restriction.
import { world, system } from "@minecraft/server";
world.beforeEvents.playerBreakBlock.subscribe((event) => {
if (event.block.typeId === "minecraft:diamond_ore") {
event.cancel = true;
system.run(() => {
event.player.sendMessage("You cannot mine diamond ore directly!");
event.player.playSound("note.bass");
});
}
});Enabling the Content Log for Debugging
Silent failures are the biggest hurdle when writing your first script. Before testing a pack, open Minecraft's Settings, navigate to the Creator section, and enable both "Enable Content Log File" and "Enable Content Log GUI".
With these enabled, any syntax errors, manifest mismatches, or runtime exceptions thrown by your code will immediately appear on screen, and `console.warn()` calls in your JavaScript will print directly to the log output.
Which versions this applies to
Applies to Minecraft Bedrock 1.20.0 and newer using format_version 2 manifests. Stable module versions (such as "1.10.0") work out of the box with no experimental toggles required. Using a "-beta" version string (such as "1.11.0-beta") requires enabling the "Beta APIs" experimental toggle in the world settings.
When it does not work
| What you see | Why | The fix |
|---|---|---|
| Content Log shows: "Module [@minecraft/server] with version [1.x.0-beta] is not a valid dependency" or complains that experimental APIs are not enabled. | Your manifest.json requests a `-beta` module version, or requests a stable version number that is newer than the version of Minecraft you are currently running. | Either enable the "Beta APIs" toggle under Experiments in the world settings (if you need beta features), or change the dependency version in `BP/manifest.json` to a stable version supported by your game client (for example, `"1.10.0"` on Bedrock 1.20.80). |
| Runtime error in Content Log: "ReferenceError: CannotModifyWorldError" or "function doesn't have privilege to call in read-only mode". | You called a state-changing method (like `dimension.spawnEntity`, `block.setType`, or `entity.kill`) directly inside a `world.beforeEvents` callback. | Wrap the state-changing calls inside `system.run(() => { ... })` so the game executes them on the normal tick phase after the before-event completes. |
| Pack loads into the world with zero errors, but nothing in `main.js` ever runs. | The `entry` path in `manifest.json` does not match the actual file location on disk, or the file has a `.ts` extension instead of `.js` because TypeScript was not compiled. | Ensure `entry` in `BP/manifest.json` is set to `"scripts/main.js"` and that the compiled JavaScript file actually sits at `BP/scripts/main.js`. |
Reference, straight from the game files
Read out of the game itself, so the ids match the version you are building for.
