The engine
A live, shared world
Every world is a multi-user session. Everything you do is live — your edits sync to everyone else in the world as you make them, and you see theirs. There is no "save" step: with a world bound, writes are durable the moment you make them (more on persistence next). The world's filesystem is the source of truth; "what you see is what exists."
Publishing a playable version for players is a separate, deliberate act (commit/push) — covered in the worlds guide. Saving and publishing are different things.
What persists
Durability comes from the world holding /zero/source, so the first question is which world that is. world.guid() answers it: a guid when one is bound, nil on a bare local boot. With no world, /zero/source is a session buffer — a write is taken, reads back exactly as written for the rest of the run, and ends with the process. The engine says so on every such write, in the notice your tool call carries back; world.create / world.swap bind a world and end it.
With a world bound, the filesystem boundary is the whole story:
/zero/source/...— durable. Anything you write here persists (it's stored in the backend and synced to everyone). This is where all authored content lives. No save step./zero/runtime/...— live runtime state. Never persists; it's rebuilt as the world runs./source/tmp— local scratch that does not sync to other clients. Use it for throwaway working state.
So "will this survive a reload / be seen by others?" has a simple answer: yes if a world is bound and it's under /zero/source (outside tmp), no otherwise.
Edit vs play
engine.mode is "edit" or "play", and you flip it directly:
engine.mode -- "edit" or "play"
engine.mode = "play" -- enter play
engine.mode = "edit" -- back to edit
engine.setMode("play", { strict = false }) -- enter play, waiving the script gate for this call
engine.onModeChange(function(newMode, oldMode) end) -- react to flips
- Edit is where you author. Entering it stops the gameplay clock, so component
update(dt)/fixedUpdate(dt)go idle andeditorUpdate(dt)takes over — setengine.paused = falsehere and all three tick together (see the pause section below). Writes to/zero/sourceare durable. - Play is where gameplay runs.
update/fixedUpdatetick.
Entering play doesn't just "start ticking": the engine snapshots the edit state, re-initialises a fresh play world (so components re-run awake/start in play), and writes during play land on a throwaway runtime copy. Leaving play discards that play world and restores the edit snapshot — which is why play-mode changes can never touch your authored source. (The exact component lifecycle across the flip is in the components guide.)
Entering play is also gated on the scripts you answer for being healthy: while a script under /zero/source (library content under /source/libs is exempt) carries an error-severity diagnostic, engine.mode = "play" raises instead of flipping — refusing to enter play — N script error(s) in your own content: followed by the offending path:line — message entries. Run lsp.checkAll({ scope = "user" }) to list them yourself, fix them, and flip again. The gate is calibrated by the lsp.strict world setting: "strict" (the default) blocks on any error, "soft" blocks only structural errors (syntax, unresolved require, broken lifecycle-callback signatures), and "off" logs a warning and proceeds. Returning to edit is never gated — you can always get back to fix the error. Neither override carries to publishing: zm.push refuses unconditionally while user-script errors exist, whoever wrote them (see the development guide).
You answer for what you wrote and for the world you opened — the content already there when you bound it, including your own from an earlier session. A file a neighbour wrote while you were already running is reported rather than gated: the flip proceeds and the diagnostics go to the engine log. Theirs to finish, since you hold a copy and correcting it would write over an edit still in flight. Read the log after a flip to see what rode along: /zero/runtime/logs/engine reads it as a file, and the logs toolbox's search tool narrows it to the lines another session's content raised.
To take one flip past a diagnostic without changing the gate for anyone else, pass the strict waiver on the call:
local report = engine.setMode("play", { strict = false })
for _, d in ipairs(report.bypassed) do
print(("entered play past %s:%d — %s"):format(d.path, d.line, d.message))
end
engine.setMode is engine.mode = ... with the gate under your control for that one call: the world's lsp.strict_mode is untouched, so every other session and every later session of the world keeps the gate it had, and report.bypassed (mirrored into the engine log) names exactly what the call went past. lsp.setStrictMode is the other lever and a different one — it writes lsp.strict_mode into the world settings, which every session reads.
Paused vs running
Pause is a separate axis from edit/play. engine.paused gates the gameplay tick:
engine.paused = true -- freeze gameplay
engine.timeScale = 0.5 -- or slow it down (1.0 real, 0.0 frozen, 2.0 double)
When paused, update(dt) / fixedUpdate(dt) stop — but editorUpdate(dt) keeps firing, so the authoring loop stays live. engine.timeScale scales time without fully stopping it. Both are orthogonal to engine.mode: edit/play and paused/running are independent, which is why the pair has four states and not two. A mode flip resets the flag to that mode's default (edit paused, play running), so the two axes look like one until something writes engine.paused — and in edit with the clock running, update and editorUpdate fire together. core/components has the table.
World lifecycle
engine.profile -- "editor" or "runtime" (read-only; the boot profile)
engine.worldLoaded -- true once the world's entrypoint has finished loading
engine.onWorldReady(function() end) -- content synced into the VFS
engine.onWorldLoaded(function() end) -- the world entrypoint has run (latched: fires immediately if already loaded)
A world boots: content syncs (onWorldReady), then /source/.world_entrypoint.luau runs (onWorldLoaded), which loads the startup scene and seeds defaults.
Finding the rest
engine is an authored module — read it at modules/api/engine/engine.module. Publishing and the shared library are in the worlds guide; how components behave across edit/play is in the components guide; how a loaded world surfaces players and the camera is in the scenes guide.