How Before-Events Work for Block Placement
The @minecraft/server module splits world events into two categories: beforeEvents and afterEvents. When a player right-clicks or taps to place a block, the engine fires world.beforeEvents.playerPlaceBlock just before the block actually enters the world and before the item is consumed from the player's hand.
Because this signal fires prior to the world state changing, the event payload includes a boolean property named cancel. Setting event.cancel = true aborts the placement cleanly. The block never appears, no placement sound is broadcast to other players, and the item remains in the player's inventory.
One critical limitation accompanies every before-event: the callback executes in read-only (restricted) execution mode. While inside the callback, you can read block positions, check player tags, and inspect the permutation being placed, but you cannot mutate the world. Calling methods like player.runCommand(), dimension.setBlockType(), or entity.teleport() directly inside the before-event handler will throw a privilege error. Any world modifications must be deferred to the next tick using system.run().
| Property | Type | Description |
|---|---|---|
| block | Block | The block at the target location before placement occurs (usually minecraft:air or a replaceable block like tall grass). |
| cancel | boolean | Set to true to prevent the block from being placed. |
| dimension | Dimension | The dimension in which the player is attempting to place the block. |
| face | Direction | The face of the existing block that the player clicked against. |
| faceLocation | Vector3 | The exact coordinates on the clicked block face where the interaction occurred. |
| permutationToPlace | BlockPermutation | The exact block permutation (type and states) that will be placed if the event is not canceled. |
| player | Player | The player attempting to place the block. |
Manifest Setup for @minecraft/server
Before writing script code, your behavior pack's manifest.json must declare a script module and a dependency on @minecraft/server. Because world.beforeEvents.playerPlaceBlock is part of the stable API in modern Bedrock releases, you can target a stable version such as 1.11.0 or higher without enabling the Beta APIs experimental toggle.
{
"format_version": 2,
"header": {
"name": "Block Protection Pack",
"description": "Prevents placing banned blocks and protects spawn.",
"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.11.0"
}
]
}Blocking Specific Block Types
A common mistakes when inspecting the playerPlaceBlock event is checking event.block.typeId to see what the player is placing. Because the event fires before placement, event.block still represents the block currently occupying that space—most often minecraft:air, water, or short grass. To inspect the block about to be placed, you must check event.permutationToPlace.type.id.
The following script prevents players from placing TNT or Bedrock unless they have been granted an admin tag. It also demonstrates how to use system.run() to play a sound or send a status message safely outside the read-only execution context.
import { world, system } from "@minecraft/server";
const BANNED_BLOCKS = new Set([
"minecraft:tnt",
"minecraft:bedrock",
"minecraft:respawn_anchor"
]);
world.beforeEvents.playerPlaceBlock.subscribe((event) => {
const { player, permutationToPlace } = event;
const blockId = permutationToPlace.type.id;
if (BANNED_BLOCKS.has(blockId) && !player.hasTag("allow_restricted_blocks")) {
event.cancel = true;
system.run(() => {
player.sendMessage(`§cYou are not allowed to place ${blockId}.`);
player.playSound("note.bass", { pitch: 0.5, volume: 1.0 });
});
}
});Creating a Protected Spawn Region
Because event.block represents the destination voxel where the new block will land, its location property gives you the exact coordinates to check against a protected bounding box. This is much more reliable than checking the player's own coordinates, as a player can stand outside a small protected zone and reach inside to place blocks.
When building a region protection check, always verify the dimension identifier alongside the coordinates. Otherwise, a spawn box centered at (0, 64, 0) in the Overworld will unintentionally block players from building around (0, 64, 0) in the Nether and the End.
import { world } from "@minecraft/server";
const SPAWN_BOUNDS = {
dimensionId: "minecraft:overworld",
min: { x: -32, y: -64, z: -32 },
max: { x: 32, y: 320, z: 32 }
};
function isInsideBox(loc, min, max) {
return (
loc.x >= min.x && loc.x <= max.x &&
loc.y >= min.y && loc.y <= max.y &&
loc.z >= min.z && loc.z <= max.z
);
}
world.beforeEvents.playerPlaceBlock.subscribe((event) => {
const { block, dimension, player } = event;
if (player.hasTag("builder")) return;
if (dimension.id !== SPAWN_BOUNDS.dimensionId) return;
if (isInsideBox(block.location, SPAWN_BOUNDS.min, SPAWN_BOUNDS.max)) {
event.cancel = true;
}
});Edge Cases What playerPlaceBlock Does Not Catch
Relying solely on playerPlaceBlock is enough for basic building restrictions, but several vanilla items place blocks or alter the world without going through the standard block placement pipeline. If you are securing a lobby or arena, you should be aware of what bypasses this event.
Buckets (lava, water, powder snow) do not fire playerPlaceBlock when emptied; they are item interactions and must be intercepted using item-use before-events such as world.beforeEvents.itemUseOn. Similarly, armor stands, boats, and minecarts are entities rather than blocks, and flint and steel ignites fire through item interaction logic.
In addition, blocks moved into a region by pistons placed outside the boundary, or blocks formed organically (such as water and lava meeting to form cobblestone, or falling sand landing), are world updates rather than player placements. None of these trigger playerPlaceBlock.
Which versions this applies to
Applies to Minecraft Bedrock 1.20.80 and 1.21.x using @minecraft/server version 1.11.0 or newer. No experimental toggles (such as Beta APIs) are required as long as you specify a stable @minecraft/server version in manifest.json.
When it does not work
| What you see | Why | The fix |
|---|---|---|
| Content Log shows: ReferenceError: Native function [Player::playSound] does not have required privileges. | You attempted to call a state-modifying method (like playSound, teleport, runCommand, or setBlockType) directly inside the world.beforeEvents.playerPlaceBlock callback, which executes in read-only mode. | Wrap any methods that modify the world or trigger player effects inside a system.run(() => { ... }) callback so they run on the next tick after the before-event finishes. |
| The script checks event.block.typeId === 'minecraft:tnt', but the condition never triggers and the TNT is placed anyway. | In a before-event, event.block refers to the block currently occupying the target space before the placement happens (almost always minecraft:air). | Check event.permutationToPlace.type.id instead of event.block.typeId to see the identifier of the block the player is trying to place. |
| Players cannot place blocks, but they can still dump water and lava buckets inside the protected region. | Emptying a liquid bucket is treated by the engine as using an item on a block face rather than placing a block item, so playerPlaceBlock never fires. | Subscribe to world.beforeEvents.itemUseOn as well, check if event.itemStack.typeId is a bucket (for example, minecraft:lava_bucket or minecraft:water_bucket), and set event.cancel = true. |
Reference, straight from the game files
Read out of the game itself, so the ids match the version you are building for.
