Wiki 9Minecraft Wiki 9Minecraft

Running Commands on Player Join

Detect when a player enters the world for the first time or reconnects to trigger commands.

6Sections
4Code examples
4Known errors

Minecraft Bedrock does not have a single built-in `/onjoin` command, so add-on creators rely on a combination of tags, scoreboards, or the Script API to detect when a player enters a world. You reach for these patterns whenever you need to hand out a starter kit, display a welcome message, initialize scoreboard values, or clean up temporary states after a player reconnects.

Choosing a Detection Method

Before writing any files, you need to distinguish between two different events: a player joining the world for the very first time, and an existing player reconnecting to a world they have already played in. The method you choose depends on which of these events you want to catch and whether your behavior pack already uses the Script API.

For a simple first-time join—such as giving a starter book or teleporting a brand-new player to a lobby—a ticking `.mcfunction` file that checks for a specific tag is the fastest and lightest solution. It requires no scripts and no experimental toggles. Every tick, the game looks for players who lack a `joined` tag, runs your setup commands on them, and immediately applies the tag so the commands never fire for that player again.

Detecting a reconnect (every join) is trickier with pure commands because tags and scoreboard scores persist across sessions. While it is possible to build a pure-command leave detector by copying scoreboard IDs to an armor stand every tick and noticing when a player's ID stops responding, that setup breaks easily when chunks unload and creates unnecessary lag on larger realms. If you need to run commands every single time a player logs in, the `@minecraft/server` Script API is the reliable, modern tool for the job. It exposes native events that fire the exact moment a player finishes loading into the dimension.

MethodDetects First JoinDetects Every ReconnectRequires Script API
Tag Filter in tick.jsonYesNo (tags persist on logout)No
Animation ControllerYesYes (with Molang query tricks)No (Not recommended after 1.20.70)
Script API playerSpawnYes (via initialSpawn)Yes (via initialSpawn)Yes (Stable)

First-Time Join with Functions and Tags

The pure-command approach relies on two files inside your behavior pack's `functions` folder: `tick.json` to schedule the check on every game tick, and a custom `.mcfunction` file to hold the actual commands. Because `.mcfunction` files run without a default target, you must route every command through `@a[tag=!joined]` so that only untagged players are affected.

First, register your function inside `BP/functions/tick.json`. Any function listed in the `values` array runs twenty times per second from the server's root execution context.

BP/functions/tick.json
{
  "values": [
    "on_first_join"
  ]
}

Writing the First-Join Function

Next, create `BP/functions/on_first_join.mcfunction`. The order of commands in this file is critical. You must perform all your welcome actions first—giving items, setting spawn points, or playing sounds—while the player still lacks the tag. The very last line of the file must grant the `joined` tag.

If you add the tag on the first line, all subsequent lines targeting `@a[tag=!joined]` will immediately skip the player who just logged in. Conversely, if you forget to add the tag at the end, the player will receive twenty stone swords and twenty chat messages every second until they leave the server.

Notice the use of `execute as @a[tag=!joined] at @s` for positional commands like `/playsound` or `/particle`. Because `tick.json` runs at the world origin `(0, 0, 0)` in the Overworld, running a sound or particle command without `at @s` will play the effect at the world coordinates rather than where the player actually spawned.

BP/functions/on_first_join.mcfunction
# Initialize player scoreboard to 0
scoreboard players add @a[tag=!joined] money 0

# Give starter items
give @a[tag=!joined] apple 5
give @a[tag=!joined] stone_sword 1

# Announce to the player and play a sound at their location
tellraw @a[tag=!joined] {"rawtext":[{"text":"§aWelcome to the server! Here is your starter kit."}]}
execute as @a[tag=!joined] at @s run playsound random.levelup @s ~ ~ ~

# Lock the trigger so this only runs once per player
tag @a[tag=!joined] add joined

Every-Join Detection with the Script API

When you need commands to fire every time a player connects to the world—not just the first time—the `@minecraft/server` module provides `world.afterEvents.playerSpawn`. Despite the name, this event comes with a boolean property called `initialSpawn`. When `initialSpawn` is `true`, the player has just joined the server and finished loading into their dimension. When `initialSpawn` is `false`, the player has simply died and clicked the respawn button.

Why use `playerSpawn` instead of `playerJoin`? The Script API does have a `world.afterEvents.playerJoin` event, which fires earlier during the connection handshake. However, at the moment `playerJoin` fires, the player entity is not fully loaded into the world yet. Attempting to run commands, teleport the player, or modify their inventory during `playerJoin` will often fail or throw an error because the entity does not exist in a dimension. Waiting for `playerSpawn` with `initialSpawn === true` guarantees the player is physically in the world and ready to receive commands.

