Execution order
Table scripts come in two kinds, and the difference is only how they are attached:
- A scene script belongs to the whole scene. It is listed in the scene's
sceneScriptIds, and it getsworldandglobalEvents. - An object script belongs to one entity. The entity points at it through
metadata.scriptId, and the script getsworld,globalEventsandrefObject— a live handle to the entity it is attached to.
One script file can be both. Attach the same script to the scene and to three entities and it runs four times, as four independent instances with their own handler registrations.
The boot sequence#
Starting the script host is deterministic, and the order does not vary between runs. It is
exercised end to end in apps/web/src/scripting/__tests__/tableScriptHost.test.ts — "runs
scene scripts in order, then object scripts, then posts started".
1. The host collects the run list
scene scripts, in sceneScriptIds order
then object scripts, in objectAttachments order
a repeated objectId + scriptId pair is skipped
a script with no compiled body is dropped, with a load diagnostic
2. The sandbox seeds handles
one ObjectHandle per attached entity, filled from the live snapshot
3. Scene scripts run, in order, one after another
4. Object scripts run, in order, one after another
5. The frame reports "started"
Three things follow from step 2 sitting before step 3:
refObjectis already populated when an object script's body runs. ReadingrefObject.nameon the first line gives you the entity's slug, not a placeholder.- The table already exists. Every entity the scene loaded is on the table before any script body evaluates. Your script never observes an empty table that is about to fill up.
onCreateddoes not fire for an entity that was already on the table. Everything present at start was created before your script existed, so put the work you would have done inonCreatedat the top level of the script body — that is the "this object is ready" moment. An entity created while scripts are running is the exception: its object script is attached before the event is dispatched, so its ownonCreateddoes fire.
What a script body should do#
Register handlers and return. The body runs once, synchronously, and everything after that is event-driven.
- Registration is global to the frame, not to the script.
globalEventsis one frozen singleton shared by every running script. Two scripts adding a handler toglobalEvents.onObjectDroppedboth get called. - A handler registered later still works — there is no "registration is closed" moment. It only misses events that already fired.
- Handler ownership is remembered. The sandbox records which script registered each handler, so a handler that throws is reported against the script that registered it, not the script whose event happened to be in flight.
- A body that throws does not stop the boot. The failure is reported as a
load-phase diagnostic naming the script, the remaining scripts still run, and the frame still reportsstarted— so the host never hangs waiting for a script that crashed.
Attachment happens at start, and again on creation#
The run list is built when the script host starts, from the live snapshot's object metadata
(falling back to the scene definition on a first load). After that it grows one way: every
objectCreated event is inspected for a metadata.scriptId, and an entity that names one gets
that object script attached (apps/web/src/scripting/objectScriptAttachment.ts, used by both
embedders — apps/web/src/ui/App.tsx and apps/web/src/ui/TableEditModeShell.tsx). The
attachment happens before the event is dispatched, so the newly attached script's own
onCreated fires for the entity it belongs to. Attaching the same script to the same entity
twice is a no-op.
That covers a world.spawnObject, a mod's api.createObject, a player's spawn, and every
gameplay path that produces an entity — a draw, a deal, either half of a split, a combine. It
does not cover a table rebuilt from a snapshot, which raises no creation events at all; see
objectCreated does not fire for a snapshot
rebuild.
Restart the scripts after a rebuild if object scripts have to be live on entities that arrived
that way.
Restarting, and what a restart clears#
In Edit Mode the viewport toolbar has ▶ Play, ⏹ Stop and ↻ Restart. A restart
disposes the sandbox iframe, drops every registered handler, rebuilds the run list from the
current snapshot, and clears the run's saved data — Edit Mode keeps script saved data in memory
for the duration of one run, so world.getSavedData() starts empty on every ▶ Play. At a real
table, saved data rides the snapshot and persists.
Starting scripts in Edit Mode also switches the simulation from Frozen to Live, because a script that spawns or rolls needs physics running to observe anything. Re-freeze afterwards if you want to inspect the result.
A script that has never been saved has no compiled body and does not run. The editor transpiles on save; the sandbox executes only the transpiled body. You get a diagnostic naming the script — "has no compiled output and will not run" — rather than silence.
The four availability values#
The availability badge answers: which kind of script can reach this member?
both#
The member is reachable from a scene script and from an object script. world and
globalEvents are injected into both kinds identically, so nearly every table-scripting member
carries this value.
object-script#
The member is reachable only through refObject, which exists only in an object script.
Known gap.
refObjectis declared as an always-presentObjectHandle, and it isundefinedin a scene script. The declaration's own comment says so — "Defined ONLY in object scripts … undefined in scene scripts" (packages/shared/src/scripting.ts) — but the type does not, so the editor offers yourefObject.flip()in a scene script and the script throws at run time. Everything aboutrefObjectworks correctly in an object script, which is where it is meant to be used. In a scene script, address entities throughworld.getObjectByIdorworld.getAllObjectsinstead. See Known limitations.
mod#
The member belongs to mod scripting. Mod scripting has one script per mod and no scene/object distinction, so every mod entry carries this value.
scene-script#
Reserved for a member reachable only from a scene script. Nothing carries it — world and
globalEvents are identical in both script kinds, and the only asymmetry runs the other way,
through refObject.
See also#
- Host authority — which peer this whole sequence happens on.
- Events and delegates — what happens after the boot sequence ends.
- Sandbox limits — why a script body is a plain program with no imports.
- The Script editor — writing, transpiling and saving a script.
- The SCRIPT section — attaching a script to one entity.
- Events — the 20
globalEventsdelegates, each with the moment it fires. ObjectHandle—onCreated, the delegate whose ordering this page pins down.- Mod scripting API —
ModSetupFunction, the one call the sandbox awaits before anything else runs.
