The Shift from JSON Events to Custom Components
If you are looking at older add-on packs or maintaining legacy content, you will likely encounter an "events" object inside the block JSON file, paired with trigger components like "minecraft:on_interact" or "minecraft:on_player_destroyed". That entire JSON event system was part of the Holiday Creator Features experimental toggle.
In Minecraft Bedrock 1.20.80, Mojang deprecated and removed those JSON event responses for any block using a "format_version" of 1.20.80 or higher. Instead of writing complex Molang and event chains inside your block JSON, you now attach a single component—"minecraft:custom_components"—to your block, and handle the actual logic in TypeScript or JavaScript using the "@minecraft/server" Script API.
This shift makes blocks cleaner to define and vastly more capable. Rather than being limited to a handful of JSON event responses like "set_block_state" or "run_command", your block events have access to the full scripting environment.
Attaching Custom Components in Block JSON
To make a block listen for events, you declare the "minecraft:custom_components" component inside your block definition. It takes an array of custom component identifiers—strings formatted with a namespace and a name, just like your block identifier.
If you also want your block to respond to scheduled or random ticks, you still pair this with the standard "minecraft:queued_ticking" or "minecraft:random_ticking" components in the JSON, though you omit the old event trigger fields inside them. Below is a complete block definition for a toggleable lamp that uses a custom component.
{
"format_version": "1.21.0",
"minecraft:block": {
"description": {
"identifier": "wiki:toggle_lamp",
"states": {
"wiki:lit": [false, true]
}
},
"components": {
"minecraft:destructible_by_mining": {
"seconds_to_destroy": 1.5
},
"minecraft:custom_components": [
"wiki:lamp_toggle_behavior"
]
},
"permutations": [
{
"condition": "q.block_state('wiki:lit') == true",
"components": {
"minecraft:light_emission": 15
}
},
{
"condition": "q.block_state('wiki:lit') == false",
"components": {
"minecraft:light_emission": 0
}
}
]
}
}Registering the Event Handler in Script
Declaring the identifier in JSON does nothing until you register a matching custom component in your Behavior Pack's script entry file. Registration must happen during world initialization, specifically inside the "worldInitialize" (or "startup" in newer API versions) before-event, using the "blockComponentRegistry" object.
You pass the exact string identifier you used in your JSON, followed by an object containing the event methods you want to listen to. For our lamp, we implement "onPlayerInteract" to flip the "wiki:lit" block state and play a click sound whenever a player uses the block.
import { world } from "@minecraft/server";
world.beforeEvents.worldInitialize.subscribe((eventData) => {
eventData.blockComponentRegistry.registerCustomComponent(
"wiki:lamp_toggle_behavior",
{
onPlayerInteract(event) {
const { block, dimension } = event;
const currentState = block.permutation.getState("wiki:lit");
const newPermutation = block.permutation.withState("wiki:lit", !currentState);
block.setPermutation(newPermutation);
dimension.playSound("random.click", block.location);
}
}
);
});Available Block Event Hooks
A single custom component object can implement multiple event hooks at once. Each hook receives an event argument containing context about what happened—always including the "block" and "dimension" where the event occurred, and often the "player" or "entity" involved.
Because a block can list multiple entries in its "minecraft:custom_components" array, you can build reusable behaviors. For example, you might have one component that handles crop growth on random ticks, and a separate component that handles fertilizing when a player interacts with bone meal.
| Method Name | When it triggers | Key Event Properties |
|---|---|---|
| onPlace | When the block is placed in the world by a player or script. | block, dimension, previousBlock |
| onPlayerInteract | When a player right-clicks or taps the block. | block, dimension, player, face, faceLocation |
| onPlayerDestroy | After a player finishes mining and breaking the block. | block, dimension, player, destroyedBlockPermutation |
| onStepOn | When an entity steps onto the top surface of the block. | block, dimension, entity |
| onStepOff | When an entity walks or jumps off the top surface of the block. | block, dimension, entity |
| onEntityFallOn | When an entity falls and lands on the block (requires a minimum fall distance configured in JSON). | block, dimension, entity, fallDistance |
| onTick | When a scheduled tick fires (requires "minecraft:queued_ticking" on the block). | block, dimension |
| onRandomTick | When a random chunk tick hits this block (requires "minecraft:random_ticking" on the block). | block, dimension |
Using Ticking Events
Two of the event hooks—"onTick" and "onRandomTick"—will not fire simply by registering them in your script. They still rely on JSON components to tell the game engine how and when to schedule ticks for the block.
For "onRandomTick", you must include ""minecraft:random_ticking": {}" as an empty object in your block's "components" list. The game will then call your script's "onRandomTick" method at the normal random tick rate. For "onTick", you must include "minecraft:queued_ticking" in your JSON with a "looping" boolean and an "interval_range" array of two numbers (minimum and maximum ticks between executions).
{
"format_version": "1.21.0",
"minecraft:block": {
"description": {
"identifier": "wiki:ticking_machine"
},
"components": {
"minecraft:queued_ticking": {
"looping": true,
"interval_range": [20, 20]
},
"minecraft:custom_components": [
"wiki:machine_tick_logic"
]
}
}
}Which versions this applies to
Applies to Minecraft Bedrock 1.21.0 and later (introduced as stable in 1.20.80). Does not require any experimental toggles when using stable "@minecraft/server" API versions (1.11.0 or higher) in your manifest.json. Legacy JSON block events only function on format_version 1.20.70 or older with the Holiday Creator Features toggle enabled, which is now removed from current releases.
When it does not work
| What you see | Why | The fix |
|---|---|---|
| Content Log error: "Failed to find custom component 'wiki:lamp_toggle_behavior'" or the block does nothing when clicked. | You either forgot to register the component in your script, misspelled the identifier between the JSON and JS files, or attempted to register it outside the world initialization event. | Ensure "registerCustomComponent" is called inside "world.beforeEvents.worldInitialize.subscribe" (and before any "await" statements), and check that your Behavior Pack manifest.json has a valid "script" module entry and "@minecraft/server" dependency. |
| Content Log error: "minecraft:on_interact is not a valid component" when loading into a world. | Your block JSON uses a modern "format_version" (such as 1.20.80 or 1.21.0) while trying to use removed Holiday Creator Features event trigger components. | Remove "minecraft:on_interact", "minecraft:on_placed", and the "events" object from your block JSON. Replace them with "minecraft:custom_components" and move the response logic into your script file. |
| The "onTick" method in the custom component never runs, even though "onPlayerInteract" on the same component works fine. | Registering "onTick" in JavaScript only sets up the listener; the block does not actually tick unless told to do so in its JSON definition. | Add the "minecraft:queued_ticking" component to your block's JSON "components" (or a permutation) with a valid "interval_range", such as "[20, 20]". |
| Noticeable lag spikes when placing hundreds of custom blocks that use "onTick". | Every block with "minecraft:queued_ticking" set to a short interval executes JavaScript code simultaneously. Running heavy logic or world queries every tick across many blocks quickly exhausts the script watchdog budget. | Use "minecraft:random_ticking" with "onRandomTick" if exact timing is not required, increase the "interval_range" on "minecraft:queued_ticking", or only enable the ticking component inside an active permutation (for example, when a machine block is switched on). |
Reference, straight from the game files
Read out of the game itself, so the ids match the version you are building for.
