Wiki 9Minecraft Wiki 9Minecraft

Block Event Migration to Custom Components

Updating legacy JSON block events and triggers to Script API custom components.

6Sections
3Code examples
3Known errors

Minecraft Bedrock removed legacy data-driven block events and trigger components in format version 1.20.80 in favor of the Script API. If you are updating an older add-on or building interactive blocks for modern versions, you must replace JSON event responses with TypeScript or JavaScript custom components. This guide walks through converting old block triggers into registered script callbacks.

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 ComponentScript API CallbackNotes
minecraft:on_interactonPlayerInteractFires when a player right-clicks or taps the block.
minecraft:on_player_destroyedonPlayerDestroyFires after a player finishes mining the block.
minecraft:on_placedonPlaceFires when the block is placed in the world.
minecraft:on_step_ononStepOnFires when an entity steps onto the top of the block.
minecraft:on_step_offonStepOffFires when an entity leaves the top of the block.
minecraft:on_fall_ononFallOnRequires minecraft:entity_fall_on in JSON to set minimum fall distance.
minecraft:queued_tickingonTickRequires minecraft:tick in JSON to set the tick interval.
minecraft:random_tickingonRandomTickFires 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.

BP/blocks/lamp_legacy.json
{
  "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.

BP/blocks/lamp.json
{
  "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.

BP/scripts/main.js
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 seeWhyThe 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 LogYour 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.