The Tag-and-Tick Pattern
The most reliable way to handle first-join logic without scripts relies on entity tags. In Minecraft Bedrock, tags applied to a player via the /tag command are saved directly to that player's NBT data on the world save. Unlike scoreboard values on offline players, which require careful management, a player's tags persist across sessions, server restarts, and even player deaths.
The logic follows a strict order of operations executed every single game tick. First, you target all players who do not have a specific marker tag (for example, 'joined') and run your initialization commands against them. On the very last line of that sequence, you apply the 'joined' tag to those exact same players. On the very next tick, twenty milliseconds later, the target selector `[tag=!joined]` will no longer match them, ensuring the commands only ever execute once per unique player.
If you are building an in-world creation without a behavior pack, you can build this with a Repeating, Unconditional, Always Active command block followed by a line of Chain, Unconditional, Always Active command blocks. However, in a behavior pack, placing these commands inside an `.mcfunction` file and registering it in `tick.json` is far cleaner, avoids chunk-loading issues associated with physical command blocks, and travels seamlessly with your pack.
| Selector Argument | What it does | Notes |
|---|---|---|
| @a[tag=!joined] | Selects all online players missing the 'joined' tag. | The exclamation mark negates the check. Tags are case-sensitive. |
| @s[tag=!joined] | Selects the executing player if they lack the tag. | Best used inside a function executed AS the target player. |
| @a[scores={joined=..0}] | Selects players whose 'joined' score is 0 or less. | Fails on fresh players because their score is `null` until initialized, not 0. |
Setting Up tick.json and the Function
To run your check automatically in a behavior pack, you need two files: `BP/functions/tick.json` and your actual `.mcfunction` file. Minecraft reads `tick.json` on world load and executes every listed function once per game tick (20 times per second). The execution context of a tick function is the server itself, centered at the world spawn point in the Overworld, with no executing entity (`@s` is empty).
Because `@s` does not exist at the top level of a tick function, running multiple commands using `@a[tag=!joined]` in each line is slightly inefficient and risks race conditions if a player joins mid-tick (though rare, it is bad practice). A much safer pattern is to use a ticking dispatcher that finds uninitialized players and runs a dedicated setup function `as` them and `at` their current location.
Below is the `tick.json` configuration. The path inside the `values` array is relative to the `BP/functions/` folder and omits the `.mcfunction` file extension.
{
"values": [
"init/check_first_join"
]
}Writing the Initialization Function
With `tick.json` pointing to `init/check_first_join`, you can separate your tick loop from your actual payload. If your `check_first_join.mcfunction` contains the single line `execute as @a[tag=!joined] at @s run function init/on_first_join`, the heavy command list in `on_first_join.mcfunction` only gets parsed and executed when a genuinely new player is standing in the world. During normal gameplay when everyone is already tagged, the game only evaluates one selector per tick.
Inside the payload function (`BP/functions/init/on_first_join.mcfunction`), `@s` refers to the newly joined player, and the execution position (`~ ~ ~`) is at their feet. This allows you to play sounds directly to them, spawn particles around their body, give them items, and initialize their scoreboards cleanly.
Always place the `tag @s add joined` command at the very bottom of the file. If you put it at the top, any subsequent commands in the same file that accidentally rely on `[tag=!joined]` will fail. Conversely, if you use `@s` for every line as shown below, the order matters less, but keeping the tag application at the end acts as a transaction commit—confirming every line above it fired.
# Give starter equipment
give @s stone_sword 1 0 {"minecraft:keep_on_death":{}}
give @s bread 16
# Initialize scoreboard values to 0 so they display and can be tested
scoreboard players add @s coins 0
scoreboard players add @s Deaths 0
# Audio and visual welcome
playsound random.levelup @s ~ ~ ~ 1.0 1.0
titleraw @s title {"rawtext":[{"text":"Welcome to the World!"}]}
tellraw @a {"rawtext":[{"selector":"@s"},{"text":" has joined for the first time!"}]}
# Mark the player so this function never runs for them again
tag @s add joinedModifying the Player Entity JSON
If your add-on already overrides `BP/entities/player.json`, you do not need `tick.json` at all. Evaluating a target selector twenty times a second is inexpensive, but running logic strictly on an entity lifecycle event is cleaner. By using the `minecraft:entity_spawned` event—or a brief timer component that checks the player's state on load—you can fire your setup commands without a continuous background loop.
However, there is a major caveat with `minecraft:entity_spawned` on the player entity: it only fires cleanly if your behavior pack is applied to the world before the player generates cleanly, and it can behave inconsistently across realm transfers or dimension changes depending on how the client loads. A rock-solid alternative inside `player.json` is an environment sensor or a tiny 1-second timer component residing in the default component group. Once the timer fires, it triggers an event that runs the `init/on_first_join` function and simultaneously removes the timer component group and adds a dummy component group so it never ticks again.
Because modifying `player.json` makes your pack incompatible with any other add-on that also modifies `player.json`, you should only use the entity approach in standalone adventure maps or total-conversion packs. For modular packs meant to be stacked with other creators' work, stick to `tick.json` or the Script API.
The Script API Approach (@minecraft/server)
In modern Bedrock add-ons, the `@minecraft/server` JavaScript module gives you an explicit event for player joins: `world.afterEvents.playerSpawn`. Despite the name `playerSpawn`, this event fires both when a player joins the world session and whenever they respawn after dying. Conveniently, the event payload includes a boolean property called `initialSpawn`. When `initialSpawn` is `true`, the player has just loaded into the world from the main menu; when `false`, they just clicked the respawn button after dying.
Note that `initialSpawn: true` means 'first spawn of this play session', not 'first time ever in this world's history'. To make it a true first-join detector, you still combine the event with `player.hasTag()` or a dynamic property (`player.getDynamicProperty()`). The massive advantage over `tick.json` is that zero code runs during normal gameplay; your check only executes at the exact moment a player connects to the server.
Because `playerSpawn` is an `afterEvent`, the player is already fully constructed in the world, meaning you can immediately run commands on them, add tags, or manipulate their inventory component without needing `system.run()` delays.
import { world } from "@minecraft/server";
world.afterEvents.playerSpawn.subscribe((eventData) => {
const { player, initialSpawn } = eventData;
// Ignore respawns after death
if (!initialSpawn) return;
// Check if they have ever joined this world before
if (!player.hasTag("joined")) {
player.addTag("joined");
// Run your initialization function as the player
player.runCommandAsync("function init/on_first_join");
}
});Handling Scoreboards and Offline Resets
A common reason creators set up a first-join function is to set a player's initial scoreboard values (such as starting money or a tutorial stage tracker). If a new player joins, they have no value on any scoreboard—their score is `null`, which causes selector checks like `@a[scores={coins=..10}]` to completely ignore them. Running `scoreboard players add @s coins 0` on first join initializes their score to 0 without overwriting it if an admin somehow gave them coins beforehand.
When testing your add-on, you will constantly need to reset your own first-join state to verify your changes. Simply run `/tag @s remove joined` in chat. If you are using the `tick.json` method, the function will re-trigger on the very next frame (within 1/20th of a second), instantly handing you a fresh starter kit. If you are using the Script API `playerSpawn` method, removing the tag will not immediately trigger the script; you must save and quit to the main menu and reload the world so `initialSpawn` fires again.
Which versions this applies to
Applies to Minecraft Bedrock 1.19.50 and newer. The function and tick.json method uses format_version 1.10.0+ in manifest.json with `min_engine_version` set to at least [1, 19, 50] for modern execute syntax. The Script API method uses `@minecraft/server` version 1.4.0 or newer and requires no experimental toggles in stable releases.
When it does not work
| What you see | Why | The fix |
|---|---|---|
| The player gets spammed with items and chat messages 20 times a second forever. | The `tag @s add joined` line at the end of the function either has a typo in the tag name (e.g., `tag @s add Joined` with a capital J while the selector checks `tag=!joined`) or the function failed to parse due to a syntax error on one of the lines. | Check the Content Log in Creator Settings on world load. In Bedrock, if a single command inside an `.mcfunction` file has invalid syntax, the entire function file refuses to load and is skipped, or if you placed the tag command in a separate chain block that isn't set to Always Active, it never fires. Match the exact capitalization of your tag. |
| Content Log error: `Function init/on_first_join not found` or nothing happens when loading into the world. | One or more commands inside `on_first_join.mcfunction` used invalid syntax—often legacy `/execute` syntax instead of the new `execute as ... run` format—causing the game's function parser to reject the entire file upon world startup. | Ensure `min_engine_version` in your `BP/manifest.json` header is set to `[1, 19, 50]` or higher, and test each command from your function individually in the in-game chat to find the line that throws a syntax error. |
| Titles, sounds, or particles do not show up when using the tick.json method on a sluggish device. | A ticking function will catch a player entity the exact tick their entity is created on the server, which can happen a couple of seconds before the player's client finishes the 'Generating World' loading screen. | Switch to the Script API `playerSpawn` event, which fires much closer to the moment the loading screen clears, or detect the player's first movement/rotation before firing visual and audio cues while keeping item distribution on the immediate join tick. |
Reference, straight from the game files
Read out of the game itself, so the ids match the version you are building for.
