Why JSON Block Events Were Replaced
In earlier Bedrock versions under the Holiday Creator Features experimental toggle, blocks could define an events object directly inside their behavior JSON. Creators paired trigger components like minecraft:on_interact or minecraft:on_step_on with event responses such as set_block_state, spawn_loot, or run_command. While convenient for tiny tasks, JSON events became tangled and hard to debug as block logic grew.
Starting in format_version 1.20.80 and finalized in 1.21.0, Mojang retired the Holiday Creator Features toggle and stripped JSON event parsing from blocks altogether. All dynamic block behavior now runs through the @minecraft/server Script API using the minecraft:custom_components component. While this requires setting up a script file in your behavior pack, it gives you full access to standard JavaScript logic, proper error logs, and far better execution performance.
Mapping Legacy Triggers to Custom Component Hooks
Every old JSON trigger component has a direct equivalent inside the BlockCustomComponent interface in @minecraft/server. Instead of declaring the trigger in JSON and pointing it at an event name, you attach a custom component identifier to your block and implement the matching callback function in your script.
The table below maps the most common legacy JSON components to their modern Script API callback methods. Note that ticking no longer requires its own separate trigger component if you use the onTick method, though you still use minecraft:tick inside the block JSON to control the interval range.
| Legacy JSON Component | Script API Callback | Notes |
|---|---|---|
| minecraft:on_interact | onPlayerInteract | Fires when a player right-clicks or taps the block. |
| minecraft:on_player_destroyed | onPlayerDestroy | Fires after a player finishes mining the block. |
| minecraft:on_placed | onPlace | Fires when the block is placed in the world. |
| minecraft:on_step_on | onStepOn | Fires when an entity steps onto the top of the block. |
| minecraft:on_step_off | onStepOff | Fires when an entity leaves the top of the block. |
| minecraft:on_fall_on | onFallOn | Requires minecraft:entity_fall_on in JSON to set minimum fall distance. |
| minecraft:queued_ticking | onTick | Requires minecraft:tick in JSON to set the tick interval. |
| minecraft:random_ticking | onRandomTick | Fires on standard Minecraft random block ticks without extra JSON. |
Legacy Block JSON Before Migration
To see how migration works in practice, consider a classic interactive lamp block written for 1.20.40. It defines a boolean block state called example:lit, uses minecraft:on_interact to listen for player clicks, and runs a JSON event that flips the state using Molang and plays a sound.
If you load the file below with a format_version of 1.20.80 or higher, the game will reject the trigger component and ignore the events block completely.
{
"format_version": "1.20.40",
"minecraft:block": {
"description": {
"identifier": "example:lamp",
"states": {
"example:lit": [false, true]
}
},
"components": {
"minecraft:on_interact": {
"event": "example:toggle_lamp"
}
},
"events": {
"example:toggle_lamp": {
"set_block_state": {
"example:lit": "!q.block_state('example:lit')"
}
}
},
"permutations": [
{
"condition": "q.block_state('example:lit') == true",
"components": {
"minecraft:light_emission": 15
}
}
]
}
}Migrated Block JSON
To bring this block up to 1.21.0 standards, bump the format_version, delete the entire events section, and remove minecraft:on_interact. In its place, add minecraft:custom_components and list a namespaced identifier for your behavior.
Notice that block states and permutations do not change at all during migration. Your visual permutations, collision boxes, and light emission rules remain strictly data-driven inside the JSON file.
{
"format_version": "1.21.0",
"minecraft:block": {
"description": {
"identifier": "example:lamp",
"states": {
"example:lit": [false, true]
}
},
"components": {
"minecraft:custom_components": [
"example:lamp_toggle"
]
},
"permutations": [
{
"condition": "q.block_state('example:lit') == true",
"components": {
"minecraft:light_emission": 15
}
}
]
}
}Registering the Custom Component in Script
With the JSON cleaned up, you now register the example:lamp_toggle component in your behavior pack's entry script. Registration must happen during world initialization by subscribing to worldInitialize (or startup in newer script API versions) and calling blockComponentRegistry.registerCustomComponent.
Inside the onPlayerInteract callback, you receive an event object containing the block, the player, and the clicked face. To replace the old set_block_state JSON response, read the block's current permutation, create a new permutation with the updated state via withState, and apply it back to the block.
import { world } from "@minecraft/server";
world.beforeEvents.worldInitialize.subscribe((initEvent) => {
initEvent.blockComponentRegistry.registerCustomComponent("example:lamp_toggle", {
onPlayerInteract(event) {
const { block, dimension } = event;
const currentState = block.permutation.getState("example:lit");
const nextPermutation = block.permutation.withState("example:lit", !currentState);
block.setPermutation(nextPermutation);
dimension.playSound("random.click", block.location);
}
});
});Converting Common Event Responses
Beyond toggling block states, legacy JSON events had several built-in actions like spawning loot, transforming a block into a different block type, or damaging the held item. In the Script API, you write these actions directly using methods on the block, dimension, or player objects passed into your callback.
For instance, the old transform_block response is now a single call to block.setType("minecraft:air") or any other block identifier. The old run_command response becomes dimension.runCommand("...") or player.runCommand("..."), though you should prefer native script methods like dimension.spawnEntity or dimension.playSound whenever possible instead of shelling out to commands. If your old event used die to destroy the block and drop its loot table, you can call dimension.runCommand with setblock destroy at the block's coordinates.
Which versions this applies to
Applies to Bedrock Edition 1.20.80 and 1.21.0+. Custom components are stable and do not require any experimental toggles in 1.21.0 and later, though your BP/manifest.json must declare a script module and a dependency on @minecraft/server (version 1.11.0 or higher).
When it does not work
| What you see | Why | The fix |
|---|---|---|
| [Blocks][error]-BP/blocks/lamp.json | minecraft:block | events : block definition does not allow 'events' | You raised the block's format_version to 1.20.80 or higher, where the legacy events object and trigger components were removed from the block schema. | Delete the events object and any legacy trigger components like minecraft:on_interact from the JSON file, and move that logic into a registered custom component script. |
| [Blocks][error]-example:lamp | Failed to find custom component 'example:lamp_toggle' | The block JSON references a custom component identifier that was never registered in script, or your script registered it after world initialization finished. | Ensure blockComponentRegistry.registerCustomComponent is called inside the worldInitialize (or startup) before-event, and verify that the identifier string matches your block JSON character for character. |
| Custom component script does not run and no errors appear in the Content Log | Your behavior pack manifest.json is missing the script module entry or the @minecraft/server dependency, so Minecraft never loads your main.js file. | Add a modules entry with type set to 'script', language set to 'javascript', and a valid entry path pointing to 'scripts/main.js', plus a dependencies block for '@minecraft/server'. |
Reference, straight from the game files
Read out of the game itself, so the ids match the version you are building for.
