How the Detection Mechanism Works
Despite their name, behavior pack animation controllers are general-purpose state machines that evaluate Molang expressions every tick. They can run slash commands, trigger entity events, and play behavior animations. To know if a player is idle, the state machine must check whether the player is translating through the world, rotating their camera, or performing actions like sneaking and interacting.
When every activity check returns false, the controller transitions from an active state into a waiting state. Inside the waiting state, a behavior pack animation acts as a countdown clock. If the player moves before the clock runs out, the controller jumps back to the active state and resets the timer. If the timer finishes while the player remains still, the controller enters the final AFK state and fires your chosen commands.
Molang Queries for Player Activity
Detecting inactivity requires checking more than just walking speed. A player standing still in a mineshaft while breaking blocks or looking around is not AFK. Conversely, a player pushed by flowing water has velocity in the world, so depending on how strict your server is, you may need to distinguish voluntary movement from external velocity.
The table below lists the standard Molang queries used to build the transition condition. Combining them with logical OR operators creates a single expression that returns true the moment the player does anything.
| Query / Variable | What it measures | Notes |
|---|---|---|
| query.is_moving | Returns 1.0 if the entity is moving horizontally. | Can be triggered by flowing water or pistons pushing the player. |
| query.ground_speed | Horizontal speed in meters per second. | Useful if you want a small deadzone threshold instead of a binary check. |
| query.is_jumping | Returns 1.0 while the jump input is held. | Catches players jumping in place. |
| query.is_sneaking | Returns 1.0 while the player is crouching. | Holding sneak continuously counts as active. |
| query.is_using_item | Returns 1.0 when eating, drawing a bow, or blocking. | Does not catch instant right-clicks, only held interactions. |
| variable.attack_time | Greater than 0.0 during an arm swing. | Available on the player entity; catches left-click mining and combat. |
| query.body_x_rotation | The player's vertical camera pitch in degrees. | Must be compared against its value from the previous tick to detect mouse look. |
| query.head_y_rotation(0) | The player's horizontal head yaw in degrees. | Like pitch, store this in a custom variable each tick to detect camera turning. |
Creating the Countdown Animation
Animation controllers do not have a built-in delay property on their states. Instead, you pair the controller with a behavior pack animation that has a fixed timeline length. While the controller sits in the waiting state, it plays this animation and watches the `query.all_animations_finished` expression.
This file goes into the `BP/animations/` folder. It does not need to run any commands on its timeline if the controller handles the state transitions; it only needs an explicit `animation_length` in seconds. The example below defines a 120-second (two-minute) idle threshold and records the player's camera orientation every tick so the controller can spot head movement.
{
"format_version": "1.10.0",
"animations": {
"animation.player.afk_countdown": {
"animation_length": 120.0,
"loop": false
},
"animation.player.track_camera": {
"loop": true,
"timeline": {
"0.0": [
"variable.last_pitch = query.body_x_rotation;",
"variable.last_yaw = query.head_y_rotation(0);"
]
}
}
}
}Building the Animation Controller
The animation controller uses three states: `active`, `waiting`, and `afk`. In the `active` state, if the player is completely still and their current camera rotation matches the rotation saved on the previous tick, the controller moves to `waiting`.
In the `waiting` state, `animation.player.afk_countdown` runs. Two transitions compete here: if the player moves or rotates their camera, the controller immediately returns to `active`. If the two-minute animation finishes first, `query.all_animations_finished` becomes true and the controller moves into the `afk` state.
When entering the `afk` state, `on_entry` adds a tag and announces the status. When the player eventually moves again, `on_exit` cleans up the tag before returning to `active`.
{
"format_version": "1.10.0",
"animation_controllers": {
"controller.animation.player.afk": {
"initial_state": "active",
"states": {
"active": {
"transitions": [
{
"waiting": "!query.is_moving && !query.is_jumping && !query.is_sneaking && !query.is_using_item && variable.attack_time == 0.0 && query.body_x_rotation == variable.last_pitch && query.head_y_rotation(0) == variable.last_yaw"
}
]
},
"waiting": {
"animations": [
"afk_countdown"
],
"transitions": [
{
"active": "query.is_moving || query.is_jumping || query.is_sneaking || query.is_using_item || variable.attack_time > 0.0 || query.body_x_rotation != variable.last_pitch || query.head_y_rotation(0) != variable.last_yaw"
},
{
"afk": "query.all_animations_finished"
}
]
},
"afk": {
"on_entry": [
"/tag @s add is_afk",
"/tellraw @a {\"rawtext\":[{\"selector\":\"@s\"},{\"text\":\" is now AFK.\"}]}"
],
"on_exit": [
"/tag @s remove is_afk",
"/tellraw @a {\"rawtext\":[{\"selector\":\"@s\"},{\"text\":\" is no longer AFK.\"}]}"
],
"transitions": [
{
"active": "query.is_moving || query.is_jumping || query.is_sneaking || query.is_using_item || variable.attack_time > 0.0 || query.body_x_rotation != variable.last_pitch || query.head_y_rotation(0) != variable.last_yaw"
}
]
}
}
}
}
}Attaching to the Player Entity
For the controller and animations to run, they must be registered in the behavior pack's player definition file (`BP/entities/player.json`). Under the `description` object, map short names to the full identifiers in `animations`, and list the controller and the camera-tracking animation inside `scripts.animate`.
Execution order inside `scripts.animate` matters. Place `controller.afk` before `track_camera` so the controller compares the current tick's rotation against the variable stored at the end of the previous tick. If you reverse the order, `variable.last_pitch` and `variable.last_yaw` will be overwritten before the controller checks them, and camera movement will never be detected.
{
"format_version": "1.18.20",
"minecraft:entity": {
"description": {
"identifier": "minecraft:player",
"is_spawnable": false,
"is_summonable": false,
"is_experimental": false,
"animations": {
"afk_countdown": "animation.player.afk_countdown",
"track_camera": "animation.player.track_camera",
"controller.afk": "controller.animation.player.afk"
},
"scripts": {
"animate": [
"controller.afk",
"track_camera"
]
}
},
"components": {}
}
}Which versions this applies to
Applies to Bedrock Edition 1.10.0 and newer for the animation and animation controller format versions. Modifying BP/entities/player.json works in stable retail builds and does not require any experimental toggles.
When it does not work
| What you see | Why | The fix |
|---|---|---|
| The player immediately enters the AFK state after the countdown even while turning their camera. | In BP/entities/player.json, the camera tracking animation is listed before the animation controller in scripts.animate, or variable.last_pitch is never initialized. | Ensure controller.afk is listed above track_camera in the scripts.animate array so the comparison happens before the variables update for the current tick. |
| Content Log error: [Animation][error]-animation.player.afk_countdown | Animations inside a Resource Pack cannot run slash commands or Molang timeline scripts. | The animation and animation controller files were placed in the Resource Pack (RP) instead of the Behavior Pack (BP). Resource pack controllers cannot execute slash commands or modify server-side entity tags. | Move both JSON files into BP/animations/ and BP/animation_controllers/, and reference them inside BP/entities/player.json. |
| The controller never leaves the waiting state to enter the afk state, even when the player stands completely still. | The short name used inside the waiting state's animations array does not match the short name defined in the description.animations block of player.json. | Verify that the string "afk_countdown" in the controller matches the key in description.animations, not the full animation.player.afk_countdown identifier. |
| Players remain marked as AFK when they log out and rejoin the world. | Entity tags added via /tag persist in the world save data, while animation controllers reset to their initial_state (active) when the entity reloads. | Add an on_entry array to the active state containing "/tag @s remove is_afk" so the tag is always cleared when a player initializes. |
Reference, straight from the game files
Read out of the game itself, so the ids match the version you are building for.
