---
title: "Runtime data"
description: "runtime_data is a world's persistent, replicated data layer — where you keep state that must survive restarts and reach every player: progression, saved builds, unlocks, per-player records, any…"
section: "Core"
slug: "core-runtime-data"
canonical: "https://origozero.ai/docs/core-runtime-data"
updated: "2026-07-16T09:43:48.358225225+00:00"
tags: ["documentation", "guide"]
---

# Runtime data

It's the durable counterpart to live multiplayer. The multiplayer relay (the multiplayer guide) carries small, fast-changing, throwaway state — positions, inputs, the current frame's action — that's meaningless a second later. `runtime_data` is for the state you want to still be there tomorrow. Reach for the relay when it's transient; reach for `runtime_data` when it's a save.

It is **generic and opaque**: it never interprets a key or a value. A value is any JSON-serializable Luau value — a number, a string, a table — and what you store, in what shape, is entirely yours.

## Two scopes

Everything lives in one of two stores, both shared across everyone in the world:

```lua
runtime_data.world           -- one shared world store
runtime_data.player(userId)  -- a player's store, keyed by id
```

- **`runtime_data.world`** is the world's own store — global progression, unlocked areas, a shared economy. Anyone can read it; anyone can write it.
- **`runtime_data.player(userId)`** is one player's store. Reads work for **any** player (their data is replicated to you, so you can read another player's stats), but writes only succeed for **your own** id. Trying to write another player's store is a loud error, and the rule is enforced for real — a client can't forge a write for someone else.

Both stores expose the identical accessor surface (below), so code that works against one works against the other.

## Hierarchical, path-keyed

Keys are slash-paths, and **every path is an independent leaf**. A deep write touches only that leaf — the rest of the document is untouched, only that one path replicates, and concurrent writes to sibling paths never clobber each other:

```lua
local me = runtime_data.player(runtime_data.localId())

me.set("inventory/weapons/primary/ammo", 30)   -- writes ONLY that leaf
me.get("inventory/weapons/primary/ammo")        -- reads ONLY that leaf
me.keys("inventory/")                            -- every path under inventory
me.getTree("inventory")                          -- reconstruct the nested table
me.merge("settings", { volume = 0.8 })           -- shallow-merge into a sub-object
```

You choose the granularity by how deeply you key. One fat blob at `"inventory"` is a single value; individually-addressable leaves under `"inventory/..."` let two systems write different slots without stepping on each other. Your call, per path.

## Reads are synchronous, writes return a promise

Reads come straight from the local replica — no round-trip, no waiting:

```lua
local hp = runtime_data.world.get("boss/hp", 100)   -- second arg is the default when absent
if runtime_data.world.has("boss/defeated") then ... end
for _, path in ipairs(runtime_data.world.keys("boss/")) do ... end
local all = runtime_data.world.list("boss/")         -- { [path] = value }
```

Writes return a promise id. Ignore it for fire-and-forget, or `task.await(...)` to confirm the write landed (`"ok"` / `"ERR:<reason>"`):

```lua
runtime_data.world.set("boss/defeated", true)        -- fire and forget
local status = task.await(runtime_data.world.set("boss/hp", 0))

runtime_data.world.set("boss/hp", nil)               -- nil value deletes
runtime_data.world.delete("boss/hp")                 -- same effect, explicit
```

## Tied to the player object

Per-player data isn't a separate thing you look up — it's already on the player. Every `world.connectedUsers` record exposes `.data`, which **is** `runtime_data.player(thatUser.identity)`:

```lua
for _, user in pairs(world.connectedUsers) do
  print(user.displayName, user.data.get("score", 0)) -- read anyone's score
end
```

So "show the scoreboard" is a read across everyone's `.data`. To write your own player's store, take the accessor for your id directly:

```lua
local me = runtime_data.player(runtime_data.localId())
me.set("profile/name", "Ada")                        -- writes my own store
me.set("score", 0)
```

(The multiplayer guide covers `connectedUsers` and the players registry.)

## Reacting to changes

Subscribe to a store to run code when it changes — a value updates for you the moment any player writes it:

```lua
local stop = runtime_data.world.onChange(function(path, value)
  -- value is the new decoded value, or nil on delete
  if path == "boss/defeated" then playVictoryStinger() end
end)
-- later:
stop()   -- the returned function unsubscribes

-- Watch every player's store at once:
runtime_data.onPlayerChange(function(userId, path, value)
  refreshScoreboardRow(userId)
end)
```

## Finding out who has data

```lua
runtime_data.players()   -- every player id that currently has any per-player data
runtime_data.localId()   -- the local player's id, or nil for an anonymous session
```

`localId()` is what gates your writes: `runtime_data.player(runtime_data.localId())` is the one store you're allowed to write, and a player accessor's `:isLocal()` tells you whether it's yours.

## The accessor surface, at a glance

Both `runtime_data.world` and every `runtime_data.player(id)` share these:

| Method | Effect |
| --- | --- |
| `get(path, default?)` | Read a value; `default` when the path is absent. |
| `has(path)` | Does the path exist? |
| `keys(prefix?)` | Every path (optionally under `prefix`). |
| `list(prefix?)` | `{ [path] = value }` for every path (optionally under `prefix`). |
| `getTree(prefix)` | Reconstruct the nested table under `prefix`. |
| `set(path, value)` | Write a leaf (`nil` deletes); returns a promise id. |
| `merge(path, partial)` | Shallow-merge a table into the value at `path`. |
| `delete(path)` | Delete a leaf; returns a promise id. |
| `onChange(cb)` | Subscribe to this store's changes; returns an unsubscribe fn. |

A player accessor additionally carries `.id` and `:isLocal()`.

## Beyond this

To confirm the live surface, iterate the table — `for k in pairs(runtime_data) do print(k) end` and the same on `runtime_data.world` — or read the module directly at `/zero/source/libs/@builtin/modules/runtime_data.module`. The concept to hold onto: `runtime_data` is the durable, shared memory of a world; the relay is its short-term memory. Put a value here when losing it would lose progress.
