Save Player Data with Bedrock Dynamic Properties

Persist player and world data with the Minecraft Bedrock Script API. Handle missing values, validate saves and test reconnects without resetting progress.

A JavaScript variable disappears when a script runtime restarts. Dynamic properties let an add-on keep small pieces of player or world state with the world. This guide builds a persistent join counter, then shows how to handle invalid values and evolve a save format without silently erasing progress.

Start with a working script pack

You need a behavior pack with a JavaScript module and an @minecraft/server dependency supported by your target game. Use the Addon Template Generator or the manifest guide to establish that baseline. Confirm a simple script loads before adding persistence. Do not paste a guessed module version into the manifest.

The examples use the current stable World and Entity methods. They do not use the old dynamic-property registration pattern found in early experimental tutorials. Verify declarations against the API version your pack actually targets.

Choose the owner of the data

  • Player property: that player's quest stage, preferences or unlocks.
  • World property: a shared campaign stage or server-wide setting.
  • Ordinary Map or variable: temporary calculations or a cache you can reconstruct.

Do not store everybody's personal progress under one world key. That creates a shared value even if the code that writes it was triggered by a player. Prefix keys consistently, and maintain a short list of their types and owners alongside your source.

Persist a player join counter

Place this in the script entry file declared by your manifest. The counter runs only for the initial spawn of a connection; an ordinary death and respawn should not increment it. It refuses unexpected saved values instead of replacing them with zero.

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

const JOIN_KEY = "icedfox:join_count";

export function readCounter(owner, key) {
  const value = owner.getDynamicProperty(key);
  if (value === undefined) return 0;
  if (typeof value !== "number" ||
      !Number.isSafeInteger(value) || value < 0) {
    throw new Error(`Unexpected saved counter: ${key}`);
  }
  return value;
}

world.afterEvents.playerSpawn.subscribe(({ player, initialSpawn }) => {
  if (!initialSpawn) return;
  try {
    const previous = readCounter(player, JOIN_KEY);
    if (previous === Number.MAX_SAFE_INTEGER) {
      throw new Error("Join counter reached its limit");
    }
    const next = previous + 1;
    player.setDynamicProperty(JOIN_KEY, next);
    player.sendMessage(`Welcome back. Recorded joins: ${next}`);
  } catch (error) {
    console.warn(`[Iced Fox persistence] ${String(error)}`);
  }
});

Microsoft's multiplayer guide explains the initial-spawn distinction and why in-memory collections do not replace persisted data. Our guard also catches negative values, fractions and unsafe integers. Keep error messages diagnostic; avoid logging private player information or dumping entire saves.

Store shared state deliberately

The same read/check/write approach works on world. For example, a server-wide campaign stage belongs there. Write from a suitable gameplay event or scheduled callback after the world is ready, not from a restricted before-event or module initialization.

Shared-state helper (call from a writable event)
function advanceCampaign(world) {
  const current = readCounter(world, "icedfox:campaign_stage");
  if (current >= 12) return; // This campaign has twelve stages.
  world.setDynamicProperty("icedfox:campaign_stage", current + 1);
}

The upper bound is a design decision for this example, not an API limit. In a real map, make advancement conditional on the expected current stage so two triggers cannot skip a chapter. A property write does not make a multi-step reward operation transactional.

Plan for missing, old and damaged data

undefined means a property has not been set. Zero may be a valid saved value; avoid truthiness checks that confuse the two. Keep each simple fact in a typed property when practical. If you serialize a larger record into JSON, validate the parsed object before using it.

Example of a versioned save record
{
  "schema": 1,
  "chapter": 3,
  "unlocked": ["forest_gate", "river_camp"]
}

Define what each field accepts, set a size budget and reject unknown future schema versions. If parsing fails, preserve the original value and report the problem. Replacing a malformed record with an empty object makes an apparent repair by deleting progress.

For a migration, back up a real test world, read the old record, validate it, construct the new record and only then write. Test both an untouched save and a partly migrated save. Keep pack identity stable during updates and test upgrades with the same release history players will have, rather than only testing a new world.

Test persistence, not just the first write

  1. Join a fresh world: the counter should be 1.
  2. Die and respawn: it should stay unchanged.
  3. Disconnect and reconnect: it should increase once.
  4. Save and close the world completely, reopen it, and confirm the previous value survived.
  5. Join with a second player: their personal counter should start independently.
  6. In a disposable copy, inject a wrong-typed value using a temporary debug script. Confirm the error is logged and the value is not overwritten.

Also test on a dedicated server if you support one. A successful local syntax check is not evidence that every lifecycle edge case works in Minecraft.

Keep writes small and purposeful

Save when meaningful state changes. Do not rewrite every player's full record every tick. Treat caches as disposable, bound arrays such as quest history, and review stored-property byte usage as the project grows. Dynamic properties are game state, not an encrypted password store or an external database.

If state appears to reset, first check the property owner, key spelling, initialization code and which pack build is active. If writes throw, inspect execution timing and value type. The debugging checklist helps separate a script-loading failure from a persistence failure.

Sources and verification

References checked on October 29, 2026. Examples were checked against these references and validated structurally; in-game acceptance checks are included above for your target version.