To enable scripts, your `BP/manifest.json` must include a `script` module pointing to your entry file, and a `dependencies` block requesting `@minecraft/server`. No experimental toggles are required if you use a stable version string like `1.11.0` or higher.

BP/manifest.json
{
  "format_version": 2,
  "header": {
    "name": "On Join Command Runner",
    "description": "Runs commands when players connect",
    "uuid": "a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d",
    "version": [1, 0, 0],
    "min_engine_version": [1, 20, 80]
  },
  "modules": [
    {
      "type": "data",
      "uuid": "b2c3d4e5-f6a7-4b5c-9d0e-1f2a3b4c5d6e",
      "version": [1, 0, 0]
    },
    {
      "type": "script",
      "language": "javascript",
      "uuid": "c3d4e5f6-a7b8-4c5d-0e1f-2a3b4c5d6e7f",
      "entry": "scripts/main.js",
      "version": [1, 0, 0]
    }
  ],
  "dependencies": [
    {
      "module_name": "@minecraft/server",
      "version": "1.11.0"
    }
  ]
}

Writing the Join Script

Inside `BP/scripts/main.js`, subscribe to `world.afterEvents.playerSpawn`. Check `event.initialSpawn` first so your login commands do not accidentally trigger every time a player dies to a Creeper and respawns at their bed. Once confirmed, you can call `player.runCommandAsync()` to run individual commands or execute an entire `.mcfunction` file as that specific player.

You can also combine the Script API with the tag check from the earlier section. By calling `player.hasTag("joined")`, you can cleanly branch your logic: run one set of commands for brand-new players, and another set of commands for returning veterans, all inside a single event listener without anything running in `tick.json`.

BP/scripts/main.js
import { world } from "@minecraft/server";

world.afterEvents.playerSpawn.subscribe((event) => {
  // Ignore regular respawns after death
  if (!event.initialSpawn) return;

  const player = event.player;

  // Commands that run on EVERY join
  player.runCommandAsync("title @s actionbar §eWelcome back to the server!");
  player.runCommandAsync("effect @s resistance 10 4 true");

  // Commands that only run on the FIRST join
  if (!player.hasTag("joined")) {
    player.addTag("joined");
    player.runCommandAsync("give @s compass 1");
    player.runCommandAsync("tellraw @a {\"rawtext\":[{\"text\":\"§bA new player has arrived: \"},{\"selector\":\"@s\"}]}");
  }
});

Legacy Method: Player Animation Controllers

In older add-ons, creators frequently detected player logins without scripts by attaching a behavior animation controller to `BP/entities/player.json`. Because animation controllers reset to their `default` state whenever an entity loads into memory, an controller that transitioned out of `default` on `query.is_alive` would fire its `on_entry` commands every time a player joined the world.

While you will still see this pattern in older packs, you should avoid it for new projects. First, modifying `player.json` makes your add-on incompatible with any other behavior pack that also edits the player entity. Second, modern versions of Bedrock removed the ability for animation controllers to execute slash commands unless the pack's `min_engine_version` in `manifest.json` is artificially locked below `1.20.70`. Using `tick.json` for first-join or `@minecraft/server` for every-join avoids editing `player.json` altogether and keeps your pack compatible with current game updates.

Which versions this applies to

The tick.json and .mcfunction method works in all modern Bedrock versions (1.16.0+) without experimental toggles. The Script API playerSpawn method is stable and requires no experimental toggles in Bedrock 1.20.40 and above when using a stable @minecraft/server version.

When it does not work

What you seeWhyThe fix
The player gets spammed with starter items and chat messages 20 times a second.The `tag @a[tag=!joined] add joined` line at the end of the function is missing, misspelled, or uses a different tag name than the target selector.Ensure the exact same tag string is used in every `@a[tag=!...]` selector and in the final `tag ... add` command.
Only the first command in the .mcfunction file runs; the rest of the commands do nothing.You placed `tag @a[tag=!joined] add joined` at the top of the function instead of the bottom. Once the player has the tag, the remaining `@a[tag=!joined]` selectors no longer match them.Move the `tag ... add` command to the very last line of your `.mcfunction` file.
Join commands also fire every time a player dies and clicks Respawn.Your script is subscribed to `world.afterEvents.playerSpawn`, which triggers on both server login and death respawn, and you did not filter out death respawns.Check `if (!event.initialSpawn) return;` at the top of your `playerSpawn` callback so it only runs on login.
Commands inside `world.afterEvents.playerJoin` fail or throw an error in the Content Log.`playerJoin` fires while the client is still loading into the world, before the player entity is physically placed in a dimension.Switch your listener from `playerJoin` to `playerSpawn` and check that `event.initialSpawn` is `true`.

Reference, straight from the game files

Read out of the game itself, so the ids match the version you are building for.