tools
The tools namespace — 422 functions.
globals/tools/bind
tools.bind(identity: string, positional: { any }?, named: { [string]: any }?, opts: BindOpts?) -> BindResult
Resolve a call's arguments against a tool's declared parameters,
turning named arguments into the positional call the tool actually
takes. This is the binder behind zero <toolbox> <tool> --name value
and the use_tool MCP tool's named args, so a name resolves the
same way whichever surface the caller reached for. Reads the schema
from tools.get, matches each name to a parameter (exactly first,
then case-insensitively), and reports the first unresolvable name,
a name that a positional argument already filled, a call longer than
the signature, and a required parameter skipped over while a later
one is filled. Resolves the call only — running it is tools.use.
Parameters
identitystring— Tool identity ("<toolbox>.<name>", with or without the leadingtools.).positional{ any }(optional) — Arguments already given by position, filling slots from 1.named{ [string]: any }(optional) — Arguments given by name,{ [parameterName]: value }.optsBindOpts(optional) —order— the order to visitnamedin, so the first problem reported is the caller's first (defaults to sorted, for a stable answer).prefix— written in front of every argument name in the failure message,"--"for a shell flag.whole— readnamedas ONE table argument when not one of its keys names a parameter, and as the first argument written inline when only some of them do, for a surface whose payload is ambiguous between the two readings.positionalCount— how many slotspositionalfills, for a caller that passed an explicitniland so cannot be measured by length.
Returns BindResult — { ok, call, count, failure? }. Call the tool with table.unpack(call, 1, count); on failure call is empty and failure carries the reason.
bind("camera.lookAt", {}, { target = { 0, 5, 0 } })
bind("MaterialAuthor.fromColor", {}, { color = { 1, 0, 0 } }, { whole = true })
globals/tools/create
tools.create(args: { [string]: any }) -> { ok: boolean, path: string?, identity: string?, signature: string?, error: string? }
Create a new tool on disk inside an EXISTING toolbox.
Scaffolds the .tool/ folder via asset.create("tool", …),
then writes the supplied code wrapped in a documented typed
function into init.luau (the --!desc/--!arg/--!return/
--!example doc block + signature built from the structured
metadata), the tags into .metadata, and a brief README.md
— so the authored tool is indistinguishable from a builtin.
Errors cleanly if the parent toolbox doesn't exist — call
tools.createToolbox first so the toolbox starts out with a
real description instead of the placeholder template body.
The engine's normal hot-reload pipeline picks up the new
files and binds the tool's global on the next pass.
Every toolbox created via tools.createToolbox ships a
shared.module/ (the template default has ok/fail
result-envelope constructors; users can extend or replace it
via sharedCode at create time or by editing the module
later). The generated init.luau automatically declares
local shared = require(".shared") before your function
body, so the code you pass can reference shared.ok(...)
/ shared.fail(...) (or whatever the toolbox's custom
helpers expose) directly. If a toolbox doesn't have a
shared.module/ for some reason, the require is skipped so
there's no dangling import to fail.
Parameters
args{ [string]: any }— Structured tool definition. Required:name(leaf, e.g."hello"),toolbox(parent toolbox, e.g."mytools"),code(the Luau function body — not a full module, just the statements that will become the function's body),description(one or more sentences describing what the tool does). Optional but recommended:args(array of{name, type, description}argument records),returns({type, description}for the return value),examples(array of call-site code strings),tags(array of strings for search).
Returns { ok: boolean, path: string?, identity: string?, signature: string?, error: string? } — { ok, path?, identity?, signature?, error? }. path — the new .tool/ folder's VFS path. identity — the tool's <toolbox>.<name> identity. signature — the derived name(args) -> ret signature.
tools.create({ name = "hello", toolbox = "mytools", description = "Greet.", code = "return 'hi'" })
globals/tools/createToolbox
tools.createToolbox(args: { [string]: any }) -> { ok: boolean, path: string?, identity: string?, error: string? }
Create a new toolbox folder. A toolbox is the namespace
container for tools — .toolbox/ on disk; once tools are authored
inside it they are invoked as tools.use("<name>", "<toolName>", …).
This call scaffolds the .toolbox/ folder via
asset.create("toolbox", …) and overwrites the placeholder
README.md with a real description so the toolbox doesn't ship
the template stub. Optionally seeds shared.module/ if the
caller supplies cross-tool helper code. Authoring a tool inside
this toolbox is the separate tools.create call.
Parameters
args{ [string]: any }— Structured toolbox definition. Required:name(the toolbox's leaf name, e.g."mytools"— becomes<name>.toolbox/on disk and the namespace for tool identities),description(one or more sentences describing the surface this toolbox exposes — what problem space the tools cover and who calls them). Optional:sharedCode— replacement body for the toolbox'sshared.module/init.luau. The toolbox template ALWAYS ships ashared.module/with defaultok/failresult-envelope constructors so tools in this toolbox canrequire(".shared")from day one. SupplysharedCodeonly when you want to override that default with custom cross-tool helpers (parsers, registries, …); the module body is left untouched if you omit it.path— explicit VFS destination (/zero/source/...form). Defaults to/zero/source/tools/<name>if omitted; pass an explicit path to author a library-scoped or package-internal toolbox.
Returns { ok: boolean, path: string?, identity: string?, error: string? } — { ok, path?, identity?, error? }. path — the new .toolbox/ folder's VFS path. identity — the toolbox's registered identity (<name>).
tools.createToolbox({ name = "mytools", description = "Custom tooling for my workflow." })
globals/tools/delete
tools.delete(identity: string) -> { ok: boolean, path: string?, error: string? }
Remove a previously-authored tool by identity. Deletes the
.tool/ folder and its contents from the VFS via
vfs.remove(..., { recursive = true }). The engine drops the
tool's global on the next hot-reload pass. Refuses to operate
on a path that doesn't exist (returns { ok = false } with
an explanatory error).
Parameters
identitystring— Tool identity ("<toolbox>.<name>").
Returns { ok: boolean, path: string?, error: string? } — Table shaped { ok, path?, error? }.
tools.delete("mytools.hello")
globals/tools/get
tools.get(identity: string) -> ToolMeta?
Read back a tool's assembled metadata — description, typed
signature, per-argument docs, return, examples, and tags — gathered
from the four canonical sources in its .tool/ folder. Individual
tools aren't entries in the asset index (only their parent toolboxes
are), so this resolves the toolbox via asset.resolve(toolbox, "toolbox") and reads the tool relative to it. Returns nil when the
toolbox or tool is missing.
Parameters
identitystring— Tool identity ("<toolbox>.<name>", with or without the leadingtools.).
Returns ToolMeta? — The assembled metadata { name, signature?, description, args, varargs, returns?, examples, typeDefs, tags }, or nil if not found. Each entry in args is { name, type, description, optional } in signature order, so a caller can tell a required parameter from an optional one without parsing the rendered signature. varargs is true when the tool accepts trailing arguments beyond the named parameters. typeDefs carries { name, definition } for every named type the signature refers to, resolved transitively, so a signature reading spawn(opts: SpawnOpts) can be called without opening the tool's source.
local meta = tools.get("entityOps.modify"); print(meta.signature)
globals/tools/list
tools.list(tier: (number | string)?, toolbox: string?) -> { stdout: string, value: any }
Discover registered code-mode tools, grouped by toolbox.
Default tier returns just { <toolbox> = { name, name, … } }
plus a formatted stdout listing every toolbox on one line
with its tools — compact enough that listing the whole
catalogue doesn't flood agent context. Higher tiers enrich
each entry with its one-line description (tier 2) or its full
assembled metadata — signature, args, returns, examples (tier 3).
Pass a toolbox name to restrict the output to one toolbox.
Parameters
tier(number | string)(optional) — Verbosity level:1(default) toolbox summary,2adds one-line descriptions,3adds the full assembled metadata. Pass a string instead to restrict to that toolbox at tier 1.toolboxstring(optional) — Optional toolbox name to restrict the output to (e.g."entityOps"). Passing a string as the first arg also works.
Returns { stdout: string, value: any } — Table shaped { stdout: string, value: <grouped> }. For tier 1, value is { [toolbox] = { toolName, … } }. For tier ≥ 2, value is { [toolbox] = { { name, identity, signature?, description, … }, … } }.
tools.list() -- default: every toolbox, names only
tools.list(2) -- include first-line descriptions
tools.list("entityOps") -- only the `entityOps` toolbox at tier 1
tools.list("entityOps", 2) -- only `entityOps`, with descriptions
globals/tools/search
tools.search(query: string?, opts: { toolbox: string?, limit: number? }?) -> { stdout: string, value: any }
Search registered code-mode tools by relevance, the canonical
in-engine tool-discovery entry point — callable from execute
Luau so an agent can find the tool it needs without leaving the
engine. Enumerates every tool across every toolbox (reusing the
same filesystem discovery tools.list uses), then scores each
against the query: a token hit in the tool NAME weighs most,
then its DESCRIPTION, then its TAGS. Tools scoring above zero are
returned best-first. With an empty/omitted query and no
toolbox filter, returns the whole catalogue (name + signature
- toolbox) so the agent can browse. Each returned
nameis the tool's<toolbox>.<tool>identity; invoke it with theuse_toolMCP tool (toolbox+tool+args, an array in signature order or an object naming the parameters), the form each entry'sexamplesare rendered in.
Parameters
querystring(optional) — Free-text search string. Tokenized lowercase on non-alphanumeric boundaries; each token is matched against tool name, description, and tags. Empty or nil with notoolboxfilter lists every tool.opts{ toolbox: string?, limit: number? }(optional) — Optional filters table.toolbox— keep only tools whose owning toolbox exactly matches.limit— maximum entries to return (default10; pass a larger value to return more, up to every match — there is no upper cap).
Returns { stdout: string, value: any } — Table shaped { stdout: string, value: { SearchEntry } }. value is the ranked array; each entry is { name = "<toolbox>.<tool>", signature?, description, toolbox?, tags?, examples? }. stdout is a one-line human summary of the match count.
tools.search("spawn camera")
tools.search("material", { limit = 5 })
tools.search("", { toolbox = "physics" }) -- list a toolbox
tools.search("") -- list everything
globals/tools/toolboxes
tools.toolboxes() -> { stdout: string, value: { { toolbox: string, purpose: string, toolCount: number } } }
Discover the registered toolboxes as a grouped overview — one
row per toolbox with its one-line purpose and tool count. The
toolbox-first entry point to tool discovery: an agent navigates by
DOMAIN (which toolbox), then drills into a toolbox's tools with
tools.search("", { toolbox = "<name>" }). purpose is sourced from
the toolbox's README.md (its first descriptive line), falling back
to the .metadata description.
Returns { stdout: string, value: { { toolbox: string, purpose: string, toolCount: number } } } — Table shaped { stdout: string, value: { { toolbox, purpose, toolCount } } }. value is toolbox-name-sorted; each entry is { toolbox = "<name>", purpose = "<one line>", toolCount = <n> }.
tools.toolboxes()
globals/tools/tryUse
tools.tryUse(toolbox: string, tool: string, ...: any?) -> ToolCall
Invoke one code-mode tool and report the outcome as a value. Takes the
same arguments as tools.use and resolves the toolbox the same way, and
returns { ok, value, error, toolbox, tool } for every outcome — an
unknown toolbox, an unknown tool, a tool that reported failure, and a tool
that raised all arrive as ok = false with the reason in error. The
fields are the ones the use_tool MCP surface reports, so a script
comparing several calls in one pass reads the same names it would over
MCP, and reads them without wrapping each call in pcall.
Parameters
toolboxstring— The owning toolbox — an ambient toolbox by its name ("entityOps") or a library toolbox by its scoped identity ("@lib::ns").toolstring— The tool's leaf name within that toolbox ("spawn")....any(optional)
Returns ToolCall — { ok, value, error, toolbox, tool }. value carries the tool's own value when ok is true; error carries the reason when it is false.
tryUse("entityOps", "spawn", { Model = { model = "cube" } })
globals/tools/use
tools.use(toolbox: string, tool: string, ...: any?) -> ...any
Invoke one code-mode tool by naming its toolbox and tool explicitly —
the Luau-code counterpart to the use_tool MCP tool. The normal path is
the use_tool MCP tool; this is the escape hatch for editor panels and
shipped modules that script a tool from engine Luau. Resolves the toolbox
from the runtime store (builtin, library, and user-authored runtime
toolboxes all work), calls the named tool with the remaining args, and
returns the tool's value directly — unwrapping the ZmToolResult
envelope and RAISING a Luau error when the tool fails. Because every call
names both toolbox and tool, it can never read as a tools.<box>
namespace. tools.tryUse takes the same arguments and reports the
outcome as a value instead of raising.
Parameters
toolboxstring— The owning toolbox — an ambient toolbox by its name ("entityOps") or a library toolbox by its scoped identity ("@lib::ns").toolstring— The tool's leaf name within that toolbox ("spawn")....any(optional)
Returns ...any — Everything the tool returned, in the order it returned it — a tool declaring (path, reason, frame) hands back all three, so a second and third value the tool states are read the way the tool's own signature says. Raises on an unknown toolbox / tool or a tool-reported failure. These are the tool's OWN values, not the { ok, value } envelope the use_tool MCP surface reports — reach for tools.tryUse when the call's outcome is what you want.
use("entityOps", "spawn", { Model = { model = "cube" } }, { position = {0, 2, 0} })
tools/MaterialAuthor/composeFolder
MaterialAuthor.composeFolder(folder: string, opts?: ComposeFolderOpts) -> AssetRef<material>
Scan folder for textures (loose images and .texture assets), infer each one's PBR slot from its name, and compose them into a single new Material. Errors when no slot-mappable texture is found. Returns the new AssetRef<material>.
Parameters
folderstringoptsComposeFolderOpts(optional)
Returns AssetRef<material>
"/zero/source/textures/brick"
"/zero/source/pbr/metal", { name = "Metal", metallic = 1 }
"/zero/source/pbr/tiles", { name = "Tiles", tiling = 8 }
tools/MaterialAuthor/duplicate
MaterialAuthor.duplicate(source: string | AssetRef<material>, newName: string, overrides?: MaterialOverrides) -> AssetRef<material>
Duplicate a material under newName, applying overrides. The source is resolved by AssetRef / identity / name; its mat.yaml definition is cloned (shader + every property + every texture binding carry over) under the new identity with overrides merged in, then written as the new material. Returns the new AssetRef<material>.
Parameters
sourcestring | AssetRef<material>newNamestringoverridesMaterialOverrides(optional)
Returns AssetRef<material>
"Gold", "WetGold", { roughness = 0.05 }
goldRef, "GoldEmissive", { emissive = { 1, 0.8, 0, 1 } }
tools/MaterialAuthor/fromColor
MaterialAuthor.fromColor(color: Color, opts?: FromColorOpts) -> AssetRef<material>
Create a flat-colour material in one call. color is {r,g,b} / {r,g,b,a} (array) or { r=, g=, b=, a= } (keyed), channels in [0,1]. Returns the new AssetRef<material>.
Parameters
colorColoroptsFromColorOpts(optional)
Returns AssetRef<material>
{ 1, 0, 0 }, { name = "Red" }
{ 1, 0.84, 0 }, { name = "Gold", metallic = 1, roughness = 0.2 }
tools/MaterialAuthor/fromTexture
MaterialAuthor.fromTexture(texture: string | table, opts?: FromTextureOpts) -> AssetRef<material>
Create a new material that binds texture to one slot. Accepts a renderer.texture handle, an AssetRef/.texture identity, or a VFS image path. The slot is taken from opts.slot, else inferred from the texture name (*_normal → normal_texture, …), else base_color_texture. Returns the new AssetRef<material>.
Parameters
texturestring | tableoptsFromTextureOpts(optional)
Returns AssetRef<material>
"logo.texture"
tex, { name = "Brick", slot = "base_color_texture", roughness = 0.6 }
tex, { name = "Ground", tiling = 300 }
tools/animation/observe
animation.observe(target?: (string | EntityRef)) -> AnimObserveResult
What the engine is posing right now, one line per body: whether its pose is actually changing, the clip driving it with its playhead, how many of its bones that clip reaches, and — when it is not moving — the one reason why. Call it with no arguments for every body in the scene, or pass an entity to narrow it to that one (a character root resolves down to the skinned body under it).
Parameters
target(string | EntityRef)(optional)
Returns AnimObserveResult
"hero"
tools/animation/play
animation.play(animation: string | AssetRef<animation>, entityArg: string | EntityRef, opts?: AnimPlayOpts) -> AnimPlayResult
Play an animation clip on an entity's skinned body for a test window, then clean up. Resolves animation by identity or name and entity by id or name, finds the SkinnedModel to drive, and plays it through the canonical AnimGraph — the same path ClipPlayer uses. Returns { ok = false, error } when the clip, the entity, or a SkinnedModel under it cannot be found. When the body already runs an animation the clip overrides it for the window and the original resumes untouched on cleanup.
Parameters
animationstring | AssetRef<animation>entityArgstring | EntityRefoptsAnimPlayOpts(optional)
Returns AnimPlayResult
"A_Walk_F_Masc", "hero"
"Idle", "hero", { offset = "5s", loop = true, duration = 8 }
tools/api/docs
api.docs(...) -> string
Browse engine VFS documentation under /zero/docs. No args lists top-level categories. Partial paths list a directory's entries (one entry per line, trailing / on directories). Full paths return the document content as a string. Each argument is a single path segment — they are joined with / to form the lookup path.
Returns string
-- list categories
"api" -- list namespaces
"api", "physics" -- list physics methods
"api", "physics", "raycast" -- full raycast docs
tools/api/searchDocs
api.searchDocs(query: string) -> string
Search the engine's documentation: the generated reference pages under /zero/docs AND the prose guides that explain the concepts behind them. The query is split on whitespace; a doc matches when EVERY word appears (case-insensitive substring) in its PATH or its BODY text. Path matches rank first, then body-only matches. Searches content, so a concept like "global illumination" or "indirect light" surfaces the tools/components/guides that describe it — not just docs named that. Every returned line is a path vfs.read accepts.
Parameters
querystring
Returns string
"velocity"
"global illumination"
"indirect light"
"voxel pivot"
tools/appearance/replaceWithContent
appearance.replaceWithContent(targets: eo.Targets, source: string, opts?: ReplaceOpts) -> ReplaceReport
Replace one or many entities with a content asset, resolution-first: pass a name / path / identity STRING (limit the search with type), and the tool resolves it and reports the canonical asset it hit. Each target entity (and its WHOLE subtree) is removed, and a fresh spawn of the content takes its slot — same parent, local position, rotation, and name; scale kept unless keepScale = false. Components on the removed entities do not carry over. Targets are entity names or ids, one or a list, or a scene.find QUERY resolved for you — { name = { "tree" } }, { component = { "Light" } }, { under = { "forest" } }. dryRun = true resolves everything and returns the plan (which entities would be removed, how big each subtree is, what would spawn) without mutating.
Parameters
targetseo.TargetssourcestringoptsReplaceOpts(optional)
Returns ReplaceReport
"rider_blockout", "a_snowboarder"
{ "tree_1", "tree_2", "tree_3" }, "pine", { type = "bundle" }
{ "tree_1", "tree_2" }, "pine", { dryRun = true }
tools/appearance/setModel
appearance.setModel(root: string, model: string | table, opts?: AppearanceOpts) -> AssignSummary
Assign a model (mesh) to the Model / SkinnedModel-bearing entities found by searching an entity hierarchy from root. Sets the model field on each match's Model / SkinnedModel. You don't have to locate the exact entity; give the root and the model, and this walks the subtree. By default it assigns to EVERY match under the root; pass { first = true } for only the first match.
Parameters
rootstringmodelstring | tableoptsAppearanceOpts(optional)
Returns AssignSummary
"Rocks", "sphere"
"Rocks", boulderMeshRef, { match = "rock_%d+" }
tools/appearance/swapMaterials
appearance.swapMaterials(material: string | AssetRef<material>, opts?: SwapMaterialsOpts) -> AssignSummary
Batch-assign one material across entity hierarchies in a single call. material is a string — combed against EVERY material in the project (exact identity wins, then exact leaf name, then the first case-insensitive substring match) — or an AssetRefField.resource("material", …) / Field.assetRef("material", …) (on Model, SkinnedModel, and any other component that declares one) is set to the resolved material. opts.roots selects the hierarchies — one entity id/name, an array of ids/names/entity proxies, or absent for the ENTIRE scene; each is scanned recursively, and entities reached through more than one root are assigned once. A blue outline flashes on each affected entity for visual confirmation.
Parameters
materialstring | AssetRef<material>optsSwapMaterialsOpts(optional)
Returns AssignSummary
"weathered_stone" -- whole scene
goldRef, { roots = "Castle", first = true }
"brick", { roots = { "wall_a", "wall_b" }, match = "wall" }
tools/appearance/swapShader
appearance.swapShader(roots?: (string | { any }), shader: string | AssetRef<shader>, opts?: AppearanceOpts) -> ShaderSwapResult
Convert every material in use across entity hierarchies to a new shader, in one call — "convert every single material in this scene to the ps1 shader". Scans the targeted hierarchies recursively, reads every component public field declared Field.resource("material", …) / Field.assetRef("material", …) (discovered from the component schemas, so any component carrying a materialRef field participates), collects the DISTINCT materials those fields hold, and routes each through the material asset's setShader — which preserves texture links and property values by canonical role (MAIN_TEX→albedo→base_color_texture and friends), rewrites the material asset's mat.yaml, and marks every instance dirty so the change persists and propagates. For a material you already hold, call matRef:setShader(shader) directly — this tool is for discovering the materials you'd otherwise have to enumerate yourself.
Parameters
roots(string | { any })(optional)shaderstring | AssetRef<shader>optsAppearanceOpts(optional)
Returns ShaderSwapResult
nil, "@builtin::shaders.ps1" -- whole scene
"Castle", unlitShaderRef
{ "wall_a", "wall_b" }, "pbr"
tools/assets/backfillSearchMeta
assets.backfillSearchMeta(opts?: BackfillOpts) -> BackfillResult
Re-derive the search .metadata (component set, humanoid flag) for every existing asset of the given types by invoking each asset's reindexSearchMeta ref method. Assets minted before the deriver existed carry no .metadata.components / .metadata.humanoid, so their search facets read false until this runs. Idempotent — re-running recomputes the same values. Only assets whose type exposes reindexSearchMeta are touched; the rest are counted as scanned and left alone.
Parameters
optsBackfillOpts(optional)
Returns BackfillResult
{ types = { "bundle" } }
tools/assets/describe
assets.describe(assetName: string, opts?: DescribeOpts) -> string
Describe an asset in agent-readable markdown: identity, type, scope, origin, description (README lead), tags, a Behavior section listing the operations any asset of this type answers to (its assetType's ref:method(...) surface, with signatures and descriptions), and a type-specific detail section (fields, events, and the component's own instance methods for a component; exports for a module; the call contract for a tool; ...). Every method, event, and field is rendered as a full signature so nothing needs the source read. Resolves assetName the same way assets.find results resolve. Pairs with assets.find (find -> describe).
Parameters
assetNamestringoptsDescribeOpts(optional)
Returns string
"@builtin::components.Camera"
tools/assets/fetchInUse
assets.fetchInUse(targets?: (string | EntityRef | { string | EntityRef })) -> FetchSummary
Fetch the unique set of asset references in use on the targeted entities — materials, meshes, textures, bundles, and every other category a component field can hold. targets is one entity id/name, an array of ids/names/entity proxies, or nil for EVERY entity in the scene; root targets are scanned recursively through their children. For each entity, every component's public data is scanned (nested tables to depth 8) for AssetRef envelopes; refs are deduped by guid and returned with the entities that use them — the agent's one-call inventory of what the targeted content is made of.
Parameters
targets(string | EntityRef | { string | EntityRef })(optional)
Returns FetchSummary
-- everything the scene references
"Castle" -- everything the Castle hierarchy uses
{ "wall_a", "wall_b" }
tools/assets/find
assets.find(substring: string, category?: string, opts?: { subassets: boolean? }) -> FindSummary
Find every asset in the entire project whose identity contains substring (case-insensitive). Searches the full asset index — every category, every library — and returns the matches grouped by category. Pass category to restrict the search to one kind (discover the available kinds via asset.categories()).
Parameters
substringstringcategorystring(optional)opts{ subassets: boolean? }(optional)
Returns FindSummary
"car"
"stone", "material"
"wheel", nil, { subassets = true }
tools/assets/fromEntity
assets.fromEntity(targets?: (string | EntityRef | { string | EntityRef }), opts?: FromEntityOpts) -> FromEntitySummary
Capture everything an entity is carrying at RUNTIME — every live procedural mesh / texture handle held in a component's resource field, across the whole hierarchy — as durable assets, and repoint each field at the new persistent asset. A runtime mesh (renderer.mesh.create) has no asset behind it, so anything keyed to it (a GI bake, a saved scene) is session-only; this makes it permanent in one call. Every public resource field of every component participates (discovered by field reflection), so it reifies ALL of an entity's runtime content, not a single hard-coded type. Persistent references (already assets) are left untouched. In edit mode the new assets persist to the world's source; in play mode they land in the ephemeral runtime store, so run this in EDIT to make content durable. Runtime resources shared across several fields are reified once and every field is repointed at the one asset.
Parameters
targets(string | EntityRef | { string | EntityRef })(optional)optsFromEntityOpts(optional)
Returns FromEntitySummary
-- freeze every runtime resource in the scene
"cornellRoom" -- freeze the cornellRoom hierarchy's runtime meshes
nil, { dryRun = true } -- report what would be reified, write nothing
tools/assets/generatePreviews
assets.generatePreviews(opts?: GeneratePreviewsOpts) -> GenerateSummary
Queue a render of the missing preview.png for every world material, texture, and bundle under root (default: the whole world source), and remove empty leftover README.md files inside the visited assets. Skips assets whose preview already exists unless force is set. Returns as soon as the scan finishes; the queued renders drain serially in the background — poll assets.previewStatus until pending is 0.
Parameters
optsGeneratePreviewsOpts(optional)
Returns GenerateSummary
{ root = "/source/projects/mygame" }
tools/assets/new
assets.new(source: string | RuntimeHandle, name?: string, opts?: NewOpts) -> AssetRef<mesh | texture>
Persist a LIVE runtime GPU resource into a durable asset. source is a resource handle from renderer.mesh.create / renderer.texture.create, or a bare guid string naming a GPU-resident resource (paired with opts.category). The resource is read back from the GPU (yields a frame or two), encoded, and written as a new asset via asset.create, so anything keyed to it — a GI bake, a saved scene, a material slot — survives a reload instead of regenerating with a fresh identity. In edit mode the asset persists to the world's source; in play mode it lands in the ephemeral runtime store, so reify in EDIT to make content durable. Raw in-hand geometry/pixels with no GPU resource go through asset.create(<type>, name, data) directly — this tool freezes a live handle.
Parameters
sourcestring | RuntimeHandlenamestring(optional)optsNewOpts(optional)
Returns AssetRef<mesh | texture>
renderer.mesh.create({ positions = p, indices = i }), "tree"
meshGuid, "terrain", { category = "mesh" }
tools/assets/previewStatus
assets.previewStatus() -> PreviewStatus
Report the preview work queue: renders still pending, the asset currently rendering, session totals, and the most recent failures. Poll after generatePreviews until pending reaches 0 and active is empty.
Returns PreviewStatus
tools/assets/typeBehavior
assets.typeBehavior(name: string) -> BehaviorResult
What an asset of this TYPE can do — the operations every ref of it answers to (ref:method(...)), the lifecycle hooks it runs, and whether it can become an entity in a scene. This is the difference between knowing a type exists and knowing what it gives you: a type that already evaluates, instantiates, validates and recompiles itself is scaffolding to build inside, not a name in a list. Read it with assets.typeDoc for the prose, and assets.types to find the name.
Parameters
namestring
Returns BehaviorResult
"procGraph"
tools/assets/typeDoc
assets.typeDoc(name: string) -> { [string]: any }
Read one asset type's own documentation — what the type IS, the shape of its folder, how it is authored, and what it is for. This is the type's README as its author wrote it, so it says things no signature can: which file is the source, what regenerates when, what the type does for you. Read this before building anything that resembles what the type describes. assets.types finds the name to pass here.
Parameters
namestring
Returns { [string]: any }
"procGraph"
"terrain"
tools/assets/types
assets.types(query?: string) -> TypesResult
Every KIND of asset this engine can make — the scaffolding available to build inside, as opposed to assets.find (content that exists) or search_tools (operations you can call). Each row is a registered assetType: what it is, the asset.create call that makes one, and whether it can become an entity in a scene. Reach for this BEFORE writing a system of your own. A job that looks like "no tool does this, so I will write it" is usually a job with a type already shaped for it — a graph that regenerates from parameters, a terrain, a voxel template, a population — and building beside one of those is a few lines where building from scratch is a project.
Parameters
querystring(optional)
Returns TypesResult
"procedural"
"terrain"
tools/assets/users
assets.users(assetArg: string | AssetRef) -> UsersSummary
Find every entity in the active layer that uses an asset. Give the asset as a word — the entire project is combed case-insensitively and the first matching asset is used — or as an AssetRef you already hold. Every entity's component data is scanned recursively for references to that asset, and each use site is reported with the entity, component, and field it lives in.
Parameters
assetArgstring | AssetRef
Returns UsersSummary
"pickup_car"
goldMaterialRef
tools/baking/__all
baking.__all(opts: AllOpts, scope?: (string | EntityRef | { string | EntityRef })) -> AllResult
Parameters
optsAllOptsscope(string | EntityRef | { string | EntityRef })(optional)
Returns AllResult
tools/baking/all
baking.all(opts?: AllOpts) -> AllResult
Run the entire GI bake in one call — static-surface lightmaps, an auto-placed irradiance probe volume for moving entities, and auto-placed reflection probes — then report what remains. This is the finalize step for a static scene: it takes the scene from "everything recomputed every frame" to baked. Each stage can be skipped (skipLightmaps / skipProbes / skipReflections); resolution / samples tune quality. Returns each stage's result plus a fresh remaining coverage report. This runs for many seconds on a real scene and is handed back as a background task: check how far it has got with this toolbox's status tool, and a lightmap bake finished notice reports the end.
Parameters
optsAllOpts(optional)
Returns AllResult
-- bake everything in the scene
{ resolution = 128, samples = 128 }
{ scope = "arena", skipReflections = true }
tools/baking/clear
baking.clear(opts?: ClearOpts) -> ClearResult
Clear baked GI across a scope — restores each static surface's original material (removing its lightmap), despawns irradiance-probe volumes, unregisters reflection probes, and deletes the cubemap assets their bake wrote. A probe attached to a scene object loses its ReflectionProbe component and the object stays; an entity the bake placed to hold a probe is despawned. Use it to re-bake from a clean slate or strip GI from a region. Each category can be kept (keepLightmaps / keepProbes / keepReflections). Returns how many of each were cleared.
Parameters
optsClearOpts(optional)
Returns ClearResult
-- clear everything in the scene
{ scope = "arena" }
{ keepReflections = true }
tools/baking/detect
baking.detect(scope?: (string | EntityRef | { string | EntityRef })) -> DetectReport
Report what in the scene needs baking / re-baking and why — the bake-coverage check. Scans the scene (or a scope) for the state that quietly wrecks performance and leaves indirect light missing: static geometry with no bake (no lightmap and no probe-field coverage), shadow-casting point/spot lights re-rendering their shadow every frame, movers outside every probe volume's field, and reflective scenes with no reflection probe. Each finding names the one call that fixes it. Returns clean = true with an empty findings list when the scene is already covered.
Parameters
scope(string | EntityRef | { string | EntityRef })(optional)
Returns DetectReport
-- whole scene
"interior"
tools/baking/lightmaps
baking.lightmaps(opts?: LightmapOpts) -> LightmapResult
Bake static-geometry lightmaps across a scope in one call — resolves the static surfaces under scope (every visible Model whose resolveMobility() is "static"; movable geometry, physics bodies, and players sample probe volumes instead) and bakes full irradiance into a per-surface lightmap, no per-entity setup. Surfaces whose UVs tile or overlap are bound to the covering probe volume instead (counted in probeLit); set Model.mobility to override a wrong mobility derivation. match narrows to surfaces whose name contains a substring; resolution / samples / intensity set quality. Returns how many baked, how many went to the probe field, how many were skipped, and a per-surface report. This runs for many seconds on a real scene and is handed back as a background task: check how far it has got with this toolbox's status tool, and a lightmap bake finished notice reports the end.
Parameters
optsLightmapOpts(optional)
Returns LightmapResult
-- whole scene
{ scope = "level_geo", resolution = 128, samples = 32 }
{ match = "floor" }
tools/baking/mobility
baking.mobility(opts?: MobilityOpts) -> MobilityResult
Report (and optionally batch-set) geometry mobility across a scope. Every Model in scope is listed with its authored mobility field and what it resolves to — static geometry receives lightmaps and occludes baked light; movable geometry samples probe volumes. Pass set to override the field on every matched Model in one call (the exclusion switch when the automatic derivation gets an entity wrong); omit it to just inspect. match narrows by name substring.
Parameters
optsMobilityOpts(optional)
Returns MobilityResult
-- report the whole scene
{ match = "crate", set = "static" }
{ scope = "props", set = "movable" }
tools/baking/probes
baking.probes(opts?: ProbeOpts) -> ProbeResult
Place irradiance light-probe volumes automatically and bake them — the whole light-probe pass in one call, no hand placement. Gathers the scene's point/spot lights (limited by scope), CLUSTERS them by influence overlap, and drops one right-sized volume per cluster (so separated rooms get separate volumes, not one box over the dead space between them). Probe density comes from spacing (world units between probes) with a hard maxProbes cap per volume, so a big volume gets coarser probes rather than a runaway grid; maxVolumes bounds the volume count by merging the nearest clusters. When a scene has no point/spot lights (sun/ambient only), it falls back to one volume over the scene's geometry bounds. Each baked field publishes into the renderer's irradiance-volume set and persists into the scene's baked-lighting container — every standard-PBR fragment inside a volume (movers, avatars, freshly spawned props) samples it with no per-entity setup, and a fresh boot restores it with no re-bake. Returns each placed volume (id, bounds, grid resolution, probe count) plus totals. The same placement is available to component/execute code as require("@builtin::systems.globalIllumination.volumeProbe").autoPlace(opts).
Parameters
optsProbeOpts(optional)
Returns ProbeResult
-- whole scene, defaults
{ spacing = 2, samples = 128 }
{ scope = "interior", maxProbes = 4096 }
tools/baking/reflections
baking.reflections(opts?: ReflectionOpts) -> ReflectionResult
Place reflection probes across the scene automatically and bake them — the whole reflection pass in one call, no hand placement. The tool bounds the scene's geometry (limited by scope), spreads a small set of probes evenly through that volume, and bakes each into its cubemap; reflective surfaces then sample the nearest probe(s), proximity-blended by the renderer. count defaults to a value derived from the scene size and is clamped to the renderer's 8-probe budget; radius defaults to a size that overlaps neighbours for seamless coverage; bake = false places without baking. Returns the placed probes (id, position, radius) and how many baked.
Parameters
optsReflectionOpts(optional)
Returns ReflectionResult
-- whole scene, auto count
{ count = 4 }
{ scope = "lobby", count = 1, radius = 20 }
tools/baking/showProbes
baking.showProbes(opts?: ShowProbesOpts) -> ShowProbesResult
Visualize the baked irradiance field — drop an unlit, color-coded marker at every probe position so an invisible SH bake becomes something you can see and judge. Each marker is tinted by the light that probe captured (brighter where the bake gathered more light, dark in occluded corners), so you can read the falloff through the volume and catch a bake that came back empty. Reads the volume's baked probe buffer (bake first). volume targets one volume (default: every VolumeProbe in the scene); gain scales brightness (default auto-normalizes to the brightest probe); stride thins a dense grid; scale sizes the markers. Re-run to refresh; baking.clear (or deleting the gi_probe_viz entity) removes it.
Parameters
optsShowProbesOpts(optional)
Returns ShowProbesResult
-- every baked volume
{ gain = 4, scale = 0.2 }
{ volume = "probe_volume_1", stride = 2 }
tools/baking/status
baking.status() -> BakeStatus
Report the running bake's progress: whether one is going, how many receivers it has FINISHED (done, which percent is derived from) and how many it has REACHED (started) out of the total it resolved, which one it is on, and how long it has been running. A bake is handed off as a background task, so this is how you check on it while it works; the lightmap bake finished notice reports the end.
Returns BakeStatus
-- while a bake is running
-- poll until `running` is false
tools/cam/frame
cam.frame(entityId: string | EntityRef, opts?: FrameOpts)
Position a camera to frame a target entity from a distance + angle, then point it at the entity in one call. opts.angle accepts three forms (matches capture.fromEntity): a number (elevation pitch in degrees — legacy scalar), {yaw, pitch} (array form), or {yaw=, pitch=} (named form). opts.yaw is still respected as a fallback when opts.angle is a number or doesn't carry a yaw entry.
Parameters
entityIdstring | EntityRefoptsFrameOpts(optional)
'my_cube'
'my_cube', { distance = 20, angle = 45, yaw = 90 }
'my_cube', { angle = {90, 60} }
tools/cam/get
cam.get(camId?: (string | EntityRef)) -> CamGetResult
Read camera info — transform + fov. Defaults to the active camera (camera.active()) when camId is omitted. Returns { error = ... } when no camera is active, and when the reference names no entity.
Parameters
camId(string | EntityRef)(optional)
Returns CamGetResult
'cam2'
tools/cam/lookAt
cam.lookAt(camIdOrX: string | EntityRef | number, xOrY: number, yOrZ: number, zOrNil?: number)
Point a camera at a world position. Computes a look-at rotation (yaw + pitch) and writes it to the camera's localRotation. Two call shapes: cam.lookAt(x, y, z) (uses active camera) and cam.lookAt(camId, x, y, z) (specific camera). Logs and returns when no camera / target is available, when the camera has no position, or when the camera is already at the target (length < 0.001).
Parameters
camIdOrXstring | EntityRef | numberxOrYnumberyOrZnumberzOrNilnumber(optional)
0, 0, 0
'cam2', 5, 3, 0
tools/cam/spawn
cam.spawn(name: string, ...) -> string
Spawn a camera entity with a Camera component at a position. Supports four call shapes for the position: no args (origin), ({x,y,z}, opts?), (opts) (opts only), or (x, y, z, opts?). When opts.renderTarget is set, creates a GPU texture and points the camera at it via Camera:setTargetTexture; sample it by entity(id).component.get("Camera").textureHandle and free it with renderer.destroy when done.
Parameters
namestring
Returns string
"main", 5, 5, 5
"rt_cam", { renderTarget = { name = "feed", width = 256, height = 256 } }
tools/camera/bindToPlayer
camera.bindToPlayer(camera?: (EntityRef | string)) -> BindToPlayerResult
Make the joining player use this camera in play — bind it to the active scene's PlayerPrototype. The prototype's camera role must be a descendant of its subtree (it is cloned with the player on spawn), so this re-parents the camera into the prototype (keeping its world pose) and sets the prototype's camera ref to it. This is how you choose the player's camera: pair it with a static behavior (camera.set { behavior = "@builtin::controller.menu" }) for a fixed PS1-style camera the player moves within, or a follow behavior (orbital_follow, third_person_follow, first_person, …) for a tracking one. On spawn the player's body is bound as the camera's follow target; a static behavior ignores that and holds its pose. Errors when the scene has no PlayerPrototype (a player-less scene has no player camera to bind). Returns { prototype, camera }.
Parameters
camera(EntityRef | string)(optional)
Returns BindToPlayerResult
"fixedCam"
tools/camera/create
camera.create(name: string, position?: (vec3 | number), opts?: CameraCreateOpts) -> shared.CameraInfo
Spawn a new camera entity at a position, with any starting parameters. opts accepts fov, near, far, priority, renderLayers (a layer-name spec like "all !ui"), debugChannel, follow (an entity the camera behavior tracks), behavior (a cameraBehavior-tagged component), lookAt (a world position or entity to aim at), and target (render-to-texture: true or { width, height, name } renders offscreen; omit for the viewport). The scene's primary camera is spawned for you from the PlayerPrototype — use this for ADDITIONAL cameras (a security view, a cutscene angle, a render-texture feed). Returns the new camera's full parameter record.
Parameters
namestringposition(vec3 | number)(optional)optsCameraCreateOpts(optional)
Returns shared.CameraInfo
"securityCam", { 0, 6, 12 }, { fov = 50, renderLayers = "all !ui" }
"feedCam", { x = 2, y = 3, z = 2 }, { target = { width = 512, height = 512 }, lookAt = "player" }
tools/camera/frame
camera.frame(camera?: (EntityRef | string), target: EntityRef | string, opts?: CameraFrameOpts) -> shared.CameraInfo
Position and aim a camera to frame a target entity in one step: place the camera at distance from the target on the yaw/pitch angle, then look at it. opts accepts distance (default 10), yaw (default 0), and pitch (default 20), all degrees. Pass nil for the first argument to frame with the active camera. Returns the camera's updated parameter record.
Parameters
camera(EntityRef | string)(optional)targetEntityRef | stringoptsCameraFrameOpts(optional)
Returns shared.CameraInfo
"securityCam", "player", { distance = 8, yaw = 30, pitch = 15 }
nil, "statue"
tools/camera/get
camera.get(camera?: (EntityRef | string)) -> shared.CameraInfo
Read the full parameter record for one camera — position, rotation, fov, near, far, priority, render-layer spec, debug channel, render-target guid, behavior, and follow target — beside whether it is enabled (the switch that decides whether it renders at all), whether it is rendering, the reason it is not, the projection the renderer built for it in frame, and mismatch naming every field where that frame disagrees with the record above. Pass a camera name/id/proxy, or nothing to read the active (on-screen) camera, falling back to the scene's main camera.
Parameters
camera(EntityRef | string)(optional)
Returns shared.CameraInfo
"securityCam"
tools/camera/list
camera.list() -> { shared.CameraRow }
List every camera in the scene, enumerated from the live ECS so a just-spawned, hidden, or runtime camera is never missed, sorted by descending priority (the order the renderer resolves the on-screen camera). Each row marks whether it is the active camera (drawn this frame), whether it is enabled — the per-camera switch that decides whether it competes for the viewport or renders into a target at all — whether it is rendering and the reason it is not, whether it is the player camera (the one the PlayerPrototype uses — the joining player's view), and its role — "main" (highest-priority active non-editor), "editor" (the editor fly-camera), or "" — plus its priority, fov, render-layer spec, and render-target guid. For the projection the renderer actually built for each camera, and what each cost, read camera.observe().
Returns { shared.CameraRow }
tools/camera/lookAt
camera.lookAt(camera?: LookAtRef, target?: LookAtRef) -> shared.CameraInfo
Aim a camera at a world position or at another entity. The target is an entity name/id or a position { x, y, z } / { x =, y =, z = }. Pass a single target to aim the active camera, or a camera plus a target to aim a specific one. Returns the camera's updated parameter record.
Parameters
cameraLookAtRef(optional)targetLookAtRef(optional)
Returns shared.CameraInfo
"securityCam", "player"
{ 10, 0, 5 }
tools/camera/set
camera.set(camera?: (EntityRef | string | CameraSetParams), params?: CameraSetParams) -> shared.CameraInfo
Change one or more parameters on a camera in a single call. Accepts fov, near, far, priority, debugPass (the diagnostic view this camera renders, BY NAME — "final" (lit), "normal", "depth", a content view like "lightmap", …; renderer.debugPass.list() enumerates them; the live viewport draws the active camera's pass), renderLayers (a layer-name spec like "all !ui" — a bare name includes a layer, !name excludes it), behavior (a cameraBehavior-tagged component ref), and follow (the entity the behavior tracks). debugChannel accepts the raw numeric channel for the same effect. Pass no camera (or nil) to change the active camera; pass { ... } as the only argument for the same. Returns the camera's updated parameter record.
Parameters
camera(EntityRef | string | CameraSetParams)(optional)paramsCameraSetParams(optional)
Returns shared.CameraInfo
"securityCam", { fov = 50, priority = 5 }
{ debugPass = "normal" }
{ debugPass = "final" } -- back to the lit image
tools/capture/cleanup
capture.cleanup() -> number
Clear every file in /source/tmp/capture/. Walks the directory with vfs.list and removes each non-directory entry via vfs.remove. Safe to call on a missing / empty directory.
Returns number
tools/capture/collage
capture.collage(opts: CaptureCollageOpts) -> (shared.CaptureResult?, string?)
Render a GRID of frames into one image and write it before returning. What varies from cell to cell is what you pass, and a collage says so rather than assuming: setups gives a cell per whole camera set-up — the contact sheet of a shot list, each entry written the way a single capture is aimed — viewpoints gives a cell per named view of one subject (front, back, left, right, top, bottom, iso — or "sides" for all six), passes gives a cell per render pass (final, albedo, normal, depth, motion_vectors, and any content-registered capture view), and duration gives a cell per sample across that many seconds of gameplay. Naming a station axis (setups or viewpoints) AND passes lays out a 2D grid: a row per station, a column per pass, so reading across a row is one set-up under every pass and reading down a column is one pass from every set-up. A collage with no axis is refused naming all of them; setups and viewpoints both say where the camera stands and cannot combine; and duration combines with none of them — a cell differing both in what it looks at and in when it was taken answers neither question. The grid shape is worked out from the number of cells; pass grid ("3x2") to state it. width and height are the whole SHEET, and a cell is rendered at the box the grid divides out of it — so a cell holds the same picture a single capture at that cell's own width/height holds, whatever the grid's shape, and a round subject reads round in every cell of a 4x1. Every cell comes back labelled on the result with the station it was rendered from and the pixels it was rendered at, so a grid is readable from the response instead of guessable from the pictures.
Parameters
optsCaptureCollageOpts
Returns (shared.CaptureResult?, string?)
{ setups = { { position = { -14.6, 1.35, 4.5 }, lookAt = { -16.3, 1.1, 1.6 }, fov = 28, label = "shot 1" }, { position = { 8, 3, 12 }, lookAt = { 0, 1, 0 }, fov = 50, label = "shot 2" } }, grid = "2x1" }
{ entity = "crate", viewpoints = "sides", basis = "local", projection = "orthographic", isolate = true }
{ entity = "crate", basis = "local", viewpoints = { "front", "iso" }, passes = { "final", "normal" } }
{ passes = { "final", "albedo", "normal", "depth" } }
{ entity = "salt_pan", viewpoints = "sides", basis = "local", clearAir = true }
{ duration = 3, grid = "3x3", path = "/source/tmp/capture/motion.jpg" }
tools/capture/fromCamera
capture.fromCamera(camera: string | EntityRef, opts?: (string | FromCameraOpts)) -> (string?, string?, shared.CaptureFrame?)
Render the scene from a specific NAMED camera, writing the PNG before returning — the returned path exists on success. By DEFAULT it uses the camera's exact authored settings — its world-space pose plus its own fov, near, far, and the scene layers it draws: the camera's own renderLayers with the editor's chrome (EditorUI) and the authoring overlays (debug) taken off, since those are drawn for whoever is editing rather than by that camera into the world. So the frame is precisely what that camera draws of the scene, without changing what is on screen. Pass opts to OVERRIDE any of those for this one capture WITHOUT touching the camera: pass (what the frame HOLDS, by name — "final", a built-in diagnostic pass like albedo / normal / depth / roughness, or a capture view a render feature published; it picks the channel rendered, the layers that pass is read under, and whether the post-process chain runs over it — a diagnostic buffer is read flat), view (a capture view named directly, by the name a render feature published it under — renderer.captureView.list() enumerates them, and a name no feature published is refused against that list), renderLayers (a layer-name spec like "all !ui"), debugChannel (the same channel selection by number), fov, near, far, width, height, and path. Use it to check any specific camera — a security view, a cutscene angle, a render-texture feed — and to preview it under different layers or a diagnostic pass.
Parameters
camerastring | EntityRefopts(string | FromCameraOpts)(optional)
Returns (string?, string?, shared.CaptureFrame?)
"securityCam"
"securityCam", { pass = "normal" }
"securityCam", { view = "overdraw" }
"securityCam", { renderLayers = "all !ui", debugChannel = 0 }
"securityCam", "/source/tmp/capture/sec.png"
tools/capture/fromEntity
capture.fromEntity(opts: CaptureEntityOpts) -> (string?, string?, shared.CaptureFrame?)
Render the scene framed on an entity (or several) and write the PNG before returning — the returned path exists on success. The camera auto-fits the union world-space bounds of the target(s), so the subject fills the frame at any scale; pass distance to orbit at a fixed radius instead.
Parameters
optsCaptureEntityOpts
Returns (string?, string?, shared.CaptureFrame?)
{ entity = "player" }
{ entity = "player", pass = "normal" }
{ entity = "crate", viewpoint = "front", basis = "local" }
{ entity = "crate", viewpoint = "top", basis = "local", projection = "orthographic", isolate = true }
{ entity = { "house", "tree" }, angle = {90, 15}, path = "/source/tmp/capture/yard.png" }
{ entity = "player", deterministic = true }
{ entity = "salt_pan", clearAir = true }
tools/capture/fromPosition
capture.fromPosition(opts: CapturePositionOpts) -> (string?, string?, shared.CaptureFrame?)
Render the scene from a world-space position and write the PNG before returning — the returned path exists on success. Spawns an ephemeral offscreen camera, renders once, reads the target back and writes it; all resources clean up before the call returns.
Parameters
optsCapturePositionOpts
Returns (string?, string?, shared.CaptureFrame?)
{ position = {0, 5, 10}, lookAt = {0, 0, 0} }
{ position = {0, 5, 10}, lookAt = {0, 0, 0}, pass = "albedo" }
{ path = "/source/tmp/capture/front.png", position = {0, 5, 10}, lookAt = {0, 0, 0} }
{ position = {0, 5, 10}, lookAt = {0, 0, 0}, deterministic = true }
{ position = {0, 5, 10}, lookAt = {0, 0, 0}, clearAir = true }
tools/capture/oneshot
capture.oneshot(opts?: CaptureOneshotOpts) -> (CaptureHandle?, string?)
Spawn an ephemeral offscreen camera configured from opts, trigger one render, and return the handles so the caller can read the render target back synchronously and clean up. Unlike fromPosition / fromEntity / viewport, this does NOT spawn a task to handle readback — it returns immediately, making it usable from MCP in edit mode where the task scheduler does not tick.
Parameters
optsCaptureOneshotOpts(optional)
Returns (CaptureHandle?, string?)
{ source = "entity", entity = "Player", distance = 8, angle = {45, 20}, pass = "normal" }
tools/capture/viewport
capture.viewport(opts?: (string | ViewportOpts)) -> (string?, string?, shared.CaptureFrame?)
Capture the current viewport and write the PNG before returning — the returned path exists on success. Mirrors the on-screen camera through the shared capture core (source = "screen"), so the frame is taken from that camera's pose, through that camera's own lens, under the render layers it draws the screen with, the editor's own chrome left out: the chrome stands around the viewport's picture, and the frame holds that picture — the scene, its debug overlays and the content UI drawn into it. Pass opts to OVERRIDE any of that for this one capture: width / height for a raster other than the viewport's own size, renderLayers (a layer-name spec like "all !EditorUI !debug") for the frame without the editor's chrome over it, pass for what the frame HOLDS ("final", a built-in diagnostic pass like albedo / normal / depth, or a capture view a render feature published), clearAir for the frame with the air between the camera and a surface taken out, and postProcessing to take the frame with the grade off.
Parameters
opts(string | ViewportOpts)(optional)
Returns (string?, string?, shared.CaptureFrame?)
"/source/tmp/capture/screenshot.png"
{ path = "/source/tmp/capture/clean.png", renderLayers = "all !EditorUI !debug" }
{ path = "/source/tmp/capture/half.png", width = 960, height = 540 }
{ path = "/source/tmp/capture/still.png", deterministic = true }
{ path = "/source/tmp/capture/normals.png", pass = "normal" }
{ path = "/source/tmp/capture/own_colour.png", clearAir = true }
tools/characterController/install
characterController.install(entityId: string, opts?: InstallOpts) -> InstallResult
Install CharacterController + Locomotion + MovementState on an entity, with an optional third-person camera child. Applies an optional preset (e.g. 'synty') for default property values, then layers user overrides on top. Idempotent per component — running install twice on the same entity is a no-op for already-present components. The camera child carries a Camera component plus the orbital_follow controller targeting the owning entity; skip it with camera = false.
Parameters
entityIdstringoptsInstallOpts(optional)
Returns InstallResult
"Player", { preset = "synty" }
player.id(), { preset = "humanoid", overrides = { moveSpeed = 8.0 } }
entityId, { camera = false } -- no camera child
tools/characterController/uninstall
characterController.uninstall(entityId: string) -> UninstallResult
Tear down everything characterController.install() added on an entity: AnimDebug (if present), Locomotion, MovementState, CharacterController, and any child entity carrying an orbital_follow controller targeting this entity. Idempotent — missing components are skipped silently. Returns the count of despawned camera children so callers can verify cleanup.
Parameters
entityIdstring
Returns UninstallResult
"Player"
player.id()
tools/cutscene/pause
cutscene.pause() -> shared.ZmToolResult
Freeze the cutscene playing now. The clock stops and so does the scene under it — components stop updating, physics stops stepping, animation stops advancing — which is what keeps one shot on screen for as many captures and edits as the work needs. cutscene.resume starts both again.
Returns shared.ZmToolResult
tools/cutscene/play
cutscene.play(source: string, paused?: boolean) -> shared.ZmToolResult
Play a cutscene now, from the module its definition is returned from — the same call a scene entrypoint or a CutsceneTrigger makes, so what you see here is what a player sees. Starting one while another is playing ends that one first.
Parameters
sourcestringpausedboolean(optional)
Returns shared.ZmToolResult
"mygame.scenes.intro"
"mygame.scenes.intro", true
tools/cutscene/rate
cutscene.rate(r: number) -> shared.ZmToolResult
Set the playback rate of the cutscene playing now. 1 is real time, 0.25 slow motion, 2 double speed; a negative rate runs it backwards. The cutscene itself is untouched — only how fast its clock moves.
Parameters
rnumber
Returns shared.ZmToolResult
0.25
-1
tools/cutscene/resume
cutscene.resume() -> shared.ZmToolResult
Start the cutscene playing now again after cutscene.pause.
Returns shared.ZmToolResult
tools/cutscene/seek
cutscene.seek(t: number) -> shared.ZmToolResult
Move the clock of the cutscene playing now to an exact moment. The frame it lands on is the frame that time would have produced — the camera, the fade and the subtitle are all re-derived — so a shot can be checked without watching the cutscene up to it.
Parameters
tnumber
Returns shared.ZmToolResult
6.5
tools/cutscene/skip
cutscene.skip() -> shared.ZmToolResult
Skip the cutscene playing now to its closing fade — what a player pressing the skip key does. Cues between here and the end still fire, so a cutscene that opens a door on its way out opens it.
Returns shared.ZmToolResult
tools/cutscene/status
cutscene.status() -> shared.ZmToolResult
Report the cutscene playing now — its name, where its clock is, which shot is on screen, and whether it is paused or running at an unusual rate. Reads the cutscene whatever started it, so this answers about the one a scene is playing, not a private copy.
Returns shared.ZmToolResult
tools/cutscene/stop
cutscene.stop() -> shared.ZmToolResult
End the cutscene playing now. The camera, the overlay and the player's controls all go back the way they were, the same as when a cutscene reaches its end on its own.
Returns shared.ZmToolResult
tools/debug/inspect
debug.inspect(target: string | { any }, opts?: InspectOpts) -> InspectResult
Inspect entity component state for every entity matching the target. A name resolves to every matching entity; an id to itself; arrays and entity proxies work too. Each match reports its script components (serialized public data) and native ECS components, selected by opts.include.
Parameters
targetstring | { any }optsInspectOpts(optional)
Returns InspectResult
'player'
'lamp', { include = "ecs" }
'enemy_*', { types = { "Health" }, limit = 5 }
tools/debug/playSession
debug.playSession(enabled?: boolean) -> { playSession: boolean }
Show or hide the debug overlay in play mode. Edit mode always shows the overlay; play mode shows it only while this session is on — the way to watch bones, colliders, or bounds on a running, animating scene. The flag lives in memory only and is never saved, so debug visibility can never be permanently toggled on by accident; it resets on engine restart.
Parameters
enabledboolean(optional)
Returns { playSession: boolean }
true
false
tools/debug/problems
debug.problems(opts?: ProblemsOpts) -> ProblemsResult
Return the errors and warnings the engine has logged since your last call — the non-crashing failures (contract violations, component awake/update throws, auto-disabled components, unresolved requires, bridged renderer/asset errors) that otherwise never surface in execute results. Each call advances a read-cursor, so repeated calls report only what is new, oldest first (the root-cause error surfaces ahead of the cascade it triggered). When something you built does not behave, call this FIRST — the engine has usually already logged why.
Parameters
optsProblemsOpts(optional)
Returns ProblemsResult
{ level = "error", limit = 20 }
{ all = true }
tools/debug/scope
debug.scope(target?: (string | { any })) -> { scoped: { string }, count: number }
Limit every enabled debug category to a set of entities, so the overlay is focused on specific objects. Call with no argument (or an empty list) to clear the focus and draw for every entity again.
Parameters
target(string | { any })(optional)
Returns { scoped: { string }, count: number }
'player'
{ 'player', 'enemy' }
tools/debug/set
debug.set(category: string, enabled?: boolean) -> { [string]: boolean }
Show or hide a debug-visualization overlay category. The overlay is ambient editor tooling drawn for the whole scene; this flips which categories draw. A category is settable before the package that draws it has registered — the state is kept and applies when it arrives — so a script that configures overlays does not depend on how far along the session is. debug.state lists what is set.
Parameters
categorystringenabledboolean(optional)
Returns { [string]: boolean }
'colliders'
'bones', true
'all', false
tools/debug/state
debug.state() -> DebugState
Read the current debug-visualization state: which categories draw, the entity scope, whether the play-mode session is on, and the engine mode. categories covers every category the session knows — those a package has registered to draw, plus any set before its provider arrived — so what this lists is what debug.set accepts.
Returns DebugState
tools/detail/resident
detail.resident(axis?: string) -> { [string]: any }
Report what a world has resident of its detail right now. Covers terrain's LOD cut, a voxel world's chunk meshes, spatial streaming's cell store and mesh LOD's level selection, each read from the system's own state rather than from the component fields that asked for it. Every voxel chunk carries what became of its mesh build, so a world that stands with the right block count and draws nothing reads as what it is. The same reading is served at /zero/runtime/observations/streaming.
Parameters
axisstring(optional)
Returns { [string]: any }
"voxel"
tools/detail/whyNotDrawn
detail.whyNotDrawn(subject: any) -> { [string]: any }
Answer why a piece of a world's detail is not on screen. The reason is the nearest cause from a closed set — drawn, notManaged, notBuilt, buildQueued, buildFailed, builtEmpty, emittedNothing, outsideRadius, evicted, coarserLevel, finerLevel, noSuchLevel, hidden — so it names the thing to change rather than a consequence of it, and detail carries the engine's own message when a mesh build was dropped. Reads the four detail systems' state rather than the components' fields.
Parameters
subjectany(optional)
Returns { [string]: any }
"Vox"
tools/diagnostics/errors
diagnostics.errors(entityFilter?: (string | EntityFilterTable)) -> { EntityErrors }
Get all component/entity errors in the scene. With no argument, returns errors for all entities (enumerated via vfs.list("/runtime/layers/main/entities")). With an entity name or ID, returns errors for that entity only. Each entity's errors are read from /runtime/layers/main/entities/<name>/errors — values of "(none)" or "" are treated as no errors. Read-only — does not modify anything.
Parameters
entityFilter(string | EntityFilterTable)(optional)
Returns { EntityErrors }
"lamp"
tools/diagnostics/summary
diagnostics.summary() -> SceneSummary
Get a summary of the current scene. Enumerates entities via vfs.list("/runtime/layers/main/entities"), counts each entity's components by listing /runtime/layers/main/entities/<name>/components, counts entities with non-empty error files, and reports Camera component instances separately. Also surfaces rendering culling mode + stats via __rendering.getCullingMode / __rendering.getCullStats when available — GPU culling has no readback path so its stats slot becomes a notice string instead. Read-only — does not modify anything.
Returns SceneSummary
tools/diagnostics/validate
diagnostics.validate() -> ValidationReport
Validate scene integrity: check for broken materials, missing model assets, and entity errors. Enumerates entities via vfs.list("/runtime/layers/main/entities"), reads each entity's errors file, and parses messages of the form material '...' does not exist / Material:...not found ...'...' / mesh '...' not found / Model:...not found ...'...' to surface broken references. Read-only — does not modify anything.
Returns ValidationReport
tools/editor/observe
editor.observe() -> { [string]: any }
What each editor action committed, and why it committed less than it was asked for. lastDrag is the most recent gizmo drag: the handle it held, the pivot it reconstructed against, what the pointer asked for and what the drag applied after snapping, and — per entity — the transform before, the transform handed to the engine, and the transform the engine HOLDS, read back from the engine. lastGrab is a press that landed on a handle and began no drag, with the reason. lastDelete / lastDuplicate carry the ids removed and created and the ones that refused; lastCommand tells a missing command from a disabled one from a broken predicate from a body that raised; lastSelect carries the ids before and after with the difference taken both ways. Every record names an outcome and, when that is not a clean commit, one reason from the closed set reasons enumerates. Answers in edit mode as well as play mode.
Returns { [string]: any }
tools/entityOps/addComponents
entityOps.addComponents(targets: Targets, components: Components, opts?: AddComponentsOpts) -> { AddResult }
Attach component(s) to one or many entities in a single call. Targets are entity NAMES or ids (arrays and scene.find records work too), or a scene.find QUERY resolved for you — { name = { "wheel" } }, { component = { "Light" } }, { under = { "car" } } — so you operate on a selection without looking it up first. Components are given as a name, an array of names, or a { Name = dataTable } map carrying each component's setup data — names accept the leaf ("Physics"), the full identity ("@builtin::components.Physics"), a VFS path (resolved to its identity), or a case-insensitive partial resolved to the one component it can mean, all before anything mutates; a name matching nothing, or several components, is an error naming what it could not settle rather than a half-applied call. create = true spawns a fresh root entity for any string target that matches nothing, so one call can both create the entity and give it its setup. Returns one { id, name, added, created? } record per entity.
Parameters
targetsTargetscomponentsComponentsoptsAddComponentsOpts(optional)
Returns { AddResult }
"crate", { Physics = { kind = "dynamic" } }
{ "crate_1", "crate_2" }, { "Physics", "Audio" }
"spinner", { Rotator = { speed = 2 } }, { create = true }
tools/entityOps/component
entityOps.component(componentType: string, opts?: ComponentInspectOpts) -> ComponentInfo
Inspect a component TYPE so you set fields that exist instead of guessing. Resolves the component by name / identity / VFS path and reports its public FIELDS (each with name, type, and current value), its callable METHODS, its lifecycle HOOKS (awake / update / onDestroy / …), asset-ref fields with their AssetRef type, and a one-line description. Pass on (an entity name or id that carries the component) and every field's value is that entity's LIVE value, read off its component proxy — a vector / struct / list as its plain table, a reference by the identity of what it points at (an asset as a { __ref, identity, name, type } envelope, an entityRef as the entity id). Omit on and each value is the DECLARED DEFAULT as written in the component's source. Use this before entityOps.addComponents / entityOps.spawn to learn the exact field names and value shapes.
Parameters
componentTypestringoptsComponentInspectOpts(optional)
Returns ComponentInfo
"Camera"
"Model", { on = "my_prop" }
"@builtin::components.Light"
tools/entityOps/duplicate
entityOps.duplicate(targets: Targets, opts?: DuplicateOpts) -> { shared.TransformState }
Copy entities that already exist — a duplicate of a prop, or a numbered run of them spread along an axis. Targets resolve by NAME or id, an array of them, scene.find records, or a QUERY resolved for you ({ name = { "crate" } }, { component = { "Light" } }, { under = { "room" } }), so a whole selection is copied without looking its ids up first. count makes that many copies of EACH source (default 1). offset is a {x,y,z} world-space step that ACCUMULATES: copy 1 sits one offset from the source, copy 2 sits two, and so on — every copy is placed relative to the SOURCE, not to the copy before it. name names each copy and a %d in it becomes the copy's 1-based index ("crate_%d" → crate_1, crate_2); without it copies are named after their source. Copies carry the source's render layer. An unrecognised option key is an error naming the accepted set, and a count below 1 is an error, so a mistyped call never reads back as a successful no-op. Returns the full { id, name, position, localPosition, rotation, localRotation, eulerAngles, scale, parent } state of every copy, in creation order.
Parameters
targetsTargetsoptsDuplicateOpts(optional)
Returns { shared.TransformState }
"crate"
"crate", { count = 5, offset = { 2, 0, 0 } }
"pillar", { count = 4, offset = { 0, 3, 0 }, name = "pillar_%d" }
{ name = { "tree" } }, { offset = { 10, 0, 0 } }
tools/entityOps/fromAsset
entityOps.fromAsset(source: string, opts?: FromAssetOpts) -> FromAssetResult
Instantiate an existing asset into the scene. Resolution-first: pass a name / path / identity STRING and the tool resolves it (limit the search with type to avoid name clashes), dispatches by the resolved category, and REPORTS the canonical asset it hit so you learn what it resolved to. A bundle or avatar instantiates via its own :instantiate() — link (default true) keeps the live Asset-component link so the source drives the hierarchy, link = false bakes the hierarchy into the scene. A mesh becomes a renderable (Model + mesh Collider). A material becomes a sphere carrying it. A texture becomes a plane sampling it. Anything else goes through the uniform scene-instantiation contract — a gaussianSplat capture becomes an entity drawing that cloud, and every other type whose assetType defines the hook works the same way, else a legible error. parent (an entity ref, id, or name) spawns the instance under that entity — it lands on the parent, and position then places it in the parent's space; an unresolvable parent fails the call before anything spawns. position / rotation / scale place it; synced replicates gameplay edits to peers.
Parameters
sourcestringoptsFromAssetOpts(optional)
Returns FromAssetResult
"@builtin::avatars.humanoid"
"oak_chest", { type = "bundle", position = {2, 0, 0} }
"truck_model", { type = "bundle", parent = "truck_rig" }
"gold", { type = "material" }
"/zero/source/generated/meshes/a_statue.bundle", { link = false }
tools/entityOps/modify
entityOps.modify(targets: Targets, ops: ModifyOps) -> { ModifyResult }
Change entity state — one tool for delete / enable / disable / internal / temporary / reparent / rename, over one or many entities. Targets are entity NAMES or ids (arrays and scene.find records work too), or a scene.find QUERY resolved for you — { name = { "wheel" } }, { component = { "Light" } }, { under = { "car" } } — so you operate on a selection without looking it up first. Boolean ops take true, false, or "toggle". delete removes the entity and its whole subtree and wins over every other op. enabled drives the entity's active state, internal marks it engine plumbing — taken out of the default entity listings and the inspector default view, while it keeps rendering, temporary marks it as not-persisted. parent (a name or id) reparents; unparent = true makes it a scene root; keepWorldTransform = false keeps the local transform instead of the world pose when reparenting. rename sets a new name (single target only). components sets field values on components the entity ALREADY carries — { Camera = { fov = 60 }, Light = { intensity = 2 } }, the same shape spawn takes, so creating with a value and changing it later are written alike; component names resolve the same forgiving way addComponents resolves them, and an entity that does not carry one named is an error rather than a silent skip. An unrecognised key is an error naming the accepted set, so a typo fails the call rather than reporting an empty applied. Returns one { id, name, applied } record per entity.
Parameters
targetsTargetsopsModifyOps
Returns { ModifyResult }
"debug_probe", { internal = true }
{ "probe_a", "probe_b" }, { delete = true }
"old_character", { enabled = "toggle" }
"hat", { parent = "player_head" }
"lamp", { components = { Light = { intensity = 4 } } }
{ component = { "Light" } }, { components = { Light = { intensity = 0 } } }
tools/entityOps/removeComponents
entityOps.removeComponents(targets: Targets, components: Components) -> { RemoveResult }
Detach component(s) from one or many entities in a single call — the counterpart to entityOps.addComponents. Targets resolve by NAME or id, an array of them, scene.find records, or a QUERY resolved for you ({ component = { "Light" } }, { name = { "lamp" } }, { under = { "room" } }), so you can strip a component from everything carrying it without naming a single entity. Components are given as a name or an array of names, and each name matches loosely: the leaf ("Light"), the full identity ("@builtin::components.Light"), a VFS path, or a case-insensitive partial ("light") resolved to the one component it can mean — the response reports the CANONICAL name it landed on, so a half-remembered name both works and teaches you the real one. A name matching several components is an error listing the candidates rather than a guess, and a name matching none is an error too, so nothing is silently skipped. Every entity comes back as { id, name, removed, absent }: removed lists what it actually carried and lost, absent lists what it never had — so a call that changed nothing is visible instead of reading like a success.
Parameters
targetsTargetscomponentsComponents
Returns { RemoveResult }
"lamp", "Light"
{ "crate_1", "crate_2" }, { "Physics", "Audio" }
{ component = { "Light" } }, "Light"
"lamp", "light"
tools/entityOps/roots
entityOps.roots(targets: Targets) -> { RootHit }
Find the root ancestor of one or many entities — walks each up its parent chain to the top-level entity. Targets are entity names or ids (a single one, an array, or scene.find records), or a scene.find QUERY resolved for you — { name = { "wheel" } }, { component = { "Light" } }, { under = { "car" } }. Returns one record per target with its resolved root id + name and the hop distance; a target that is already a root reports itself with depth 0.
Parameters
targetsTargets
Returns { RootHit }
"player_hand"
{ "wheel_fl", "wheel_fr" }
tools/entityOps/spawn
entityOps.spawn(components: SpawnComponents | SpawnBatch, opts?: SpawnOpts) -> SpawnResult | { SpawnResult }
Spawn entities from a template — you name the components they carry and their field values, plus a transform. Spawn MANY in ONE call by passing a { defaults, items } list: defaults holds what every entity shares and each items entry overrides only what differs, so a six-walled room is one call rather than six. Per-item keys win over the matching defaults key, and components merges per COMPONENT — an item naming Model replaces only that entry and keeps the shared Collider. Every item is validated before any entity is created, so a typo fails the whole call instead of leaving a half-built scene. Or pass a single { [ComponentName] = { field = value } } map plus opts to spawn ONE entity. Each component is an AUTHORED component (leaf name like "Model", a full identity, or a VFS path) added with its data so it serializes — native ECS components are intentionally not exposed, so you never author something that renders at runtime but vanishes on save. position places it; rotation takes a {x,y,z,w} (or {x=,y=,z=,w=}) quaternion or {pitch,yaw,roll} (or {pitch=,yaw=,roll=}) euler degrees; scale a number or {x,y,z}; name defaults to the first component's name. A spawn is recorded into the scene as it is made, so it is there again the next time the scene loads; temporary leaves an entity out of that record — nothing is written for it as it spawns and no save carries it — which is the spelling for content a build reproduces for itself. internal, synced (replicate gameplay edits to peers), and attributes carry the rest of the lifecycle. An unrecognised key is an error naming the accepted set — in opts, at the top level, in defaults, and in an item alike. Pass an empty components map to spawn a bare entity. Returns the spawned id, name, and component leaf names — one record for the single form, an array in item order for the list form.
Parameters
componentsSpawnComponents | SpawnBatchoptsSpawnOpts(optional)
Returns SpawnResult | { SpawnResult }
{ Model = { model = "cube" }, BoxCollider = {} }, { position = {0, 1, 0} }
{ Light = { kind = "point", intensity = 5 } }, { name = "lamp", position = {0, 3, 0} }
{ Text3D = { content = "Hello", fontSize = 48 } }, { position = {0, 2, 0}, rotation = {0, 45, 0} }
{ defaults = { components = { Model = { model = "cube" }, BoxCollider = {} }, scale = { 4, 3, 0.2 } }, items = { { name = "wall_n", position = { 0, 1.5, -2 } }, { name = "wall_s", position = { 0, 1.5, 2 } }, { name = "wall_e", position = { 2, 1.5, 0 }, rotation = { 0, 90, 0 } }, { name = "wall_w", position = { -2, 1.5, 0 }, rotation = { 0, 90, 0 } } } }
{ defaults = { components = { Light = { kind = "point", intensity = 4 } } }, items = { { name = "lamp_a", position = { -3, 3, 0 } }, { name = "lamp_b", position = { 3, 3, 0 }, components = { Light = { kind = "point", intensity = 9 } } } } }
tools/entityOps/swap
entityOps.swap(target: string | EntityRef, replacement: string | EntityRef, opts?: SwapOpts) -> SwapPlan
Replace one entity with another entity that already exists in the scene. The replacement is MOVED into the target's slot — same parent, local position, and rotation; the target's scale unless keepScale = false; the target's name unless keepName = false — and the target (with its whole subtree) is removed. Both arguments are entity names, ids, or EntityRef proxies. dryRun = true reports exactly what would move and what would be removed, without mutating.
Parameters
targetstring | EntityRefreplacementstring | EntityRefoptsSwapOpts(optional)
Returns SwapPlan
"DefaultBody", "my_knight"
"placeholder_car", "sports_car", { keepName = false }
"DefaultBody", "my_knight", { dryRun = true }
tools/entityOps/transform
entityOps.transform(targets: Targets | { TransformTarget }, ops?: TransformOps) -> { shared.TransformState }
Move, rotate and scale entities that already exist — set an absolute position / rotation / scale, apply a relative offset / rotation / scale multiplier, or snap to a grid, over one or many entities. Targets resolve by NAME or id, an array of them, scene.find records, or a QUERY resolved for you ({ name = { "wall" } }, { component = { "Light" } }, { under = { "building" } }) so you can move things without looking their ids up first. Absolute ops: position and rotation are world-space, localPosition and localRotation are parent-relative, scale is a number or {x,y,z}. Relative ops: offset adds a position delta, rotate applies a rotation on top of the current one, scaleBy multiplies the current scale; space ("world" default, or "local") governs those relative ops. snap quantizes the resulting position to grid steps. Rotations take a {x,y,z,w} quaternion or {pitch,yaw,roll} euler degrees, the same forms entityOps.spawn accepts. Absolute and relative forms of one channel are exclusive and passing both is an error, and an unrecognised option key is an error naming the accepted set, so a mistyped key never reads back as a successful no-op. Pass a LIST of { target = ..., <ops> } records to give each entity its own values in a single call; a bad entry there fails the whole call before any entity moves. Called with NO ops it is a pure read. Always returns the resulting { id, name, position, localPosition, rotation, localRotation, eulerAngles, scale, parent } per entity, so the call is both the write and the read.
Parameters
targetsTargets | { TransformTarget }opsTransformOps(optional)
Returns { shared.TransformState }
"lamp", { position = { 0, 3, 0 } }
{ "wall_n", "wall_s" }, { offset = { 0, 2, 0 } }
{ name = { "wall" } }, { offset = { 0, 2, 0 } }
"crate", { rotation = { 0, 45, 0 }, scale = 2 }
"prop", { offset = { 1, 0, 0 }, space = "local", snap = 0.5 }
"lamp"
tools/gui/bounds
gui.bounds(id?: ElementId) -> (ElementRect | { [string]: ElementRect })?
Read a UI element's on-screen layout rect by id - { x, y, w, h } plus content bounds/centres, in logical points. Omit the id for a map of every recorded element's rect. This is the space gui.clickAt and anchors use.
Parameters
idElementId(optional)
Returns (ElementRect | { [string]: ElementRect })?
"glass/vertical@8"
tools/gui/captureElement
gui.captureElement(screen?: (string | CaptureElementArgs), element?: (string | { string }), opts?: CaptureElementOpts) -> CaptureElementResult
Render one UI element — or several composited by their on-screen positions — to a texture and save it as a JPG under /source/tmp, so you can READ it to see exactly how those elements render. Each element (by its elementTree id) is rendered into a right-sized render texture the same way a camera renders into a render texture; the result is read back, encoded JPG, written, and the texture destroyed (nothing stays GPU-resident). Pass one id for a single element, or a list of ids to capture several LAYERS together — the image covers their union bounds and each element is drawn at its real relative offset, so a panel and an icon sitting on different screens composite into one picture. Ids may span different screens; each element's screen is taken from its id. Find ids with gui.elementTree. The screen must have rendered at least once (so the element has a measured size).
Parameters
screen(string | CaptureElementArgs)(optional)element(string | { string })(optional)optsCaptureElementOpts(optional)
Returns CaptureElementResult
"glass", "glass/vertical@10"
tools/gui/click
gui.click(callbackId: string, value?: any)
Simulate a widget click by callback ID. Forwards to ui.click(callbackId, value), which dispatches the registered onCallback handler associated with callbackId.
Parameters
callbackIdstringvalueany(optional)
"reset_btn"
"volume_slider", 0.5
tools/gui/clickAt
gui.clickAt(x: (number | ClickAtArgs), y?: number, opts?: { button: (number | string)? }) -> ClickAtResult
Click at a logical screen position - a REAL pointer down+up. Coordinates are logical UI points (the space gui.size / gui.bounds / anchors use), not capture pixels.
Parameters
x(number | ClickAtArgs)ynumber(optional)opts{ button: (number | string)? }(optional)
Returns ClickAtResult
640, 360
tools/gui/clickElement
gui.clickElement(id: (string | ClickElementArgs), opts?: { button: (number | string)?, duration: number? }) -> ClickElementResult
Click a UI element by its elementTree / layout id: a REAL pointer down+up at the element's centre. Works on any visible widget (no author-assigned callback needed) - the complement to gui.click, which needs a callback id. Find ids with gui.elementTree.
Parameters
id(string | ClickElementArgs)opts{ button: (number | string)?, duration: number? }(optional)
Returns ClickElementResult
"glass/vertical@10"
tools/gui/dragElement
gui.dragElement(from: (string | DragElementArgs), to?: string, opts?: { button: (number | string)? }) -> DragElementResult
Drag between two UI elements by their elementTree ids: press at the first element's centre, move to the second, release.
Parameters
from(string | DragElementArgs)tostring(optional)opts{ button: (number | string)? }(optional)
Returns DragElementResult
"slider_nub", "slider_end"
tools/gui/elementTree
gui.elementTree(screen: string | { screen: string })
List a screen's rendered element hierarchy with each element's on-screen layout rect. Each node is { id, type, bounds, children } where bounds is { x, y, w, h } in logical points, present once the element has been measured. id is the id set on the node when the author gave it one, otherwise <screen>/<type>@<path>. Pass an id (or several) to gui.captureElement to screenshot that panel or widget.
Parameters
screenstring | { screen: string }
"glass"
tools/gui/focus
gui.focus(id?: ElementId) -> FocusResult
Focus a widget by id so it takes keyboard input; call with no id (or "") to blur the current focus. Forwards to ui.focus / ui.blur.
Parameters
idElementId(optional)
Returns FocusResult
"search_input"
tools/gui/fonts
gui.fonts(system?: (boolean | { system: boolean })) -> { any }
List every font family a style.fontFamily can select, with the aliases that also select it, the concrete face in each weight/style slot, and whether it came from the host OS. Read from the registry the UI text renderer resolves a family through, so a family listed here is one a label renders in. A fontFamily naming something absent from this list paints in the default proportional face and is reported as unknown-font-family through gui validate. Forwards to ui.listFonts().
Parameters
system(boolean | { system: boolean })(optional)
Returns { any }
true
tools/gui/getTree
gui.getTree(name: string)
Get the widget tree of a screen (for reading state). Forwards to ui.getScreenTree(name). The returned tree mirrors the registered widget list and is suitable for inspection / diff-based test assertions.
Parameters
namestring
"hud"
tools/gui/hide
gui.hide(name: string) -> shared.ScreenVisibility
Hide a screen and report the visibility it now carries. Forwards to ui.hideScreen(name) — the registered screen remains in the registry and can be re-shown via gui.show. Raises when no screen answers to name, naming the ones that do.
Parameters
namestring
Returns shared.ScreenVisibility
"hud"
tools/gui/hoverElement
gui.hoverElement(id: ElementId) -> HoverElementResult
Move the pointer to a UI element's centre by its elementTree id, so the element's hover state paints. The cursor stays put - follow with gui.captureElement to see the hovered look.
Parameters
idElementId
Returns HoverElementResult
"glass/vertical@9"
tools/gui/panel
gui.panel(name: string, widgets: { any }, opts?: { layer: number?, visible: boolean? }) -> shared.ScreenVisibility
Create a UI screen from a widget list and report the visibility it now carries. Registers the screen via ui.registerScreen at opts.layer, then leaves it shown — or hidden when opts.visible is false, so a screen can be built ahead of the moment it appears. Each widget is { type, text?, id?, value?, color?, children?, onCallback?, ... }.
Parameters
namestringwidgets{ any }opts{ layer: number?, visible: boolean? }(optional)
Returns shared.ScreenVisibility
"hud", { { type = "label", text = "Score: 0", id = "score" }, { type = "button", text = "Reset", onCallback = "reset_btn" } }
tools/gui/screens
gui.screens()
List every registered screen with { name, visible, layer, hasRoot } - the top-level map of what UI exists right now. Forwards to ui.listScreens.
tools/gui/scroll
gui.scroll(dx: number, dy: number)
Simulate scroll input. Forwards to ui.scroll(dx, dy).
Parameters
dxnumberdynumber
0, -10
tools/gui/scrollElement
gui.scrollElement(id: (string | ScrollElementArgs), offsetY?: number) -> ScrollElementResult
Scroll a scrollArea to a vertical offset (pixels from the top) by its elementTree id — the reliable, element-targeted scroll. Prefer this over scroll (a mouse-wheel simulation) when you know which area to move.
Parameters
id(string | ScrollElementArgs)offsetYnumber(optional)
Returns ScrollElementResult
"app/scrollArea@1.0.1", 240
tools/gui/setTheme
gui.setTheme(name: string)
Set the active UI theme. Forwards to ui.setTheme(name) — subsequent screen draws use the new theme.
Parameters
namestring
"dark"
tools/gui/show
gui.show(name: string) -> shared.ScreenVisibility
Show a screen and report the visibility it now carries. Forwards to ui.showScreen(name). The screen must already be registered (via gui.panel or ui.registerScreen); a name no screen answers to raises, naming the ones that do.
Parameters
namestring
Returns shared.ScreenVisibility
"hud"
tools/gui/size
gui.size()
Return the UI coordinate space { width, height } in logical points - the space gui.bounds, anchors, and gui.clickAt use (not a capture's pixel size). Forwards to ui.screenSize.
tools/gui/state
gui.state(id: ElementId) -> WidgetResponse?
Read a widget's live interaction snapshot by id: { clicked, hovered, focused, changed, value }. Assert what a UI is doing after you drive it. Forwards to ui.response.
Parameters
idElementId
Returns WidgetResponse?
"volume_slider"
tools/gui/themes
gui.themes() -> { string }
List registered themes. Forwards to ui.listThemes().
Returns { string }
tools/gui/toggle
gui.toggle(name: string) -> shared.ScreenVisibility
Flip a screen's visibility and report the visibility it now carries. Reads the screen's current state from ui.listScreens(), then calls ui.hideScreen or ui.showScreen. Raises when no screen answers to name, naming the ones that do.
Parameters
namestring
Returns shared.ScreenVisibility
"hud"
tools/gui/update
gui.update(screenName: string, widgetId: string, props: { [string]: any })
Update a specific widget inside a screen by widget ID. Reads the current widget tree via ui.getScreenTree(screenName), walks it to find the widget with the matching id, copies every key from props onto the matched node, and writes the modified tree back via ui.updateScreen. If the screen does not exist, prints a warning and returns without erroring.
Parameters
screenNamestringwidgetIdstringprops{ [string]: any }
'hud', 'score', { text = 'Score: 500' }
tools/gui/validate
gui.validate(screen?: (string | { screen: string })) -> any
Return the diagnostics from the last render of a screen (or all screens when no name is given): unknown widget types, bad props, decode/build errors. First stop when a screen renders wrong. Forwards to ui.lastValidation.
Parameters
screen(string | { screen: string })(optional)
Returns any
"glass"
tools/gui/widgetProps
gui.widgetProps(typeName: string) -> table?
Get property definitions for a widget type. Forwards to ui.getWidgetProps(typeName) — the returned table describes every property a widget of this type accepts (name, type, default).
Parameters
typeNamestring
Returns table?
"button"
tools/gui/widgetTypes
gui.widgetTypes() -> { string }
List available widget types. Forwards to ui.getWidgetTypes().
Returns { string }
tools/ik/bones
ik.bones(of: string | EntityRef, filter?: string) -> BonesResult
List a rigged body's bones with their world positions and parents. Resolves the body itself or the first descendant carrying a rigged skeleton, so it can be called on a character root without knowing where inside the spawned hierarchy the skeleton lives. Also returns the canonical role map when the rig has one, which is what IKLimb.role and IKRig resolve through; a rig with no retarget profile returns an empty map, and its chains have to be named bone by bone.
Parameters
ofstring | EntityReffilterstring(optional)
Returns BonesResult
"hero"
"hero", "hand"
player.avatar.id()
tools/ik/reach
ik.reach(of: string | EntityRef, chain: string, target?: (string | EntityRef)) -> ReachResult
Report a chain's reachable band and, when a target is given, whether that target falls inside it. A limb reaches as far as its bones are long and no further: beyond that the chain extends straight toward the target and stops, which is correct and indistinguishable from a broken solver. Under minReach (the fold limit) it cannot fold tightly enough. Everything is reported in world units, so the rig's model space — where a 0.61 m arm reports a reach near 61 — does not have to be reasoned about.
Parameters
ofstring | EntityRefchainstringtarget(string | EntityRef)(optional)
Returns ReachResult
"hero", "rightarm"
"hero", "rightarm", "doorknob"
"crane", "boom_base..boom_tip", "hook"
tools/ik/state
ik.state(of: string | EntityRef) -> StateResult
Gather every IK component on an entity and report each one's state, so "is anything solving at all" is a single call. Each entry carries a status in words: a body spawned from a bundle takes a few frames to arrive, and a paused engine never advances those frames, so "not ready" on its own does not distinguish a wait that clears from one that never will. Where a component reports a distance to its target, that number is the residual after solving — small means the solve arrived, and it is not the distance to compare against a limb's reach (use ik.reach).
Parameters
ofstring | EntityRef
Returns StateResult
"hero"
player.avatar.id()
tools/importers/await
importers.await(path: string, timeoutSecs?: number) -> AwaitResult
Wait until the import job for path reaches a terminal state — imported, failed, skipped, or unclaimed — or the timeout elapses. Returns the job record (still running on timeout). When no job exists for the path at all, returns the gate explanation instead, so the call always answers what happened.
Parameters
pathstringtimeoutSecsnumber(optional)
Returns AwaitResult
"/zero/source/models/gun.fbx"
"/zero/source/models/gun.fbx", 60
tools/importers/cancel
importers.cancel(path?: string) -> CancelResult
Cancel a queued source before it imports, or every queued source at once. Pass a path to cancel just that one; omit it to cancel ALL queued sources (an accidental 1k-file drop). Sources already importing are left to finish. A cancelled source settles as a cancelled job, visible in importers.status.
Parameters
pathstring(optional)
Returns CancelResult
"/zero/source/models/gun.fbx"
tools/importers/explain
importers.explain(path: string) -> ExplainResult
Explain what the importer system would do with path right now, without importing: whether the file exists and reads, which importers claim it, and which gate (if any) blocks an import. verdict is would-import or blocked; when blocked, reason names the gate — missing (no such file), unreadable, no-claimant (no importer claims the extension/content), already-in-own-bundle (the source already lives inside its produced asset), or unchanged-content (the bytes match the last import; output points at the existing asset).
Parameters
pathstring
Returns ExplainResult
"/zero/source/models/gun.fbx"
tools/importers/imported
importers.imported() -> ImportedResult
List every imported asset in the world with its source and the importer that produced it. Each row: { asset, source = { guid, path }, importer (guid), importerName, iteration, at }, newest import first. The stored importer link is the guid; importerName resolves it for display.
Returns ImportedResult
use_tool { toolbox = "importers", tool = "imported" }
tools/importers/list
importers.list() -> { count: number, importers: { { name: string, identity: string } } }
List every registered importer with its name and identity. Importers are .importer/ assets, each a canImport(path, bytes) + import(ctx) pair; a written source file is offered to each claimant in registration order and the first to produce output wins.
Returns { count: number, importers: { { name: string, identity: string } } }
tools/importers/queue
importers.queue() -> QueueResult
The live import backlog: queued sources first (still waiting behind the bounded concurrency, in FIFO order) then the ones importing right now. A bulk drop of N files imports one at a time, so this is where you watch the queue drain. Cancel queued items with importers.cancel.
Returns QueueResult
tools/importers/run
importers.run(target: string | { string }, opts?: RunOpts) -> RunResult
Import/reimport one target, many targets, or a folder. A produced asset reimports in place (source resolved from its provenance); a loose source imports; a folder is scanned (recursive by default), its imported assets reimported and loose sources imported, filtered by opts.mode ("all" | "new" | "existing"). mode governs folder scanning; explicit path/array targets are always processed.
Parameters
targetstring | { string }optsRunOpts(optional)
Returns RunResult
"/zero/source/guns"
tools/importers/status
importers.status(opts?: { state: ImportJobState?, path: string?, limit: number? }) -> StatusSummary
List recent import jobs, newest first, with the live queue depth. Every dispatch is recorded: queued (waiting behind the bounded concurrency), running (import in flight), imported (done — output is the produced asset path), failed (the importer threw — error carries the message), unclaimed (every importer deferred), skipped (gated — reason is already-in-own-bundle, unchanged-content, no-claimant, or unreadable), and cancelled (a queued source cancelled before it ran). backlog is the exact queue depth ({ queued, running }), active lists the jobs importing right now, and totals tallies every recorded job by state — poll those three to watch a bulk drop drain. See the full backlog list with importers.queue; drop queued items with importers.cancel. Filter the jobs window with opts.state (one state), opts.path (substring match on the source path), and opts.limit (default 25).
Parameters
opts{ state: ImportJobState?, path: string?, limit: number? }(optional)
Returns StatusSummary
{ state = "failed" }
{ path = "weapons", limit = 5 }
tools/inputAuthor/catalog
inputAuthor.catalog() -> { any }
Every control the catalog knows, with what each is bound to on keyboard, gamepad and touch. Naming one of these in map or control gets all three filled in.
Returns { any }
tools/inputAuthor/check
inputAuthor.check(map: string) -> any
Read a .inputMap and report every problem across its controls — a missing device class, a malformed record, a button whose class is not an array. Reports all of them at once rather than stopping at the first, and activates nothing, so a scheme can be checked before a world runs it.
Parameters
mapstring
Returns any
"racer"
tools/inputAuthor/control
inputAuthor.control(mapPath: string, control: any) -> string
Add a control to an existing .inputMap, carrying keyboard, gamepad and touch. A catalog name arrives pre-filled; anything else needs all three classes from you.
Parameters
mapPathstringcontrolany(optional)
Returns string
"/zero/source/inputMaps/racer.inputMap", "jump"
mapPath, { name = "horn", label = "Horn", kind = "button", kbm = '{ B.key("KeyH") }', gamepad = '{ B.padButton("north") }', touch = '{ B.touchButton({ zone = "right-lower" }) }' }
tools/inputAuthor/map
inputAuthor.map(name: string, controls: { any }, opts?: MapOpts) -> any
Write a <name>.inputMap/ and every control it declares, each carrying keyboard, gamepad and touch.
Parameters
namestringcontrols{ any }optsMapOpts(optional)
Returns any
"racer", { "move", "brake", { name = "boost", from = "sprint", label = "Nitro" } }
"shooter", { "move", "look", "attack", "aim", "reload", "jump" }
"driving", { "move", "brake" }, { group = "vehicle", suppresses = { "player" } }
tools/inputSim/bindings
inputSim.bindings() -> { any }
Every control that is live right now: its name, the label a player sees, its kind, and which activated map contributed it.
Returns { any }
tools/inputSim/click
inputSim.click(opts?: ClickOpts)
Click a mouse button — press, optional hold, release — optionally moving the cursor to (x, y) first.
Parameters
optsClickOpts(optional)
{ x = 400, y = 300 }
{ button = 1 }
tools/inputSim/device
inputSim.device(class?: string) -> string
Read the active device class, or force one. With no argument this reports what the session currently looks like: "kbm", "touch" or "gamepad".
Parameters
classstring(optional)
Returns string
"touch"
"auto"
tools/inputSim/drag
inputSim.drag(dx: number, dy: number, steps?: number)
Drag across the on-screen look zone by (dx, dy) pixels, in steps increments so the per-frame deltas a look binding reads are real rather than one impossible jump.
Parameters
dxnumberdynumberstepsnumber(optional)
200, 0
tools/inputSim/fired
inputSim.fired(peek?: boolean) -> { any }
Every control that fired since this tool last ran, with how many times and on which device class. Reading CLEARS the record, so two calls around an action answer "did that input reach the game" without the previous test's results bleeding in.
Parameters
peekboolean(optional)
Returns { any }
true
tools/inputSim/held
inputSim.held() -> any
Everything the session is holding down right now: keys, mouse buttons, touch contacts, the on-screen stick, and connected pads with the buttons they hold. atRest is true when it holds nothing.
Returns any
tools/inputSim/key
inputSim.key(key: string, duration?: number)
Tap a key — down, optionally held for duration seconds, then up. Queues the simulated events, yields a frame so they drain, and ticks so subscribers fire before the call returns.
Parameters
keystringdurationnumber(optional)
"Space"
"KeyW", 2
tools/inputSim/keyDown
inputSim.keyDown(key: string)
Press a key and leave it HELD until keyUp releases it. A second keyDown on a held key produces no new press edge — pair every keyDown with a keyUp, or use key for a whole tap.
Parameters
keystring
"ShiftLeft"
tools/inputSim/keyUp
inputSim.keyUp(key: string)
Release a key. Only the release re-arms the next press edge.
Parameters
keystring
"ShiftLeft"
tools/inputSim/layout
inputSim.layout() -> any
What the touch overlay is drawing right now: each visible button with its label and centre, the stick and look zones, and whatever overflowed behind the fan.
Returns any
tools/inputSim/listMacros
inputSim.listMacros() -> { AssetRef<inputMacro> }
Enumerate every registered .inputMacro asset in the project. Thin wrapper over asset.list("inputMacro"). Returns an array of AssetRef handles suitable for direct passing to sim.macro.
Returns { AssetRef<inputMacro> }
tools/inputSim/lockPointer
inputSim.lockPointer(locked: boolean) -> boolean
Lock the pointer (cursor captured and hidden) or release it. A mouse-look binding is usually gated on the lock, so a look test that moves the cursor without locking first reads as no movement at all.
Parameters
lockedboolean
Returns boolean
true
tools/inputSim/macro
inputSim.macro(refOrEvents: MacroEventList | MacroEventsWrapper | AssetRef<inputMacro> | string)
Play a macro of timed input events. Accepts: - inline event list: { { t = 0, op = "keyDown", arg = "KeyW" }, ... } - AssetRef<inputMacro> — loaded via asset.resolve + vfs.read - identity / path string of an .inputMacro asset. Each event: { t, op, arg }. t is seconds since macro start (events are sorted on play). op is one of: keyDown, keyUp, mouseDown, mouseUp, mouseMove, scroll, lockPointer, tap, click. arg shape matches the op. No game-specific recipes — drive a player, drive a car, or drive a UI test through the same primitive. Author macros by hand or record them via sim.recordMacro and persist via sim.saveMacro.
Parameters
refOrEventsMacroEventList | MacroEventsWrapper | AssetRef<inputMacro> | string
{ { t = 0.0, op = "mouseDown", arg = 1 }, { t = 0.0, op = "keyDown", arg = "KeyW" }, { t = 0.5, op = "mouseMove", arg = { dx = 200, dy = 0 } }, { t = 1.5, op = "keyUp", arg = "KeyW" }, { t = 1.5, op = "mouseUp", arg = 1 } }
asset.ref("my_walkthrough", "inputMacro")
tools/inputSim/mouseDown
inputSim.mouseDown(button?: number)
Press a mouse button and leave it HELD until mouseUp.
Parameters
buttonnumber(optional)
1
tools/inputSim/mouseMove
inputSim.mouseMove(x: number, y: number)
Move the cursor to (x, y) in screen coordinates. The delta from the previous position is what a mouse-look binding reads, so two calls in a row produce a look movement.
Parameters
xnumberynumber
640, 360
tools/inputSim/mouseMoveBy
inputSim.mouseMoveBy(dx: number, dy: number)
Move the pointer by (dx, dy) pixels, the motion a mouse device reports. The same motion repeated keeps producing look movement, so a look axis can be held, or steered by a controller issuing a correction each tick.
Parameters
dxnumberdynumber
0, 140
tools/inputSim/mouseUp
inputSim.mouseUp(button?: number)
Release a mouse button.
Parameters
buttonnumber(optional)
1
tools/inputSim/pad
inputSim.pad(name?: string) -> number
Connect a simulated pad and return the slot it took. name is the device name the pad reports, which is what decides the button legends a prompt draws — pass an Xbox, PlayStation or Nintendo name to check a scheme's prompts on that family.
Parameters
namestring(optional)
Returns number
"Xbox Wireless Controller"
"Sony DualSense Wireless Controller"
tools/inputSim/padDown
inputSim.padDown(button: string, slot?: number)
Push a canonical pad button down and leave it held until padUp.
Parameters
buttonstringslotnumber(optional)
"right_trigger"
tools/inputSim/padOff
inputSim.padOff(slot?: number)
Disconnect the pad in slot. Anything it still held is released first, so a held action ends rather than sticking.
Parameters
slotnumber(optional)
0
tools/inputSim/padPress
inputSim.padPress(button: string, duration?: number, slot?: number)
Press a canonical pad button — down, optionally held, then up. Buttons are named by POSITION, not by the letter printed on them, so the same call works whatever pad is connected: south, east, west, north, left_shoulder, right_shoulder, left_trigger, right_trigger, left_stick, right_stick, select, start, guide, dpad_up, dpad_down, dpad_left, dpad_right.
Parameters
buttonstringdurationnumber(optional)slotnumber(optional)
"south"
"left_shoulder", 1.5
tools/inputSim/padStick
inputSim.padStick(stick: string, x: number, y: number, duration?: number, slot?: number)
Push a thumbstick to (x, y), each -1..1 with y screen-down positive — the same convention the on-screen stick reports, so a binding reads the same shape from either. Holds until pushed back to zero, or for duration seconds if you pass one.
Parameters
stickstringxnumberynumberdurationnumber(optional)slotnumber(optional)
"left", 0, -1
"right", 1, 0, 2
tools/inputSim/padTrigger
inputSim.padTrigger(trigger: string, value: number, duration?: number, slot?: number)
Squeeze a trigger to value, 0..1. Past half throw the trigger's digital button latches too, so a scheme binding either the analog travel or the button sees this.
Parameters
triggerstringvaluenumberdurationnumber(optional)slotnumber(optional)
"right", 1
tools/inputSim/padUp
inputSim.padUp(button: string, slot?: number)
Release a canonical pad button.
Parameters
buttonstringslotnumber(optional)
"right_trigger"
tools/inputSim/pinch
inputSim.pinch(amount: number, steps?: number)
Pinch two fingers together (negative) or spread them apart (positive) by amount pixels, over steps frames. What a zoom binding reads on a phone.
Parameters
amountnumberstepsnumber(optional)
150
tools/inputSim/recordMacro
inputSim.recordMacro(opts?: RecordMacroOpts) -> ({ MacroEvent }, number)
Capture real input over a window. Polls Zin.state.* every sampleInterval seconds (default 0.05); emits a timed event whenever a key, mouse button, mouse delta, or scroll changes. Returns the events array and the spawned task handle. Caller decides whether to play immediately, save to disk via sim.saveMacro, or both. Generic — works for any input-driven scenario (player walk, car drive, UI flow, build sequence). Stops automatically after duration seconds, or earlier if until_() returns true.
Parameters
optsRecordMacroOpts(optional)
Returns ({ MacroEvent }, number)
{ duration = 3.0 }
tools/inputSim/release
inputSim.release() -> any
Return the session to rest: lift every touch contact, drop the on-screen stick, release every key and mouse button that is down, and disconnect every connected pad.
Returns any
tools/inputSim/saveMacro
inputSim.saveMacro(name: string, events: { MacroEventRecord }) -> AssetRef<inputMacro>
Persist a recorded or hand-authored event list to a .inputMacro asset on disk at /zero/source/<name>.inputMacro/events.json. Returns the AssetRef of the saved macro for immediate playback.
Parameters
namestringevents{ MacroEventRecord }
Returns AssetRef<inputMacro>
"walk_demo", { { t = 0.0, op = "keyDown", arg = "KeyW" }, { t = 1.0, op = "keyUp", arg = "KeyW" } }
tools/inputSim/scroll
inputSim.scroll(dy: number)
Turn the mouse wheel by dy. Positive scrolls one way, negative the other; what that means is the binding's to decide.
Parameters
dynumber
-3
tools/inputSim/stick
inputSim.stick(x: number, y: number, duration?: number)
Push the on-screen movement stick to (x, y), each -1..1, with y screen-down positive — the same convention a pad stick reports, so a binding reads the same shape from either.
Parameters
xnumberynumberdurationnumber(optional)
0, -1 -- full forward, held
0, 0 -- let go
1, 0, 2 -- hold right for two seconds, then release
tools/inputSim/tapButton
inputSim.tapButton(label: string, duration?: number)
Tap the on-screen button labelled label — the text a player reads on it. The tap lands at the button's real centre, so it proves the control is reachable, not just that the action exists.
Parameters
labelstringdurationnumber(optional)
"Jump"
tools/inputSim/touch
inputSim.touch(x: number, y: number, duration?: number)
Tap the screen at (x, y) — contact, optional hold, lift. The contact id is chosen for you; use touchDown / touchMove / touchUp when you need to drive several fingers at once.
Parameters
xnumberynumberdurationnumber(optional)
200, 600
200, 600, 0.5
tools/inputSim/touchDown
inputSim.touchDown(x: number, y: number, pressure?: number) -> number
Put a finger down at (x, y) and leave it there. Returns the contact id touchMove and touchUp take, so several fingers can be driven at once — a two-finger pinch is two of these.
Parameters
xnumberynumberpressurenumber(optional)
Returns number
200, 600
tools/inputSim/touchMove
inputSim.touchMove(id: number, x: number, y: number, pressure?: number)
Move the contact id to (x, y). The per-frame delta is what a drag-look binding reads, so a look test is several of these in a row rather than one big jump.
Parameters
idnumberxnumberynumberpressurenumber(optional)
id, 260, 580
tools/inputSim/touchUp
inputSim.touchUp(id: number)
Lift the contact id.
Parameters
idnumber
id
tools/lib/has
lib.has(path: string) -> boolean
Check if a library asset exists at a path. Thin wrapper over library.has(path).
Parameters
pathstring
Returns boolean
"@builtin/models/Sample/DamagedHelmet"
tools/lib/list
lib.list(assetType?: string)
List available library content. No args returns every registered library asset; pass an asset-type string to filter (e.g. "model", "shader", "material", "component"). Thin wrapper over library.list(assetType).
Parameters
assetTypestring(optional)
-- all library assets
"model" -- just models
tools/lighting/addLight
lighting.addLight(name: string, position: Vec3, opts?: AddLightOpts) -> AddLightResult
Add a light to the scene: spawns an entity at position carrying a point Light (default) or SpotLight component. One consistent shape — use setLight to tweak it afterwards and removeLight to delete it. Lights cast shadows by default (a light whose objects cast none reads as broken); pass castsShadows = false for a cheap fill light. intensity means the same thing to both kinds: the two are rows of one light buffer shaded by color * intensity * falloff, so the same number at the same radius puts the same light on a surface either kind faces from the same place. A spot delivers it to the cone it opens on rather than all around itself, so it lights a smaller part of the room at that number — aim it and read the surface it lands on, and reach for radius and angle before reaching for a bigger intensity.
Parameters
namestringpositionVec3optsAddLightOpts(optional)
Returns AddLightResult
"torch1", { x = 5, y = 3, z = 0 }, { color = {1, 0.6, 0.2}, intensity = 1.5, radius = 8 }
"beam", { x = 0, y = 6, z = 0 }, { kind = "spot", intensity = 1.5, radius = 8, angle = 30 }
tools/lighting/get
lighting.get() -> GetLightingResult
Read the scene's lighting + sky state. Returns every light (entity name, id, kind, values), the sky entity and its component type + current values, and — when the sky is a material Skybox — the material's editable parameter names/kinds/values (what setSky params accepts). sky.source = "fallback" means the scene has NO explicit sky entity and renders the engine fallback (adds the missing-sky warning; setSky fixes it). Each light also reports mobility (how GI baking treats it), enabled (the component's own switch) and contributing — whether this light reaches the frame. Each kind answers it from what carries a light of that kind: a punctual light from the row the renderer holds for it, a directional light from whether it holds the sun, an ambient light from whether it holds the scene's ambient term. So it is false for a switched-off light of any kind, which holds neither a row nor a singleton, for a punctual light the renderer holds no row for, for a directional light that does not hold the sun, for an ambient light that does not hold the ambient term, and for a baked static light whose light comes from the bake rather than from the light itself. A light the renderer holds reports what it is doing with the light's shadow, beside the castsShadows flag the scene authored: shadowing is whether a shadow map is being drawn for it this frame, shadowSlot the point-shadow cube slot a point light was seated in, and shadowLayer the spot-shadow atlas layer a spot or rect filed into. Each is -1 for a caster the pool had no room for, which renders lit and throws no shadow. The same row carries layerMask: the render layer mask of the entity carrying the light, which cameras whose include mask intersects it are lit by. A punctual, directional or ambient light answers contributing, shadowing, shadowSlot, shadowLayer and holdsAmbient from the light set the renderer resolved on its LAST frame, so one authored earlier in the same call reads enabled = true with contributing = false and no shadow fields; read it again a frame on and the renderer has an answer for it. A directional light reports holdsSun: the scene's sun is one field set with one holder, so holdsSun = true marks the light whose direction, colour, intensity and castsShadows the scene is lit and shadowed by. The values of a directional light that does not hold it are authored and unread until it takes the sun back. An ambient light reports holdsAmbient on the same rule: the scene's ambient term is one field set with one holder, so holdsAmbient = true marks the light whose colour and intensity the ambient term carries, and the values of the ambient lights beside it are authored and unread. A sky with syncSunToLight on drives the sun holder from its time of day, so that light's direction, color and intensity are what the sky put there rather than what the scene authored. When the two differ the record carries resolved — the values the geometry is lit by — and drivenBy, naming what is writing them. directionals and ambients are the scoped collections: one record per light entity, each carrying entityId, layerMask (the render layer mask of the entity carrying it — cameras whose include mask intersects it are lit by it), color and intensity, plus direction and castsShadows on a directional record. A light on an entity with no explicit render layer records the default layer.
Returns GetLightingResult
tools/lighting/removeLight
lighting.removeLight(name: string) -> RemoveLightResult
Remove a light entity by name (exact, then substring — ambiguity errors and lists the scene's lights). Despawns the entity.
Parameters
namestring
Returns RemoveLightResult
"torch1"
tools/lighting/setAmbient
lighting.setAmbient(opts: AmbientOpts) -> SetAmbientResult
Set the scene's ambient light. Modifies the entity carrying the scene's ambient light, spawning one when the scene has none, and reports the entity it wrote. Read-merge-write: ONLY the fields you pass change — { intensity = 0.5 } keeps the authored color.
Parameters
optsAmbientOpts
Returns SetAmbientResult
{ color = {0.4, 0.4, 0.6}, intensity = 0.3 }
{ intensity = 0.5 }
tools/lighting/setLight
lighting.setLight(name: string, opts: SetLightOpts) -> SetLightResult
Update a light by entity name — exact match first, then substring (ambiguity errors and lists the scene's lights). Only the fields you pass change. Works on point, spot, area and distant lights and the sun/ambient entities.
Parameters
namestringoptsSetLightOpts
Returns SetLightResult
"torch1", { intensity = 3 }
"sun", { direction = {-0.2, -1, -0.1} }
tools/lighting/setSky
lighting.setSky(sky?: string, opts?: SkyOpts) -> SetSkyResult
Set or reconfigure the scene's sky in one call. sky is a word: a procedural preset (clear_day, sunset, sunrise, overcast, night) applies that look via a ProceduralSky component; studio is the solid gray sky (Skybox over sky_solid); none turns the sky off explicitly (Skybox kind="none"); any other word resolves against the project's materials FILTERED to sky-domain shaders (exact identity → leaf name → substring) and the sky renders that material via a Skybox. Omit sky to reconfigure the current sky with opts only. The tool finds the scene's sky entity (creating a "sky" entity when absent) and writes the component — changes persist, replicate, and survive reload.
Parameters
skystring(optional)optsSkyOpts(optional)
Returns SetSkyResult
"sunset"
"aurora", { params = { exposure = 1.4 } }
nil, { timeOfDay = 3, autoCycle = true }
"none"
tools/lighting/setSun
lighting.setSun(opts: SunOpts) -> SetSunResult
Set the scene's directional sun light. Modifies the entity carrying the scene's directional light, spawning one when the scene has none, and reports the entity it wrote. Read-merge-write: ONLY the fields you pass change — { intensity = 2 } keeps the authored direction and color. A sky with syncSunToLight on drives the sun from its time of day, so setting the sun here takes it back: that switch goes off and the result reports releasedFromSky with the sky entity's name. Move the sun by timeOfDay (setSky) to keep the sky driving it.
Parameters
optsSunOpts
Returns SetSunResult
{ direction = {-0.5, -1, -0.3}, color = {1, 0.95, 0.8}, intensity = 1.2 }
{ intensity = 2 }
tools/lighting/setup
lighting.setup(opts: SetupOpts) -> SetupResult
Set up the whole scene lighting rig in one call. Only provided sections change. sun/ambient are partial updates on the entities carrying those lights; sky + skyOpts follow setSky semantics (preset word, sky-material word, procedural controls / material params); lights is a map of light name → addLight opts with a position — each is created if missing, updated if present. Naming sun takes the sun back from a sky driving it from its time of day: that sky's syncSunToLight goes off and releasedFromSky reports its entity name.
Parameters
optsSetupOpts
Returns SetupResult
{ sun = { intensity = 1.2 }, sky = "sunset", lights = { torch1 = { position = {x=5,y=3,z=0}, color = {1,0.6,0.2} } } }
tools/localContent/forget
localContent.forget(path: string) -> string
Stop holding a path under /source/local/ on this machine. The file goes and the record of it goes too, so the next boot does not restore it. A folder takes its contents with it. Content already promoted with localContent.persist is untouched — that copy belongs to the world.
Parameters
pathstring
Returns string
"gestures/wave.json"
tools/localContent/list
localContent.list() -> HeldContent
List everything this machine holds under /source/local/ — the content a session wrote that stays on the device instead of going to the world. Each entry reports its path and its size in bytes, in path order, with the total alongside. Pairs with localContent.persist (list -> persist).
Returns HeldContent
tools/localContent/persist
localContent.persist(path: string, destination?: string) -> { string }
Copy content this machine has been holding under /source/local/ into the world, at the same relative path under /source/ unless destination says otherwise. Takes a file or a whole folder. The local copy stays where it is — persisting is a promotion, not a move, so a failed publish never costs the only copy. Refused while play is running: a /source write during play lands on the play shadow and would not persist, which is the exact failure this tool exists to end. Pairs with localContent.list (list -> persist).
Parameters
pathstringdestinationstring(optional)
Returns { string }
"gestures/wave.json"
tools/logs/clear
logs.clear() -> ClearResult
Drop the buffered log entries, so a following search sees only what happened after the clear. Lifetime per-level counts are preserved, so "were there ever any errors" stays answerable. Rarely the right move: summary returns a cursor which, passed back to search as since, isolates what an action logged without discarding lines anybody else may want.
Returns ClearResult
tools/logs/errors
logs.errors(opts?: (string | ErrorsOpts)) -> ErrorsPage
Recent errors and warnings, newest first. A snapshot — the same lines however many times it is called. That is the difference from the problems tool in the debug toolbox, which drains: it advances a read-cursor and reports only what has not been seen, so that one answers "what is new" while this answers "what is there". Accepts the same filters as search.
Parameters
opts(string | ErrorsOpts)(optional)
Returns ErrorsPage
{ level = "error", limit = 10 }
{ within = 120 }
"shader"
tools/logs/search
logs.search(opts?: (string | SearchQuery)) -> SearchResult
Search what the engine logged. Filter by severity, subsystem, substring or regex, the script or entity that logged it, and a time window — every filter is optional and they narrow together. This is the general view over the same ring the problems tool in the debug toolbox drains: reach for it when you need a specific message, a specific entity, a specific minute, or the lines that led up to a failure. The same ring reads as plain text at /zero/runtime/logs/engine, which is the shorter reach for a string you can already name; this is the view that filters it.
Parameters
opts(string | SearchQuery)(optional)
Returns SearchResult
{ level = "error", within = 60 }
{ entity = "ent_9f3c", limit = 20 }
{ level = "error", context = 5 }
{ level = "warn", group = true }
{ contains = "shader", type = "RENDERER" }
"shader"
tools/logs/summary
logs.summary(opts?: (string | SummaryOpts)) -> SummaryReport
How many lines the engine logged, of what severity, from which subsystems and entities, and which messages repeat — the shape of the noise before you go looking inside it. Also the cheapest "did anything go wrong" check, and the source of the cursor for incremental polling: read cursor before an action, pass it back to search as since afterwards, and you see only what that action logged. Accepts the same filters as search, so it can describe a slice as well as the whole.
Parameters
opts(string | SummaryOpts)(optional)
Returns SummaryReport
{ level = "error" }
{ within = 300 }
tools/logs/tail
logs.tail(opts?: (string | TailOpts)) -> TailPage
The most recent lines the engine logged, any level, oldest first — the last line is the newest. The view for when you do not yet know what you are looking for; once you do, search narrows it. Accepts the same filters as search.
Parameters
opts(string | TailOpts)(optional)
Returns TailPage
{ limit = 40 }
{ type = "SCRIPT" }
"shader"
tools/notices/drain
notices.drain() -> Drained?
Take every pending notice and return the formatted delivery block — the same text that would otherwise be attached to your next tool result. DRAINING: what this returns is no longer queued, so read it. Returns nil when nothing is pending. For a caller that pulls its own context; if you read notices off your tool results, you do not need this. Overflow content is written to /source/tmp/notices/ and named in the block.
Returns Drained?
tools/notices/list
notices.list() -> NoticesList
Show the currently pending notice keys and the suppressed set. Each pending row carries its key, template, severity, origin, count, and source; each suppressed row carries its key and the number of times it has fired while muted. Use a key from here with suppress.
Returns NoticesList
tools/notices/suppress
notices.suppress(key: string) -> { suppressed: string }
Mute a notice key you have acknowledged. It keeps counting (visible in list) but never renders on a tool call again this session. Available to the operating agent; a loaded world component cannot mute its own notices.
Parameters
keystring
Returns { suppressed: string }
"user:atlas:texture rebuilt"
tools/notices/unsuppress
notices.unsuppress(key: string) -> { unsuppressed: string }
Un-mute a notice key you previously suppressed, so it renders on a tool call again. Available to the operating agent.
Parameters
keystring
Returns { unsuppressed: string }
"user:atlas:texture rebuilt"
tools/phys/addBody
phys.addBody(id: string, bodyType?: ("dynamic" | "static" | "kinematic"), opts?: BodyOpts) -> string
Add Physics + Collider components to an entity in one call. bodyType selects the rigid-body kind: * "dynamic" — falls / responds to forces and collisions * "static" — immovable, only collides * "kinematic" — script-driven, ignores forces Flashes a purple outline (0.5s) so the change is visible.
Parameters
idstringbodyType("dynamic" | "static" | "kinematic")(optional)optsBodyOpts(optional)
Returns string
'my_cube' -- dynamic box
'floor', 'static', { shape = 'box' } -- static floor
'ball', 'dynamic', { shape = 'sphere', radius = 0.5 }
tools/phys/addConstraint
phys.addConstraint(entityId: string | EntityRef, constraintType: ConstraintKind, targetEntityId: string | EntityRef, opts?: ConstraintOpts) -> string | EntityRef
Add a transform constraint to an entity. Constraint types: * "position" — follow target's position * "rotation" — follow target's rotation * "scale" — follow target's scale * "lookat" — orient toward target * "parent" — full local-space parent relationship * "aim" — orient one axis toward target The constraint runs on the constraints system every frame, not the physics solver — so it works on entities without Physics bodies too.
Parameters
entityIdstring | EntityRefconstraintTypeConstraintKindtargetEntityIdstring | EntityRefoptsConstraintOpts(optional)
Returns string | EntityRef
"follower", "lookat", "player"
"camera", "position", "car", { offset = {0, 4, 8}, weight = 0.9 }
tools/phys/addJoint
phys.addJoint(entityIdA: string, entityIdB?: (string | AddJointOpts), opts?: AddJointOpts) -> string
Add a physics joint between two entities. Both must have Physics components. Two call shapes are accepted: * phys.addJoint(entityA, entityB, opts?) — explicit two-entity form. * phys.addJoint(entityA, opts) where opts.target is the second entity name or ID — matches the README's phys.addJoint("crate", { target = "wall", ... }) shape and the Joint component's target alias. Validates joint kind against the supported set (fixed, hinge, ball, prismatic, spring, rope), resolves names to entity IDs, normalises vec3 args (localAnchor, remoteAnchor, axis), and rejects stiffness / damping on kind = "fixed" and kind = "rope" since neither has a motorable axis (Closes #1062 for the silently-discarded case; Closes #527 for the silent no-op when entities were missing). kind = "rope" requires maxDistance > 0 — the maximum distance between the anchor points; slack is free, taut resists extension. breakForce (newtons of linear reaction) and breakTorque (the angular row of the same reaction) release the joint once the reaction it carries exceeds either; the break is reported through Physics.onJointBreak and the joint's own onBreak, and a joint given neither is unbreakable.
Parameters
entityIdAstringentityIdB(string | AddJointOpts)(optional)optsAddJointOpts(optional)
Returns string
"door", "frame", { kind = "hinge", axis = {0, 1, 0} }
"crate", { target = "wall", kind = "spring", stiffness = 200, damping = 10 }
"ball", { target = "hook", kind = "rope", maxDistance = 8 }
"crate", { target = "wall", kind = "fixed", breakForce = 1200, breakTorque = 800 }
tools/phys/ignoreCollision
phys.ignoreCollision(entityIdA: string, entityIdB: string, ignore?: boolean) -> string
Set whether two entities ignore collisions between each other. ignore defaults to true; pass false to re-enable collisions. Useful for "drop pickup through self" and similar paired-entity rules without touching collision groups.
Parameters
entityIdAstringentityIdBstringignoreboolean(optional)
Returns string
"player", "pickup"
"player", "pickup", false -- re-enable
tools/phys/lockRotation
phys.lockRotation(entityId: string | EntityRef, lockX: boolean, lockY: boolean, lockZ: boolean) -> string | EntityRef
Lock rotation axes on an entity's rigid body. Each flag is independent — (true, false, true) lets the body spin only around Y. Useful for top-down characters, hinged doors, and constrained vehicles.
Parameters
entityIdstring | EntityReflockXbooleanlockYbooleanlockZboolean
Returns string | EntityRef
"cube", true, false, true -- only rotate around Y
tools/phys/lockTranslation
phys.lockTranslation(entityId: string | EntityRef, lockX: boolean, lockY: boolean, lockZ: boolean) -> string | EntityRef
Lock translation axes on an entity's rigid body. Each flag is independent — (true, true, false) lets the body only slide along Z (e.g. a rail / track).
Parameters
entityIdstring | EntityReflockXbooleanlockYbooleanlockZboolean
Returns string | EntityRef
"rail", true, true, false -- only move along Z
tools/phys/removeBody
phys.removeBody(id: string) -> string
Remove an entity's Physics component and whichever collider component it carries. Strips the collider before Physics so the engine never sees a collider without a body during the transition.
Parameters
idstring
Returns string
"my_cube"
tools/phys/removeConstraint
phys.removeConstraint(entityId: string | EntityRef, index?: number) -> string | EntityRef
Remove transform constraints from an entity. Pass an 0-based index to remove a specific constraint; omit it to remove all of them.
Parameters
entityIdstring | EntityRefindexnumber(optional)
Returns string | EntityRef
"follower" -- remove all
"follower", 0 -- remove just the first
tools/phys/removeJoint
phys.removeJoint(entityId: string | EntityRef) -> string | EntityRef
Remove all joints from an entity. Thin wrapper over Physics.removeJoint(entityId) — included in the toolbox so add/remove joint flows live behind the same phys.* surface.
Parameters
entityIdstring | EntityRef
Returns string | EntityRef
"crate"
tools/phys/setBodyType
phys.setBodyType(entityId: string | EntityRef, bodyType: "dynamic" | "kinematic" | "static") -> string | EntityRef
Change a rigid body's type at runtime. Preserves mass, colliders, and joints — only the body's response to forces and position writes changes. Useful for ragdoll activation (freeze pose as kinematic until hit, then flip to dynamic), freeze / unfreeze mechanics, and switching platforms between kinematic (scripted motion) and static. An unknown bodyType string raises a Luau error with the valid set.
Parameters
entityIdstring | EntityRefbodyType"dynamic" | "kinematic" | "static"
Returns string | EntityRef
"player", "kinematic" -- freeze
"player", "dynamic" -- unfreeze, respond to forces
tools/phys/setCollisionGroups
phys.setCollisionGroups(entityId: string | EntityRef, membership: number, filter: number) -> string | EntityRef
Set collision group bitmasks on an entity's colliders. The two bitmasks encode "what am I" (membership) and "what do I collide with" (filter) separately — two entities collide iff each entity's filter contains a bit from the other entity's membership.
Parameters
entityIdstring | EntityRefmembershipnumberfilternumber
Returns string | EntityRef
"player", 0x1, 0x2 -- Player on group 1 collides only with terrain (group 2)
tools/phys/spawnDynamic
phys.spawnDynamic(name: string, url?: (string | AssetRef<mesh>), x?: number, y?: number, z?: number, opts?: SpawnOpts) -> string
Spawn an entity with a Model + dynamic Physics body in one call. The returned id is the unique ent_... handle from entity.spawn (not the display name), matching sc.spawnModel's contract (#579). Pops the entity in with a purple viz pulse.
Parameters
namestringurl(string | AssetRef<mesh>)(optional)xnumber(optional)ynumber(optional)znumber(optional)optsSpawnOpts(optional)
Returns string
'my_box'
'ball', 'sphere', 0, 5, 0, { mass = 1 }
tools/phys/spawnStatic
phys.spawnStatic(name: string, url?: (string | AssetRef<mesh>), x?: number, y?: number, z?: number, opts?: SpawnOpts) -> string
Spawn an entity with a Model + static collider in one call. Static = immovable, only collides. The returned id is the unique ent_... handle from entity.spawn, matching sc.spawnModel's contract (#579).
Parameters
namestringurl(string | AssetRef<mesh>)(optional)xnumber(optional)ynumber(optional)znumber(optional)optsSpawnOpts(optional)
Returns string
'floor', 'cube', 0, 0, 0, { scale = { 10, 1, 10 } }
tools/phys/whyStill
phys.whyStill(entityId: string | EntityRef) -> { [string]: any }
Answer why the solver is not moving an entity's rigid body. The reason is the nearest cause from a closed set — noBody, simulationNotStepping, disabled, static, kinematic, infiniteMass, translationLocked, gravityDisabled, asleep, outsideIsland, resting, aboutToMove — so it names the thing to change rather than a consequence of it. A body the solver IS advancing reports moving = true and no reason. Every value comes off the simulation, so a write the solver refused or clamped reads back as what it kept. Answers in edit mode as well as play mode.
Parameters
entityIdstring | EntityRef
Returns { [string]: any }
"crate"
tools/phys/worldState
phys.worldState() -> { [string]: any }
Report what the physics simulation is actually holding: bodies by type, how many are awake and asleep, colliders, joints, contact pairs / touching pairs / contact points, the bodies the last step integrated, world gravity, the timestep, whether the pipeline is stepping at all, and what the last step cost. Counted off the solver, so a body that failed to build is missing here while its Physics component still exists. Answers in edit mode as well as play mode.
Returns { [string]: any }
-- no arguments
tools/pixelArt/add
pixelArt.add(opts: PaletteAdd)
Parameters
optsPaletteAdd
tools/pixelArt/addFrame
pixelArt.addFrame(opts: AnimAddFrame) -> number?
Parameters
optsAnimAddFrame
Returns number?
tools/pixelArt/anim
pixelArt.anim(opts: AnimOpts) -> number?
Multi-frame animation control. Pick the action via opts.op. All ops take target (handle/id/name). Frames share the canvas palette + size; draw into a frame by making it active (setActiveFrame) then using Pixel.draw.
Parameters
optsAnimOpts
Returns number?
{ op = 'duplicateFrame', target = s }
{ op = 'setFps', target = s, fps = 8 }
tools/pixelArt/bounds
pixelArt.bounds(opts: QueryBounds) -> any
Parameters
optsQueryBounds
Returns any
tools/pixelArt/circle
pixelArt.circle(opts: DrawCircle)
Parameters
optsDrawCircle
tools/pixelArt/clear
pixelArt.clear(opts: DrawClear)
Parameters
optsDrawClear
tools/pixelArt/clone
pixelArt.clone(opts: SpawnClone) -> Handle
Parameters
optsSpawnClone
Returns Handle
tools/pixelArt/countByName
pixelArt.countByName(opts: QueryCountByName) -> any
Parameters
optsQueryCountByName
Returns any
tools/pixelArt/create
pixelArt.create(opts: SpawnCreate) -> Handle
Parameters
optsSpawnCreate
Returns Handle
tools/pixelArt/deleteTemplate
pixelArt.deleteTemplate(opts: PersistDeleteTemplate) -> boolean
Parameters
optsPersistDeleteTemplate
Returns boolean
tools/pixelArt/destroy
pixelArt.destroy(target: Handle | string | number)
Despawn a pixel canvas. target is a handle, entity id, or name.
Parameters
targetHandle | string | number
"hero"
tools/pixelArt/draw
pixelArt.draw(opts: DrawOpts)
Raster drawing onto a canvas's ACTIVE frame. Pick the primitive via opts.op. All ops take target (handle/id/name) + color (palette name; nil clears) plus op-specific coords.
Parameters
optsDrawOpts
{ op = 'fillCircle', target = s, cx = 8, cy = 8, r = 6, color = 'red' }
{ op = 'rows', target = s, rows = {'rr','rr'}, legend = { r = 'red' } }
tools/pixelArt/duplicateFrame
pixelArt.duplicateFrame(opts: AnimDuplicateFrame) -> number?
Parameters
optsAnimDuplicateFrame
Returns number?
tools/pixelArt/ellipse
pixelArt.ellipse(opts: DrawEllipse)
Parameters
optsDrawEllipse
tools/pixelArt/fillCircle
pixelArt.fillCircle(opts: DrawFillCircle)
Parameters
optsDrawFillCircle
tools/pixelArt/fillRect
pixelArt.fillRect(opts: DrawFillRect)
Parameters
optsDrawFillRect
tools/pixelArt/frameCount
pixelArt.frameCount(opts: QueryFrameCount) -> number
Parameters
optsQueryFrameCount
Returns number
tools/pixelArt/getPixel
pixelArt.getPixel(opts: QueryGetPixel) -> string?
Parameters
optsQueryGetPixel
Returns string?
tools/pixelArt/line
pixelArt.line(opts: DrawLine)
Parameters
optsDrawLine
tools/pixelArt/listTemplates
pixelArt.listTemplates(_opts: QueryListTemplates) -> { string }
Parameters
_optsQueryListTemplates
Returns { string }
tools/pixelArt/palette
pixelArt.palette(opts: PaletteOpts)
Palette editing. op='set' replaces the whole palette (cell indices preserved, so it recolors every frame in place); op='add' adds/updates one entry.
Parameters
optsPaletteOpts
{ op = 'add', target = s, name = 'blue', color = {0,0,1} }
tools/pixelArt/persist
pixelArt.persist(opts: PersistOpts) -> string | boolean
Template persistence. op='saveAsTemplate' exports a canvas (all frames) as a reusable pixelTemplate asset; op='deleteTemplate' removes one. Instantiate saved templates with Pixel.spawn(op='template').
Parameters
optsPersistOpts
Returns string | boolean
{ op = 'saveAsTemplate', target = s, name = 'hero' }
{ op = 'deleteTemplate', name = 'hero' }
tools/pixelArt/pixel
pixelArt.pixel(opts: DrawPixel)
Parameters
optsDrawPixel
tools/pixelArt/play
pixelArt.play(opts: AnimPlay)
Parameters
optsAnimPlay
tools/pixelArt/query
pixelArt.query(opts: QueryOpts) -> any
Read-only queries against a canvas (or the template library). Pick via opts.op. Per-canvas ops take target (handle/id/name).
Parameters
optsQueryOpts
Returns any
{ op = 'getPixel', target = s, x = 0, y = 0 }
{ op = 'listTemplates' }
tools/pixelArt/rect
pixelArt.rect(opts: DrawRect)
Parameters
optsDrawRect
tools/pixelArt/removeFrame
pixelArt.removeFrame(opts: AnimRemoveFrame)
Parameters
optsAnimRemoveFrame
tools/pixelArt/rows
pixelArt.rows(opts: SpawnRows) -> Handle
Parameters
optsSpawnRows
Returns Handle
tools/pixelArt/saveAsTemplate
pixelArt.saveAsTemplate(opts: PersistSaveAsTemplate) -> string
Parameters
optsPersistSaveAsTemplate
Returns string
tools/pixelArt/set
pixelArt.set(opts: PaletteSet)
Parameters
optsPaletteSet
tools/pixelArt/setActiveFrame
pixelArt.setActiveFrame(opts: AnimSetActiveFrame)
Parameters
optsAnimSetActiveFrame
tools/pixelArt/setFps
pixelArt.setFps(opts: AnimSetFps)
Parameters
optsAnimSetFps
tools/pixelArt/spawn
pixelArt.spawn(opts: SpawnOpts) -> Handle
Generic pixel-canvas creation + placement. Pick behavior via opts.op: 'create' (blank/seeded sheet), 'rows' (paint from text + a legend), 'template' (instantiate a saved pixelTemplate), or 'clone' (duplicate an existing canvas). Returns a handle.
Parameters
optsSpawnOpts
Returns Handle
{ op = 'create', width = 16, height = 16, palette = { red = {1,0,0} } }
{ op = 'rows', rows = {" r ","rrr"," r "}, legend = { r = "red" }, palette = { red = {1,0,0} } }
{ op = 'template', name = 'hero', position = { 0, 1, 0 } }
tools/pixelArt/stop
pixelArt.stop(opts: AnimStop)
Parameters
optsAnimStop
tools/pixelArt/template
pixelArt.template(opts: SpawnTemplate) -> Handle
Parameters
optsSpawnTemplate
Returns Handle
tools/pp/add
pp.add(name: string, opts?: PostEffectOpts) -> string
Add a post-processing effect. Use a built-in preset name (bloom, color_correction, colorGrade, fog, grayscale, invert, lut, sepia, tonemap, vignette) or provide custom WGSL via opts.source (author only fn fragment(in: PostInput) -> vec4<f32>, declare named material properties via opts.properties). Forwards to postprocess.add(name, source, { properties, priority, enabled, layer }), applies each property value via postprocess.setProperty(name, prop, value), and binds each texture via postprocess.setTexture(name, prop, path). Every property the effect's schema declares is settable by its own name in this call — the schema is what decides which keys opts takes, so a key it does not declare is refused against that set rather than dropped. postprocess.describe(name) lists the schema of a registered effect and the value each property holds; asset.resolve("@builtin::shaders.post.<preset>", "shader"):getProperties() lists a preset's before it is added. opts.layer picks which composited image the effect grades: "scene" runs it before the UI is drawn, so it grades the rendered picture and leaves every widget on screen as authored, and "all" (the default) runs it after the UI has landed, so the interface is graded along with the picture. A custom opts.source reads the scene's depth at a pixel with zero_scene_depth(in.uv), which a defocus, a distance haze or a depth-keyed grade drives from.
Parameters
namestringoptsPostEffectOpts(optional)
Returns string
'vignette', { intensity = 0.42, radius = 0.8, softness = 0.55 }
'bloom', { threshold = 1.0, intensity = 1.5, radius = 12 }
'color_correction', { brightness = 0.1, saturation = 0.3 }
'tonemap', { mode = 'ACES', exposure = 1.2 }
'lut', { texture = lut.install('dusk', lut.fromGrade({ temperature = -0.2 })).guid }
'myeffect', { source = 'fn fragment(in: PostInput) -> vec4<f32> {...}', properties = {{name='k', type='float', default={1}}}, k = 0.25 }
tools/pp/list
pp.list() -> { string }
List the currently-active post-processing effects. Forwards to postprocess.list().
Returns { string }
tools/pp/presets
pp.presets() -> {string}
List the built-in preset effect names sorted alphabetically. The same names are accepted by pp.add.
Returns {string}
tools/pp/remove
pp.remove(name: string)
Remove a post-processing effect by name. Forwards to postprocess.remove(name). Removing an effect that does not exist follows the underlying API's contract.
Parameters
namestring
'vignette'
tools/pp/setEnabled
pp.setEnabled(name: string, enabled: boolean)
Enable or disable a post-processing effect without removing it. Forwards to postprocess.setEnabled(name, enabled).
Parameters
namestringenabledboolean
'bloom', false
tools/primitives/cube
primitives.cube(name: SpawnTarget, x?: number, y?: number, z?: number, opts?: CubeOpts) -> string
Spawn a visual cube at a position. No physics by default — add Physics and Collider components explicitly if collision is wanted. Set opts.scale to size the cube — a number for a uniform cube, or three components as {x, y, z} or { x = , y = , z = } for a box. Set opts.animate=true to play a viz.popIn entrance (default off — at bulk-spawn rates the per-call string.format + tween-source compile dominates spawn time).
Parameters
nameSpawnTargetxnumber(optional)ynumber(optional)znumber(optional)optsCubeOpts(optional)
Returns string
"box1", 0, 5, 0
{ name = "crate", position = { 4, 0, 2 } }
"backdrop", 0, 0, -10, { scale = 5 }
"beam", 0, 1, 0, { scale = { 4, 0.4, 0.4 } }
"falling", 0, 5, 0
tools/primitives/ground
primitives.ground(name: SpawnTarget, x?: number, y?: number, z?: number, color?: shared.ColorInput, size?: number) -> string
Spawn a flat ground plane with optional color tint and size. The base mesh is 1x0.1x1 unit; size scales it uniformly in XZ. Always has static physics (it's a ground plane).
Parameters
nameSpawnTargetxnumber(optional)ynumber(optional)znumber(optional)colorshared.ColorInput(optional)sizenumber(optional)
Returns string
"floor", 0, 0, 0
{ name = "floor", position = { 0, -1, 0 } }
"red_floor", 0, 0, 0, {r=200, g=50, b=50}
"blue_floor", 0, 0, 0, {50, 90, 200}
"big_floor", 0, 0, 0, nil, 100
tools/primitives/groundHex
primitives.groundHex(name: SpawnTarget, x?: number, y?: number, z?: number, color?: shared.ColorInput) -> string
Spawn a hexagonal ground tile with optional color tint. Includes static physics so objects rest on it.
Parameters
nameSpawnTargetxnumber(optional)ynumber(optional)znumber(optional)colorshared.ColorInput(optional)
Returns string
"hex_tile", 0, 0, 0
{ name = "hex_tile", position = { 3, 0, 3 } }
"grass_hex", 0, 0, 0, {r=80, g=160, b=60}
"sand_hex", 0, 0, 0, {200, 180, 120}
tools/primitives/tree
primitives.tree(name: SpawnTarget, x?: number, y?: number, z?: number, preset?: TreePreset, seed?: number) -> string
Spawn a procedural tree from a preset. Includes static physics collider so objects collide with it.
Parameters
nameSpawnTargetxnumber(optional)ynumber(optional)znumber(optional)presetTreePreset(optional)seednumber(optional)
Returns string
"oak1", 5, 0, 3, "oak"
{ name = "oak1", position = { 5, 0, 3 } }, nil, nil, nil, "oak"
"tree_" .. i, math.random(-20, 20), 0, math.random(-20, 20), "pine", i * 42
tools/procgen/adjust
procgen.adjust(args: AdjustArgs) -> AdjustResult
Adjust a live generator in place: merge values into its param overrides (each key overrides the graph input of the same name), switch which output it realizes, or toggle autoBake. The change queues a cook on the generator's async runner and returns immediately — a heavy graph keeps cooking across frames while this call reports back. Watch it with procgen.cooks, cancel it with procgen.cancel.
Parameters
argsAdjustArgs
Returns AdjustResult
{ entity = "LegoSculpture", params = { model = "@builtin::meshes.torus" } }
tools/procgen/apply
procgen.apply(args: ApplyArgs) -> ApplyResult
Flatten a live generator and detach it: the current output is written as durable content (real mesh assets), spawned as permanent children, and the Generator component is removed — the result stands on its own with no graph and no generator at runtime, like applying a modifier.
Parameters
argsApplyArgs
Returns ApplyResult
{ entity = "LegoSculpture" }
tools/procgen/bake
procgen.bake(args: BakeArgs) -> BakeResult
Evaluate a .procGraph graph target and realize it into the live scene as durable content, then verify it rendered. Geometry becomes a .mesh asset + a Model entity; an InstanceSet becomes live per-instance entities; a Bundle/Prefab becomes durable live entities that survive the edit->play reload. Pass replace to swap an existing entity in place (inheriting its position); position to place it; material to tint a Geometry bake.
Parameters
argsBakeArgs
Returns BakeResult
{ graph = "/source/proc/tower.procGraph", name = "tower", position = { 0, 0, 0 } }
tools/procgen/cancel
procgen.cancel(args: CancelArgs) -> CancelResult
Cancel the in-flight cook on an entity's Generator. The previous output stays; the next param change cooks fresh.
Parameters
argsCancelArgs
Returns CancelResult
{ entity = "LegoSculpture" }
tools/procgen/collapse
procgen.collapse(args: CollapseArgs) -> CollapseResult
Extract nodes out of a .procGraph into a new .procGraph named name, written as its own init.luau — a graph to keep editing. Every ref crossing the extracted cluster's boundary becomes a declared child input (outside producer -> extracted node) or child output (extracted node -> outside consumer); identical boundary sources/sinks share one socket. The host graph is left as it was; procgen.reference is the call that rewrites its source to use the child, and this returns the exact one to make.
Parameters
argsCollapseArgs
Returns CollapseResult
{ graph = "/source/proc/scene.procGraph", nodes = { "pts", "inst" }, name = "Scatter" }
tools/procgen/cooks
procgen.cooks() -> CooksResult
List the active cooks and the recent cook history — progress, status, and failure detail (which node failed). A finished record's durationMs spans the whole regen: evaluation plus realizing the output and refreshing the bake snapshot. The scene-wide cook monitor.
Returns CooksResult
tools/procgen/diff
procgen.diff(args: DiffArgs) -> DiffResult
Compare two .procGraph graphs and report changed/added/removed nodes plus per-node op/param/input deltas (structural, via the content hash).
Parameters
argsDiffArgs
Returns DiffResult
{ a = "/source/proc/box.procGraph", b = updatedGraph }
tools/procgen/explain
procgen.explain(args: ExplainArgs) -> ExplainResult
Agent-oriented natural-language summary of a .procGraph graph: its size, declared inputs (with types), the op of each node, the composite references, and where each output comes from.
Parameters
argsExplainArgs
Returns ExplainResult
{ graph = "/source/proc/box.procGraph" }
tools/procgen/find
procgen.find(args?: FindArgs) -> FindResult
Discover procedural graphs by what they produce. Lists every .procGraph asset, opens each, reads its declared outputs' types, and filters on name (substring over name/id), tag, category, and/or produces (an output type or producing-op substring, e.g. "Bundle", "Geometry", "mesh."). Returns each match with its resolved outputs — so you can find a graph by its result, not just its description.
Parameters
argsFindArgs(optional)
Returns FindResult
{ produces = "Bundle" }
tools/procgen/flatten
procgen.flatten(args: FlattenArgs) -> FlattenResult
Inline every composite of a .procGraph graph into a single composite-free graph (the opt-in bake). Child nodes are namespaced under the composite id, inputs rewired to the bound values, composite outputs repointed at the inlined child outputs. Nested composites flatten recursively; cross-graph cycles error with the full path chain.
Parameters
argsFlattenArgs
Returns FlattenResult
{ graph = "/source/proc/bank_vault.procGraph", out = "/source/proc/bank_vault_flat.procGraph" }
tools/procgen/generators
procgen.generators() -> GeneratorsResult
List every live procedural generator in the scene — the entities carrying a Generator component — with each one's bound graph, selected output, current param overrides, realized child count, and cook state (whether it's cooking right now, its progress, and its last finished cook's outcome). Use it to find what's generating — or why a generator's output looks stale — before adjusting (procgen.adjust) or flattening (procgen.apply).
Returns GeneratorsResult
tools/procgen/inspect
procgen.inspect(args: InspectArgs) -> InspectResult
Inspect one node of a .procGraph graph: its op, declared input/param/output sockets (from the registry), the InputRefs feeding it (producers), and the nodes that consume its outputs (consumers).
Parameters
argsInspectArgs
Returns InspectResult
{ graph = g, node = "box" }
tools/procgen/modify
procgen.modify(args: ModifyArgs) -> ModifyResult
Apply structural edits to a .procGraph graph, type-check the result, and persist it — a step above procgen.patch. Each edit is a { op } table: { op = "add_node", id, node } (node = the op id, e.g. "mesh.box"); { op = "set_param", node, name, value }; { op = "connect", node, socket, from } (from = { node, output? } for a node output, else a constant); { op = "set_output", name, from } (declare/rebind a graph output); { op = "rename_node", from, to } (updates every reference); { op = "remove_node", node }.
Parameters
argsModifyArgs
Returns ModifyResult
{ graph = "/source/proc/tower.procGraph", edits = { { op = "add_node", id = "n", node = "mesh.sphere" }, { op = "set_output", name = "mesh", from = { node = "n" } } } }
tools/procgen/ops
procgen.ops(query?: string, pack?: string, kind?: string, limit?: number) -> OpsResult
Search the procedural OP REGISTRY — every operation a graph node can name. Every op belongs to a pack: the families this package ships, the terrain.* ops, and any .procPack authored in a world, all listed the same way; a .procNode asset contributes one op addressed by its guid. Filter by free-text query over id and description, by the pack an op came from, or by kind. With no arguments it answers for the whole vocabulary. Every hit's id is what procgen.schema describes and what a node's op field takes, so this is the first call when composing a graph and the one that settles whether an op you remember actually exists. The answer comes in two halves: catalogue names EVERY matching op grouped by family, complete however many there are and small enough to read whole, and results describes the first limit of them. So one call maps the vocabulary and a second, narrowed by query, describes the corner you want.
Parameters
querystring(optional)packstring(optional)kindstring(optional)limitnumber(optional)
Returns OpsResult
"scatter"
nil, nil, nil, 60
tools/procgen/patch
procgen.patch(args: PatchArgs) -> PatchResult
Apply a list of structured edits to a .procGraph graph, then optionally save it. Supported edits (each a table with op): { op = "set_param", node, name, value } — set a node param; { op = "set_input", node, socket, value } — rewire an input (value = { node = "<id>", output? } for a node output, else a constant); { op = "remove_node", node } — delete a node.
Parameters
argsPatchArgs
Returns PatchResult
{ graph = "/source/proc/box.procGraph", edits = { { op = "set_param", node = "box", name = "size", value = 3 } } }
tools/procgen/preview
procgen.preview(args: PreviewArgs) -> PreviewResult
Evaluate a .procGraph graph target, visualize it (any output type), render it, and write image + stats artifacts under /source/tmp/proc-preview/
Parameters
argsPreviewArgs
Returns PreviewResult
{ graph = "/source/proc/box.procGraph", target = "output:mesh" }
{ graph = "/source/proc/valley.procGraph", target = "node:snowBand", layer = "snow" }
tools/procgen/promote
procgen.promote(args: PromoteArgs) -> PromoteResult
Publish the .procGraph at graph as an OPERATION — a .procNode bound to it, which puts the graph in the op registry where ops searches and any graph can reach it by name. Its params are the graph's declared inputs and its outputs are the graph's declared outputs, and it stays bound: editing the graph changes the op, and every graph using it re-cooks. A graph referenced directly as a node is private to whatever names it; an op is part of the shared vocabulary. Promote the graphs worth reusing.
Parameters
argsPromoteArgs
Returns PromoteResult
{ graph = "/source/proc/Scatter.procGraph" }
tools/procgen/reference
procgen.reference(args: ReferenceArgs) -> ReferenceResult
Replace nodes in the .procGraph at graph with a single node that references the .procGraph at child, and write the host back out as source. The host's boundary is derived exactly as collapse derives it, so the graph evaluates to the same result through the child that it did through the nodes — and because the host's init.luau is what the type compiles, this is the step that makes the change the host's actual definition. The host's source is REGENERATED from its graph: every node, wire and declared input survives, and comments in the file do not.
Parameters
argsReferenceArgs
Returns ReferenceResult
{ graph = "/source/proc/scene.procGraph", child = "/source/proc/Scatter.procGraph", nodes = { "pts", "inst" } }
tools/procgen/schema
procgen.schema(id: string) -> any
Describe ONE procedural op: the input sockets it consumes and their types, the params it takes with their declared defaults and units, and the outputs it produces (with the one a wire picks by default). Accepts a builtin op id or a content op's guid, and describes a .procNode exactly as it describes a builtin. This is the call that settles what a node needs before it is wired — an op named from memory, or wired with a param it does not declare, is the usual reason a graph refuses to cook. Find the id with procgen.ops.
Parameters
idstring
Returns any
"terrain.erode"
tools/procgen/search
procgen.search(args?: SearchArgs) -> SearchResult
Search the procedural-graph library — every .procGraph this engine can see, the ones the world authored and the ones it ships. A free-text query matches the graph's name, its identity, the description its .metadata carries and its tags, so the word you would use for the thing finds it even when the graph is named something else. tag and category narrow it. Ask this before describing something from nothing: a graph that already makes what you need is two calls away, and rewriting one costs the work twice over.
Parameters
argsSearchArgs(optional)
Returns SearchResult
{ query = "scatter" }
{ tag = "terrain" }
tools/procgen/spawn
procgen.spawn(args: SpawnArgs) -> SpawnResult
Spawn a live procedural generator from a .procGraph graph: creates an entity with a Generator component bound to the graph, which immediately realizes the graph's output as the entity's managed children and keeps regenerating live as params change. This is the entry point for putting a graph in the scene — no manual entity.spawn / component.add needed.
Parameters
argsSpawnArgs
Returns SpawnResult
{ graph = "/source/legoify.procGraph", name = "LegoSculpture", position = { 0, 3, 0 } }
tools/procgen/stats
procgen.stats(args: StatsArgs) -> StatsResult
Structural counts of a .procGraph graph (nodes / composites / inputs / outputs). When target is given, also evaluates it and reports the output type, elapsed duration, and (for geometry) vertex/triangle counts. Timing is reported only — it never feeds graph evaluation (determinism).
Parameters
argsStatsArgs
Returns StatsResult
{ graph = g, target = "output:mesh" }
tools/procgen/trace
procgen.trace(args: TraceArgs) -> TraceResult
Dependency chain for a target: the node ids reachable from the target, in evaluation order (each node's producers appear before it — a post-order walk of the node_output edges). Composite/const/graph_input leaves stop the walk.
Parameters
argsTraceArgs
Returns TraceResult
{ graph = g, target = "output:mesh" }
tools/procgen/tweak
procgen.tweak(args: TweakArgs) -> TweakResult
Set one or more node params on a .procGraph graph, then re-evaluate + preview in a single step — the parameter-tuning loop. Each entry of params is { node = "<id>", name = "<param>", value = <any> }. Persists the tweaked graph when out is given (else the edit is in-memory for this preview only).
Parameters
argsTweakArgs
Returns TweakResult
{ graph = "/source/proc/tower.procGraph", params = { { node = "floors", name = "count", value = 12 } } }
tools/procgen/validate
procgen.validate(args: ValidateArgs) -> ValidateResult
Type-check a .procGraph graph: missing inputs, type mismatches, cycles, invalid composite bindings. Returns the diagnostics list.
Parameters
argsValidateArgs
Returns ValidateResult
{ graph = "/source/proc/box.procGraph" }
tools/profiler/compare
profiler.compare(before?: string, after?: string) -> string
Diff two recordings (made with record): the change in avg/p90 frametime, and the systems that moved the most between them. Record a baseline, make a change, record again, then compare to see whether the change helped and which system it moved. A positive frametime delta means B is slower than A.
Parameters
beforestring(optional)afterstring(optional)
Returns string
"baseline", "optimized" -- did 'optimized' beat 'baseline', and where
tools/profiler/flamegraph
profiler.flamegraph(seconds?: number, mode?: ("run" | "start" | "stop" | "snapshot"), label?: string) -> string
Sample the Luau call stack to find the hottest code paths — the depth view under a hot component or task_scheduler. flamegraph(seconds) runs the sampler for that long (resetting first) and returns the top stacks plus a folded-stack file for flamegraph rendering. mode picks a manual phase instead: "start" / "stop" a long session, or "snapshot" the current top stacks without stopping.
Parameters
secondsnumber(optional)mode("run" | "start" | "stop" | "snapshot")(optional)labelstring(optional)
Returns string
-- 3s sample, top stacks + folded file
5 -- 5s sample
0, "snapshot" -- top stacks right now without stopping a session
tools/profiler/frame
profiler.frame(source?: string, which?: ("worst" | "typical"), minMs?: number) -> string
The current frame's time as a self-accounting tree: schedule -> system -> sub-timing, each row showing SELF (own time excluding children), TOTAL, calls, and % of frame. Time no schedule covers (the frame limiter / vsync wait, GPU present, event loop) shows as present / idle; each expensive component's update(dt) is listed by component + entity under lua_update.vm_call. Read the live frame with source "now", or a stopped recording by its label (its worst or typical frame). Read from top: the row with the largest SELF time is where the frame time actually goes.
Parameters
sourcestring(optional)which("worst" | "typical")(optional)minMsnumber(optional)
Returns string
-- live frame, full attributed tree
"combat", "worst" -- the worst frame of the 'combat' recording
"combat", "typical" -- the typical (average) frame of that recording
tools/profiler/gpu
profiler.gpu(n?: number) -> string
GPU pass timings over the last resolved frames, from GPU timestamp queries. Each row is one label: the median of its per-frame total, the min/max that median sits in, the passes per frame, and how many of the window's frames carried it. A row marked floor ran but the device resolved no duration for it — its two timestamps retired within a few ticks of each other. The last column is how many resolved frames ago the label last recorded a pass: now is a pass running in the frame the table describes, and anything else is a row the window still holds after the work under it stopped. The readback is asynchronous, so the window lags the live frame by a few frames. Use this when frame shows the time under present / idle (GPU-bound) and you need to know which passes the GPU spends it on.
Parameters
nnumber(optional)
Returns string
-- top 20 GPU spans of the window
40 -- top 40
tools/profiler/hits
profiler.hits(label?: string, spikeMs?: number) -> string
Drain the frames a record-mode watch caught and report them as a spike-cluster distribution — every caught frame grouped by its dominant hotspot, so repeated hitches collapse to their handful of causes instead of a wall of individual frames. Draining clears the buffer. The snapshot is stored under label — drill into any cluster with frame <label> / hotspots <label>. Arm the watchdog first with watch <ceilingMs>.
Parameters
labelstring(optional)spikeMsnumber(optional)
Returns string
-- review everything the watchdog caught
"collapse_hits" -- store under a name for later drill-down
tools/profiler/hotspots
profiler.hotspots(n?: number, source?: string, which?: ("worst" | "typical")) -> string
Rank the frame's costs by SELF time (a node's own cost, excluding its children) and return the top n as a flat table. Because it ranks by SELF, the top rows are the actual expensive leaves — a system's own work or a single heavy component's update(dt) — not the schedules that merely contain them. This is the "just tell me what's slow" tool; follow a hit down with frame. Reads the live frame ("now") or a stopped recording's worst/typical frame (pass its label).
Parameters
nnumber(optional)sourcestring(optional)which("worst" | "typical")(optional)
Returns string
-- top 12 costs in the live frame
20 -- top 20
12, "combat" -- top 12 in the worst frame of the 'combat' recording
tools/profiler/memory
profiler.memory(n?: number) -> string
The Luau VM's memory: total heap + GC state, then the components retaining the most memory (per instance). Use it to catch a growing script — take it, play/test, take it again, and watch which component's retained bytes climb. Complements the frame-time tools: memory pressure shows up as GC cost in frame (the gc node) and as crashes under load, not as one slow system.
Parameters
nnumber(optional)
Returns string
-- VM total + top 20 components by retained memory
10 -- top 10
tools/profiler/record
profiler.record(action?: ("start" | "stop" | "status"), label?: string, spikeMs?: number) -> string
Start / stop / check a background profiling recording that spans a play session. record("start", label) begins capturing every frame; play or test the game across as many turns as you want, then record("stop", label) returns the windowed breakdown: avg / p50 / p90 / p99 / max frametime, spike count, the single WORST frame's attributed tree, and a typical-frame tree. record("status") reports whether a recording is running. A stopped recording is kept under its label — analyse it later with frame, hotspots, or scripts (pass the label as their source), or diff two of them with compare.
Parameters
action("start" | "stop" | "status")(optional)labelstring(optional)spikeMsnumber(optional)
Returns string
"start", "combat" -- begin recording a combat encounter
"stop", "combat" -- end it, get the windowed breakdown
"status" -- is a recording running right now?
tools/profiler/retro
profiler.retro(seconds?: number, label?: string, spikeMs?: number) -> string
Retroactively read the ring buffer's last seconds of frames (default: the whole ring) as a spike-cluster report — the latency-immune profiler. Enable the ring first (ring on), drive the scene, then call this AFTER the spike: the data is historical, so your call's timing does not matter. The report groups every spike frame by its dominant hotspot (so one call shows the full distribution of what's slow, not one anecdote), on EFFECTIVE frame time (your own execute cost excluded). The snapshot is stored under label — drill into any cluster with frame <label> / hotspots <label>.
Parameters
secondsnumber(optional)labelstring(optional)spikeMsnumber(optional)
Returns string
-- the whole ring, clustered
8 -- just the last 8 seconds
8, "collapse" -- last 8s, stored as 'collapse' for drill-down
tools/profiler/ring
profiler.ring(action?: ("on" | "off" | "status"), seconds?: number) -> string
Control the retroactive ring buffer — an always-recording, bounded history of the last N seconds of per-frame data you query AFTER the fact with retro. ring("on", seconds) starts it (default 20s); ring("off") stops and clears it; ring("status") reports whether it's on, how many frames and seconds it holds. Editor profile only — a no-op in the runtime profile. The ring is off until you turn it on, so it costs nothing until then. This is the fix for "I can't profile a spike I only see afterwards".
Parameters
action("on" | "off" | "status")(optional)secondsnumber(optional)
Returns string
"on", 30 -- keep the last 30 seconds, always
"status" -- is the ring on? how much does it hold?
"off" -- stop recording and clear the history
tools/profiler/scripts
profiler.scripts(n?: number, minMs?: number, type_?: string) -> string
Rank components by update(dt) cost — the content view of where the frame's script time goes. Rolled up per component type by default (many instances of a type collapse to Type xN with summed cost; a lone instance keeps its @ entity), so it stays readable whether a world has three scripts or three hundred. Pass a type to drill into that one type's individual instances (which entity is the heavy one). Use it after hotspots/frame point at lua_update.vm_call. Counts COMPONENT update loops; a scene entrypoint's per-frame update / editorUpdate runs on the scheduler, and scene.cost ranks the loaded scenes by what theirs costs.
Parameters
nnumber(optional)minMsnumber(optional)type_string(optional)
Returns string
-- every component type, heaviest first
10 -- the 10 heaviest types
20, 0.5, "MyMover" -- instances of MyMover costing >= 0.5ms, by entity
tools/profiler/tasks
profiler.tasks(n?: number, minMs?: number) -> string
Rank components by the time the scheduler spent resuming their coroutines this frame — the content breakdown of task_scheduler. Rolled up per component type (many instances collapse to Type xN; a lone instance keeps its @ entity). Use this when frame/hotspots show task_scheduler hot and you need to know whose task.spawn / task.wait work is behind it.
Parameters
nnumber(optional)minMsnumber(optional)
Returns string
-- every component's coroutine cost, heaviest first
10 -- the 10 heaviest
tools/profiler/watch
profiler.watch(ceilingMs?: (number | "off" | "status"), mode?: ("record" | "pause"), excludeAgent?: boolean) -> string
Arm a frame-time watchdog that catches bad frames without you having to poll (which always lands seconds late). Call with a number to arm: watch(50) records every frame whose EFFECTIVE time (agent cost excluded) is >= 50ms; watch(50, "pause") instead pauses gameplay the first time the ceiling is crossed, freezing the bad state for you to inspect (then read it with retro). watch("off") disarms; watch("status") (or no arg) reports state and hit count. Read recorded hits with the hits tool. Editor profile only — returns a refusal in the runtime profile. excludeAgent (default true) keeps your own execute/write frames from tripping it.
Parameters
ceilingMs(number | "off" | "status")(optional)mode("record" | "pause")(optional)excludeAgentboolean(optional)
Returns string
50 -- record every frame over 50ms effective
50, "pause" -- pause gameplay the first time a frame exceeds 50ms
"status" -- armed? how many hits so far?
"off" -- disarm
tools/renderLayer/camera
renderLayer.camera(targets: Targets, filter?: string) -> { any }
Choose which render layers a camera DRAWS, as a layer-NAME filter — the visibility half of the render-layer system, and the way to keep a wall, prop or overlay out of a shot while it still exists for lighting, shadows and physics. A filter is a space-separated spec like "all", "all !ui", or "all !shell !EditorUI", where a bare name includes that layer and a !name excludes it; this is the same language capture and the Camera component already speak. Targets are entities carrying a Camera component, resolved by NAME or id, an array of them, scene.find records, or a QUERY ({ component = { "Camera" } } reaches every camera at once). Call with NO filter to read the current one back. An entity without a Camera component is an error. Returns { id, name, renderLayers } per camera, so the write is also the read. Use renderLayer.set to put geometry ON a layer — that is the other half; a camera can only exclude a layer something is actually on.
Parameters
targetsTargetsfilterstring(optional)
Returns { any }
"mainCamera", "all !shell"
"previewCamera", "all !ui !EditorUI"
{ component = { "Camera" } }, "all !debug"
"mainCamera"
tools/renderLayer/get
renderLayer.get(targets: Targets, opts?: GetOpts) -> { any }
Read which render layers one or many entities are on — what is actually drawn where, reported as layer NAMES rather than a bitmask. Targets resolve by entity NAME or id, an array of them, scene.find records, or a QUERY resolved for you ({ name = { "wall" } }, { component = { "Camera" } }, { under = { "building" } }) so you can inspect a selection without looking its ids up first. Pass tree = true to read each target AND every descendant, which is how you check a hierarchy a renderLayer.set with tree = true just wrote. Returns { id, name, layers, skipped } per entity read: layers is the array of layer names it carries — an entity nothing has re-layered reports default — and skipped counts the descendants this read left out, so reading a root of 46 children on its own reports skipped = 46 rather than reading like the whole building. An unrecognised option key is an error naming the accepted set. Use renderLayer.set to change membership, renderLayer.camera to see or change which layers a camera draws, and renderLayer.list to see every layer that exists and who is on it.
Parameters
targetsTargetsoptsGetOpts(optional)
Returns { any }
"frontWall"
{ "marker_a", "marker_b" }
{ under = { "building" } }
"building", { tree = true }
tools/renderLayer/list
renderLayer.list() -> any
Show every render layer that exists in this world, which entities are on each, and which cameras include or exclude it — the answer to "what layers do I have" and "why is nothing showing up on this one". Layers are reported in bit order with a builtin flag: default, ui, debug, sky and EditorUI are seeded, everything else interned the first time a name was referenced. This is the tool that makes a MISTYPED layer visible — referencing a name that does not exist CREATES it, so "shel" instead of "shell" yields a real but empty layer that renders nothing and raises no error, and listing shows it with no members. Returns { layers, cameras } where each layer is { name, bit, builtin, entities } and each camera is { id, name, renderLayers } carrying its name filter. Use renderLayer.set to change entity membership and renderLayer.camera to change what a camera draws.
Returns any
tools/renderLayer/screen
renderLayer.screen(screen: string, layers: LayerNames) -> any
Choose which render layers a UI screen DRAWS INTO, by layer NAME — the way to keep a HUD, overlay or editor panel out of a camera's shot or a screenshot while it keeps rendering elsewhere. A screen appears in a camera or capture only when its layers intersect that camera's include filter, the same rule geometry follows. Layers are given by NAME — a single name, an array of names, or a space-separated string like "debug ui" — never a bitmask. Content UI sits on ui by default and the editor places its chrome on EditorUI, which is why a capture asking for "all !EditorUI" drops the editor panels and keeps the game HUD. Returns { screen, layers } with the layers the screen now draws into. Use renderLayer.camera to choose what a camera renders, renderLayer.set for entity membership, and renderLayer.list to see every layer that exists.
Parameters
screenstringlayersLayerNames
Returns any
"hud", "ui"
"debugOverlay", { "debug", "ui" }
tools/renderLayer/set
renderLayer.set(targets: Targets, layers: LayerNames, opts?: SetOpts) -> { any }
Put one or many entities ON named render layers — the membership half of the render-layer system, and the way to hide a wall, prop or whole building from a camera while it still exists for lighting, shadows and physics. Targets resolve by entity NAME or id, an array of them, scene.find records, or a QUERY resolved for you ({ name = { "wall" } }, { component = { "Light" } }, { under = { "building" } }) so you can re-layer things without looking their ids up first. Layers are given by NAME — a single name, an array of names, or a space-separated string like "shell debug" — never a bitmask. Pass tree = true to apply to each target AND every descendant, which is what you want when the thing you are re-layering is a hierarchy rather than a single mesh. Setting layers REPLACES an entity's membership rather than adding to it. An unrecognised option key is an error naming the accepted set, so a mistyped key never reads back as a successful no-op. Referencing a layer name that does not exist yet CREATES it, so a typo yields a real but empty layer that renders nothing and raises no error — renderLayer.list shows what actually exists. Returns { id, name, layers, moved, skipped } per TARGET: layers is what that entity now carries, moved counts the entities this call wrote through it — an entity two targets both span is written and counted once, so the counts sum to the entities the call moved — and skipped counts the descendants it left where they were. A root of 46 children re-layered on its own reports moved = 1, skipped = 46; the same call with tree = true reports moved = 47, skipped = 0, so the size of the write is in the reply rather than in a follow-up read. Pair it with renderLayer.camera, which chooses which layers a camera draws.
Parameters
targetsTargetslayersLayerNamesoptsSetOpts(optional)
Returns { any }
"frontWall", "shell"
{ "marker_a", "marker_b" }, "debug"
{ name = { "wall" } }, "shell"
"building", "shell", { tree = true }
tools/sc/addComponent
sc.addComponent(ids: IdList, typeName: string, data?: ComponentData)
Add component to one or many entities.
Parameters
idsIdListtypeNamestringdataComponentData(optional)
"box", "Physics", { kind = "dynamic" }
ids, "Physics", { kind = "static" }
tools/sc/clear
sc.clear()
Clear scene (despawn all non-camera entities).
tools/sc/clearVisuals
sc.clearVisuals(ids: IdList)
Clear tint + outline from one or many entities.
Parameters
idsIdList
"box"
ids
tools/sc/despawn
sc.despawn(ids: IdList)
Despawn one or many entities.
Parameters
idsIdList
"box"
ids
tools/sc/move
sc.move(ids: IdList, dx: number, dy: number, dz: number)
Move entities by offset with smooth visual transition.
Parameters
idsIdListdxnumberdynumberdznumber
"box", 0, 1, 0
ids, 0, 5, 0
tools/sc/outline
sc.outline(ids: IdList, r: number, g: number, b: number, intensity?: number)
Set outline on one or many entities.
Parameters
idsIdListrnumbergnumberbnumberintensitynumber(optional)
"box", 1, 1, 0
ids, 1, 0, 0, 2
tools/sc/quantize
sc.quantize(ids: IdList, opts?: QuantizeOpts)
Snap transform to grid. Each axis is optional.
Parameters
idsIdListoptsQuantizeOpts(optional)
"box", { position = 0.5 }
ids, { rotation = 45 }
tools/sc/removeComponent
sc.removeComponent(ids: IdList, typeName: string)
Remove component from one or many entities.
Parameters
idsIdListtypeNamestring
"box", "Physics"
ids, "BoxCollider"
tools/sc/replaceWithAsset
sc.replaceWithAsset(target: string, source: AssetRef<bundle|mesh>, opts?: ReplaceWithAssetOpts) -> string
Replace an already-spawned placeholder entity with a real asset, in place — keeping its position, rotation, scale, name, and parent. Removes the placeholder (and its whole subtree) and spawns the asset in its slot. The everyday "I blocked this out, now drop in the generated (or library) asset" move — so a scene built from rough stand-ins becomes the real thing without re-laying-out anything.
Parameters
targetstringsourceAssetRef<bundle|mesh>optsReplaceWithAssetOpts(optional)
Returns string
"rider_blockout", "/zero/source/generated/meshes/a_snowboarder.bundle"
"tree_box", "@libname::pine.bundle", { keepScale = false }
tools/sc/setParent
sc.setParent(ids: IdList, parentId: string | EntityRef)
Parent one or many entities to a single parent.
Parameters
idsIdListparentIdstring | EntityRef
"hat", "player"
ids, "torso"
tools/sc/spawnCamera
sc.spawnCamera(name: string, ...) -> string
Spawn a camera at position.
Parameters
namestring
Returns string
"main_cam", 0, 5, 10, { fov = 60 }
tools/sc/spawnCircle
sc.spawnCircle(model: AssetRef<bundle|mesh>, name: string, count: number, opts?: CircleOpts) -> { string }
Spawn entities in a circle. Returns array of IDs.
Parameters
modelAssetRef<bundle|mesh>namestringcountnumberoptsCircleOpts(optional)
Returns { string }
"sphere", "pillar", 8, { radius = 10 }
tools/sc/spawnGrid
sc.spawnGrid(model: AssetRef<bundle|mesh>, name: string, cols: number, rows: number, opts?: GridOpts) -> { string }
Spawn a grid of entities. Returns array of IDs.
Parameters
modelAssetRef<bundle|mesh>namestringcolsnumberrowsnumberoptsGridOpts(optional)
Returns { string }
"cube", "tile", 5, 5, { spacing = 2, y = 0.5 }
tools/sc/spawnLine
sc.spawnLine(model: AssetRef<bundle|mesh>, name: string, count: number, opts?: LineOpts) -> { string }
Spawn entities in a line. Returns array of IDs.
Parameters
modelAssetRef<bundle|mesh>namestringcountnumberoptsLineOpts(optional)
Returns { string }
"cube", "wall", 10, { spacing = 1 }
"cube", "col", 5, { spacing = 1, dirY = 1 }
tools/sc/spawnModel
sc.spawnModel(name: string, source: AssetRef<bundle|mesh>, ...) -> string
Spawn entity with model at position. Includes static Physics by default unless opts.physics overrides it.
Parameters
namestringsourceAssetRef<bundle|mesh>
Returns string
"box", "plain_name", 0, 2, 0
"crate", "cube", 0, 0, 0, { scale = { 6, 3, 1 }, rotation = { 0, 45, 0 } }
"player", "@libname::identity.in.library", 0, 0, 0
"player", "@identity.of.asset", 0, 0, 0
"player", "path/to/asset.bundle", 0, 0, 0
tools/sc/spawnPhysics
sc.spawnPhysics(name: string, model: AssetRef<bundle|mesh>, ...) -> string
Spawn entity with model + dynamic physics. Shorthand for sc.spawnModel with physics = "dynamic".
Parameters
namestringmodelAssetRef<bundle|mesh>
Returns string
"ball", "sphere", 0, 5, 0
tools/sc/spawnText
sc.spawnText(name: string, content: string, ...) -> string
Spawn 3D text at position.
Parameters
namestringcontentstring
Returns string
"title", "Hello!", 0, 3, 0, { color = "yellow", fontSize = 48 }
tools/sc/tint
sc.tint(ids: IdList, r: number, g: number, b: number, blend?: number)
Set tint color on one or many entities.
Parameters
idsIdListrnumbergnumberbnumberblendnumber(optional)
"box", 1, 0, 0
ids, 0, 1, 0, 0.8
tools/sc/transform
sc.transform(ids: IdList, opts?: TransformOpts)
Set transform on one or many entities. All fields optional. Smooth visual transition by default.
Parameters
idsIdListoptsTransformOpts(optional)
"box", { position = {5, 0, 0} }
"box", { scale = 2, position = {0, 5, 0} }
ids, { scale = 0.5 }
ids, { pos = {0, 0, 0}, smooth = 0 }
tools/sc/unparent
sc.unparent(ids: IdList)
Unparent one or many entities (make roots).
Parameters
idsIdList
"hat"
ids
tools/scene/clean
scene.clean(opts?: CleanOpts) -> CleanResult
Clear the active scene down to a protected keep-set. By default ONLY the player setup survives — every PlayerPrototype subtree (with its body/camera) and every PlayerSpawn — so a clean never orphans or destroys the player (a joining user's avatar replaces the default body, so the player must persist). keep additionally protects a CATEGORY of entities: { component = { "Light", "Sky" } } keeps lighting, { tag = { "landmark" } } keeps tagged entities, { under = "rig" } keeps a subtree. It selects a group, not one named entity. keepPlayer = false wipes the player too (a full empty). dryRun = true reports what would be removed/kept without mutating. This is a LIVE mutation only — it does not commit to the saved scene, so scene.revert undoes it and scene.save commits it. If the scene has UNSAVED edits, clean REFUSES (it errors without changing anything): its removals would fold into those edits and no revert could restore the pre-clean state, so the unsaved work would be lost. There is no confirm/force flag — resolve the edits first with scene.save (keep them) or scene.revert (discard them), then clean. (The one exception is play mode running unpaused, where the dirty overlay is gated and untouched, so the guard does not apply.) Returns { removed, kept, intent }, plus a warning when the clean left the scene with no lights.
Parameters
optsCleanOpts(optional)
Returns CleanResult
-- clear everything except the player
{ keep = { component = { "Light", "Sky" } } } -- also keep lighting
{ keepPlayer = false } -- full empty
{ dryRun = true } -- preview only
tools/scene/cost
scene.cost(reset?: boolean) -> { [string]: any }
Rank the loaded scenes by what their per-frame tick costs — the answer to "which scene is eating the frame time". A scene entrypoint's update / editorUpdate runs on the scheduler rather than as a component, so it is absent from profiler.scripts; this times it where it runs and attributes it to the layer that owns it. avgMs is the per-tick cost, which is the per-frame cost for a tick that runs every frame; totalMs is a SUM across the window since the last reset (or engine start), which window reports. A layer whose entrypoint declares no tick is absent — it costs nothing per frame. Answers in edit mode as well as play.
Parameters
resetboolean(optional)
Returns { [string]: any }
true
tools/scene/create
scene.create(name: string, template?: ("empty" | "flat" | "player" | "menu"), opts?: { startup: boolean? }) -> CreateCatalog | CreateResult
Create a NEW scene from a template and load it. template is typed — omit it to get the catalog of templates and what each contains; an unknown value errors with the valid choices. "empty" is a blank canvas; "flat" is ground + lighting, no player; "player" is ground + lighting + the default player (the canonical, reusable setup a joining user's avatar replaces — prefer this over hand-rolling a player); "menu" is lighting only, no ground, no player (title screens / UI). Pass opts.startup = true to also make the new scene the world's startup scene. The scene is minted as a fresh asset and loaded — a name a scene already stands at is refused, naming the path it stands at, since writing a template over it would load that scene rather than a new one.
Parameters
namestringtemplate("empty" | "flat" | "player" | "menu")(optional)opts{ startup: boolean? }(optional)
Returns CreateCatalog | CreateResult
"level_1", "flat" -- ground + lighting, no player
"title", "menu", { startup = true } -- a menu scene the world opens into
"sandbox" -- returns the template catalog
tools/scene/find
scene.find(query: shared.FindQueryArg) -> shared.FindResult
Find entities across the whole live population — runtime clones, temporary, and detached-root entities included (it reads the flat entity query, not a scene-root walk). Pick a search axis, each taking a LIST so many candidates resolve in one call: id (exact entity ids — the ids this tool itself reports, so a batch of them resolves in one call), name (entity name terms), component (component names/identities — entities carrying any), attribute (an attribute name + optional value). Multiple axes = union; each hit reports WHICH criterion matched so you learn the right wording. A key the query does not recognise is an error naming the axes and scopes it takes, so a misspelled axis never reads back as the whole scene. substring (default true) matches name parts case-insensitively — a short term like "muzzle" reaches "muzzle_light_ent_620d…"; set false for exact. A name term carrying * or ? is matched as a GLOB over the whole name ("ik_*", "wall_?") — the same convention entity.find / entity.findAll take — in both modes, since * states a pattern either way. Entities marked internal are excluded unless includeInternal. Scope with roots (top-level only), maxDepth, under (only descendants of these entities), ancestorsOf (only ancestors of these), and additive (default false = active scene only; true also searches loaded overlay scenes). include makes the query ANSWER the question instead of handing back a list to interrogate one entity at a time: pass an array of projections and every match carries them — "transform" adds position / rotation / eulerAngles / scale, "componentValues" adds each component's LIVE field values (not just its name, and not the type's declared defaults), "attributes" adds the entity's attributes, "scriptComponents" adds every script-component instance the live entity carries. Nothing is added unless include names it, and an unrecognised projection is an error listing the valid ones. Returns every hit with its FULL canonical id + name.
Parameters
queryshared.FindQueryArg
Returns shared.FindResult
"muzzle"
{ id = { "ent_5ce7e0a14d3f9b07" } }
{ id = ids, include = { "transform" } }
{ name = { "muzzle", "flash" } }
{ name = { "body" }, substring = false }
{ component = { "Light", "Camera" } }
{ name = { "wheel" }, under = { "car" } }
{ attribute = { name = "team", value = "red" } }
{ roots = true }
{ name = { "wall" }, include = { "transform" } }
{ component = { "Light" }, include = { "transform", "componentValues" } }
{ under = { "room" }, include = { "transform", "componentValues", "attributes", "scriptComponents" } }
tools/scene/list
scene.list(scope?: SceneListScope) -> SceneListResult
List the SCENES available to load — the world's own scenes and the builtin library scenes — so you can find a scene by name instead of guessing. scope filters: "world" (default, the scenes this world authored), "library" (builtin @builtin:: scenes — templates, demos, canonical setups), or "all". Each entry reports its name, identity, path, scope ("world"/"library"), and whether it is currently loaded / active. Load one with scene.load(name); the currently loaded LAYERS are scene.loadedLayers.
Parameters
scopeSceneListScope(optional)
Returns SceneListResult
-- the world's own scenes
"library" -- builtin templates / demos / canonical scenes
"all"
tools/scene/load
scene.load(name: string) -> LoadResult
Load a scene into the live world and make it the active scene. name is a scene NAME (see scene.list), a builtin identity (e.g. "@builtin::scenes.test_arena"), or a scene path. Loading a scene that is ALREADY loaded changes nothing — it is idempotent — so this is NOT how you re-run an edited scene: use scene.reload for that. Records name so a later no-arg scene.save() targets it.
Parameters
namestring
Returns LoadResult
"main"
"@builtin::scenes.test_arena"
tools/scene/loadedLayers
scene.loadedLayers() -> { LoadedLayer }
The scene LAYERS loaded into the live scene right now — the root scene plus any additive overlays (editor UI, effect passes). Each layer is reported as { name, scene, status, visible, additive, root }: scene is the backing scene identity, status the load state, additive false marks the active ROOT scene and true an overlay layered over it. This is what is composing the live scene; to list the scenes you can LOAD (world + library), use scene.list.
Returns { LoadedLayer }
tools/scene/migrate_v6_to_v7
scene.migrate_v6_to_v7(opts: MigrateOpts) -> MigrateResult
Convert a v6 scene.json to v7 in place. Reads the scene's scene.json, decides the player intent from the v6 player.required_in_play flag ("spawns" when true, else "none"), removes the v6 player{}/camera{} blocks, sets the top-level string player intent, and — for "spawns" — injects the final-shape PlayerSetups/Spawns structure with per-scene-unique entity ids. format, lighting, and every existing entity are preserved. Idempotent: a scene already at version >= 7 returns "skipped"; a scene with a missing or > 7 version returns an "error" result without mutating. Also scans the sibling entrypoint.luau for references to the removed player/camera flow and returns them as review notes (the Luau is never rewritten).
Parameters
optsMigrateOpts
Returns MigrateResult
{ path = "/source/libs/@builtin/demos/minecraft/minecraft.scene/scene.json" }
tools/scene/observe
scene.observe() -> { [string]: any }
What every scene load did, and what each loaded scene costs in frame time. lastLoad is the most recent load's report: the scene it loaded, the root it replaced and the overlays that went with it, the entity counts on each side, the milliseconds each phase took, and every failure it produced. layers is one row per loaded layer with what the engine attributes to it and whether it came up whole; cost is the per-frame tick cost of each layer whose entrypoint declares one, summed across the window window reports. A load that produced failures reports outcome = "partial" and a reason from a closed set. Answers in edit mode as well as play mode.
Returns { [string]: any }
tools/scene/player
scene.player(intent?: ("spawns" | "none"), roles?: PlayerRoles) -> PlayerReport
The scene's player setup, in one tool. No argument reports the intent plus every PlayerPrototype (with body/camera wiring) and PlayerSpawn. intent is typed: "none" makes the scene playerless — every PlayerPrototype subtree and PlayerSpawn is removed; "spawns" gives it a player per joining user, authoring the canonical default setup (humanoid body, third-person camera, origin spawn) when the scene has no PlayerPrototype yet. Pass roles (implies "spawns") to reshape: body adopts an entity OR repoints the body's avatar from an avatar asset ref; camera wires an entity carrying a Camera (scoped OwnerOnly); behavior sets the player camera's Camera.behavior to any cameraBehavior component (built-in or your own); prototype picks one when the scene has several. Reuse this default setup — a joining player's avatar replaces the default body, so a hand-rolled player breaks multiplayer. Setting anything saves the scene.
Parameters
intent("spawns" | "none")(optional)rolesPlayerRoles(optional)
Returns PlayerReport
-- report the current player setup
"none" -- strip the player: no prototype, no spawns
"spawns" -- player per joining user (authors the canonical setup if missing)
"spawns", { body = "@builtin::avatars.minimal_player" } -- swap the avatar
"spawns", { behavior = "@builtin::controller.first_person" } -- first-person player
tools/scene/reload
scene.reload(name?: string) -> ReloadResult
Unload and re-load a scene layer in place, running the scene's build.luau and its entrypoint again. scene.load of a scene that is already loaded and unchanged is an idempotent no-op, so editing either of them and loading it again rebuilds nothing; this re-runs them. The build runs against what it resolves right now, so a script whose inputs moved — a component that now exists, an asset that now resolves — produces the scene it describes today. Works in edit mode, so iterating on a build script does not mean flipping to play and back through the play shadow.
Parameters
namestring(optional)
Returns ReloadResult
"main"
tools/scene/remove
scene.remove(targets: Targets) -> RemoveResult
Remove entities from the active scene. Targets are entity NAMES or ids, arrays of them, scene.find records, or a scene.find QUERY ({ name = { "ground" } }, { component = { "Light" } }, { under = { "rig" } }) — so you delete a selection without looking it up first. Each target is removed with its whole subtree. Removing the default floor is scene.remove("ground"). This is a LIVE mutation only — it does not commit to the saved scene, so scene.revert undoes it and scene.save commits it. To clear a scene while keeping the player setup, use scene.clean instead.
Parameters
targetsTargets
Returns RemoveResult
"ground" -- delete the default floor
{ "probe_a", "probe_b" }
{ component = { "PointLight" } } -- delete every entity with that component
tools/scene/replace
scene.replace(source: string, opts?: ReplaceOpts) -> ReplaceReport
Replace a loaded scene's content with another scene's content. source is a scene name / path / identity string, resolved internally (the resolved identity is reported back). Every content file in the target scene's folder (scene.json, entrypoint.luau, ...) is overwritten with the source's copy; target content files the source doesn't have are removed; the target scene's identity (guid, name) is untouched. The scene then reloads so the world reflects the new content immediately. Targets the ACTIVE scene unless opts.scene names another loaded scene. dryRun = true reports exactly which files would be written and removed, and the entities the source declares, without touching anything.
Parameters
sourcestringoptsReplaceOpts(optional)
Returns ReplaceReport
"@builtin::scenes.test_arena"
"my_template", { dryRun = true }
tools/scene/revert
scene.revert(name?: string) -> RevertResult
Discard the active scene's UNSAVED edits and restore its saved scene.json. Every edit-mode change since the last scene.save — a scene.clean, a scene.remove, spawned/moved/deleted entities — lives in the scene's dirty overlay until you commit it; this throws that overlay away and respawns the scene from its saved state. It is the undo for scene.clean / scene.remove and the counterpart to scene.save. Pass name to revert a specific loaded scene; omit it for the active one. Reverts only the edit-mode dirty overlay — it is not the play-mode baseline restore.
Parameters
namestring(optional)
Returns RevertResult
-- undo unsaved edits on the active scene
"main" -- revert the loaded scene named 'main'
tools/scene/save
scene.save(name?: string) -> SaveResult
Save the loaded scene named name back to its own scene.json, writing the path synchronously. Resolves the scene by name via layers.find and saves THAT scene — so saving scene A never writes scene B. No-args form uses the toolbox's last scene name (default "main"). Errors when no scene by that name is loaded.
Parameters
namestring(optional)
Returns SaveResult
-- save the last-used scene (or "main")
"main" -- save the loaded scene named 'main'
tools/scene/setStartup
scene.setStartup() -> SetStartupResult
Make the CURRENTLY ACTIVE scene the world's startup scene — the scene the world auto-loads every time it loads. Takes no arguments: it pins whatever scene is live now, so after building or loading a scene you call this to make it stick across world reloads instead of the world snapping back to its previous startup. There is no clear/unset — a world with no startup scene has no scene context and every script fails. The current startup scene shows in scene.summary.
Returns SetStartupResult
-- pin the active scene as the world's entry point
tools/scene/spawns
scene.spawns(action?: SpawnAction, opts?: SpawnOpts) -> SpawnsResult
The scene's spawn points, in one tool — where joining players arrive, as opposed to scene.player, which decides whether the scene spawns players at all and what they become. No argument reports every PlayerSpawn with its world transform, the prototype it instantiates (and whether that reference still resolves), and its team / role / maxPlayers / spawnPolicy / placement. "add" places a new spawn: it takes the prototype named in opts.prototype, or the scene's only one, and is parented alongside the spawns already there so spawn points stay collected. "set" reconfigures an existing spawn — every field is optional and the ones you leave out keep their value. "remove" deletes one. opts.spawn names WHICH spawn ("set" / "remove"), or the new entity's name ("add"); with several spawns in the scene it is required, with one it is implied. A spawn's own transform is where a player's body lands while placement is at_spawn_transform, and its forward axis is the direction they face — the edit-mode overlay draws that ring, heading arrow and standing capsule so a spawn is visible where it stands. Mutating saves the scene.
Parameters
actionSpawnAction(optional)optsSpawnOpts(optional)
Returns SpawnsResult
-- report every spawn point in the scene
"add", { position = { 12, 0, -4 } } -- a second spawn over there
"add", { spawn = "RedSpawn", position = { -8, 0, 0 }, rotation = { 0, 90, 0 }, team = "red" }
"set", { spawn = "DefaultSpawn", position = { 0, 0, 6 } } -- move it
"set", { spawn = "RedSpawn", maxPlayers = 4 }
"remove", { spawn = "RedSpawn" }
tools/scene/summary
scene.summary() -> SceneSummary
Scene overview: entity + root counts, the root list, the active camera, the PLAYER setup (intent + prototype/spawn counts, with the verb to change it), and the world's STARTUP scene. The player block is how you learn a scene's player type without knowing it exists — intent is "spawns" (a player per joining user) or "none" (playerless), changed with scene.player("spawns" / "none").
Returns SceneSummary
tools/scene/tree
scene.tree(target?: TreeTarget, opts?: TreeOpts) -> TreeResult
Render the entity hierarchy as an indented tree — the quick "what is the current structure" view. Shows each entity's name, its component types, and its children down to depth levels; anything deeper (or beyond the node budget) is summarized as a count instead of silently dropped. With no target it draws the whole active world from its roots; a target (id, name, proxy, or an array) draws just those subtrees.
Parameters
targetTreeTarget(optional)optsTreeOpts(optional)
Returns TreeResult
'player'
nil, { depth = 2 }
tools/scene/whyPartial
scene.whyPartial(scene?: string) -> { [string]: any }
Answer why a loaded scene layer is not whole. reason is the nearest cause from a closed set — loaderRaised, entrypointCompileFailed, entrypointBodyRaised, entrypointRaised, buildRaised, entityFailed, parentMissing, parentRefused, parentAbandoned, componentUnresolved, componentRefused, subscriberRaised, updateRaised — so it names the thing to fix. failures carries every one of them, each with the entity, component identity or lifecycle hook it is about and the engine's own message. A layer that produced everything its scene declared reports ok = true with no reason.
Parameters
scenestring(optional)
Returns { [string]: any }
"scenes.main"
tools/sceneAuthoring/acceptChanges
sceneAuthoring.acceptChanges(selection?: shared.Selection, opts?: AcceptOpts) -> AcceptResult
Accept changes shown by tools.use("sceneAuthoring", "changes") — all of them, or a selection. Writes the scene, saves the play-created assets the accepted entities reference into the world's source, saves play-edited source files when included (group "edits"), and resumes the engine. A partial accept leaves the rest live — the next changes() shows the remainder. If another editor changed a record since the review was taken, the accept ABORTS with a conflict report (nothing written) unless opts.onConflict is "mine" or "theirs".
Parameters
selectionshared.Selection(optional)optsAcceptOpts(optional)
Returns AcceptResult
{ "tile_", "hero", "edits" }
"my_component/init.luau"
nil, { onConflict = "mine" }
tools/sceneAuthoring/changes
sceneAuthoring.changes(opts?: ChangesOpts) -> string
Review what exists live but is not yet part of the scene: entities you spawned (clustered — one spawn batch is one decision), edits to existing scene entities (with field-level diffs), the assets those changes reference (flagging play-created ones and reload-broken orphans), source files edited during play, and session post-process effects and UI screens. Pauses the engine at the exact moment of the call so the review is a frozen snapshot; writes NOTHING. The scene only ever changes via tools.use("sceneAuthoring", "acceptChanges"); drop changes with tools.use("sceneAuthoring", "rejectChanges").
Parameters
optsChangesOpts(optional)
Returns string
{ as = "checkpoint_1" }
tools/sceneAuthoring/rejectChanges
sceneAuthoring.rejectChanges(selection?: shared.Selection) -> RejectResult
Reject changes shown by tools.use("sceneAuthoring", "changes") — all of them, or a selection. Rejected entity changes never enter the scene; the live objects stay live for this session and are hidden from later reviews. Each rejected change is taken back out of the scene's pending set, so the scene stops carrying it and the publication gate that refuses a commit over unbaked entity edits reads it as settled. A rejected REMOVAL keeps the entity in the scene: the despawn holds for this session and the entity is there again on the next load. Rejected file edits (group "edits", or individual paths) are REVERTED: each file is restored to its pre-play state (the last edit-mode content, unstaged edits included; a file created during play is removed), hot-reload picks the original back up, and the edit no longer blocks leaving play. Resumes the engine.
Parameters
selectionshared.Selection(optional)
Returns RejectResult
"tile_"
"my_component/init.luau"
tools/sceneAuthoring/reviewState
sceneAuthoring.reviewState() -> ReviewState
What is under review right now, and how much of it: the entities the scene does not have or holds differently, the ones despawned this session, the source files edited during play, and the effects, screens and features registered from execute. On an engine several agents drive at once, edits counts this agent's own in-play source edits and foreignEdits counts a co-author's — both hold the play-exit gate, and only the first is what a bare acceptChanges or rejectChanges settles. The counts come from the same survey of the live world tools.use("sceneAuthoring", "changes") renders and the leave-play safeguard enforces, so an agent deciding whether it has work to keep reads the same answer all three give. open is true while anything is pending, which is when acceptChanges and rejectChanges have something to settle. Every count is zero and open is false once a verdict has settled the lot, and while no scene is loaded as the root layer. The engine keeps running across the call.
Returns ReviewState
-- branch instead of relying on the refusal
-- local s = tools.use("sceneAuthoring", "reviewState")
-- if s.open then tools.use("sceneAuthoring", "changes") end
tools/services/attach
services.attach(jobId: string) -> AttachedJob
Read a generation's gateway job by its jobId (the jobId on a services.status / services.jobs row). A submitted job is kept on the gateway, with its result, for hours after it finishes — longer than the engine that started it is guaranteed to last — so this reaches work that was already paid for once the run that started it is gone. Returns { jobId, status, result?, error? }: status is pending / running / succeeded / failed, and a succeeded job's result is the provider's response — the URL or bytes the run would have downloaded. Costs nothing; the charge happened at submit.
Parameters
jobIdstring
Returns AttachedJob
"01M12BH4P0C1CVXMN3QX9JEJKZ"
tools/services/balance
services.balance() -> (BalanceReport?, string?)
What you can spend on a generation. Compare an operation's cost (from services.list) against spendable — that is the binding number. pool is the account's whole balance and agentRemaining is what is left of your own allocation when you work under one; whichever is smaller is what spendable reports, because an allocation that is spent stops a generation however large the pool is. Returns (report, nil) when signed in, (nil, reason) otherwise.
Returns (BalanceReport?, string?)
tools/services/generate
services.generate(service: string, input?: GenerateInput, operation?: string) -> GenerateHandle
Start a generation. service is a name from services.list; input is that operation's inputs (e.g. { prompt = "a wooden treasure chest" }). Returns immediately with { id, service, operation } — generation takes minutes and runs in the background, surviving this call. Track it with services.status(id) across turns until status == "completed", then spawn the row's asset. Consumes credits (see the operation's cost in services.list).
Parameters
servicestringinputGenerateInput(optional)operationstring(optional)
Returns GenerateHandle
"mesh_gen", { prompt = "a wooden chest" }
"audio_gen", { operation = "sfx", prompt = "a heavy wooden door slamming" }
"audio_gen", { text = "Welcome aboard." }, "speech"
"image_gen", { prompt = "a mossy stone idol", asset_path = "/zero/source/game/art/idol.png" }
tools/services/jobs
services.jobs() -> { JobStatus }
List every generation job this session has started — active and finished, newest first. Each row is the same shape as services.status: { id, service, operation, prompt, status, progress, asset, error }. The live view of what's running and where the finished ones landed. (For the durable, cross-session record of generated assets, use services.outputs.)
Returns { JobStatus }
tools/services/list
services.list() -> { ServiceEntry }
List the generation services you can use — the things you can generate (3D meshes, textures, audio, …) by spending credits. Fully self-describing: each service entry is { name, description, default, operations }, and each operation is { name, description, inputs, produces, cost } — inputs is an ordered array of { name, type, required, desc, framework }, produces is the kind of asset you get back, cost is credits per call. An input marked framework = true belongs to the call rather than to the operation: operation names which of the service's operations to run (required where the service declares no default), and asset_path names where the run writes what it generates. Every input listed here — framework or declared — goes in the one input table. An entry's default is the operation a call runs when it names none, so a service listing several operations says which of them a bare call reaches. The entry point: call this first, then services.generate(service, input) with what you learn here.
Returns { ServiceEntry }
tools/services/outputs
services.outputs() -> { GeneratedOutput }
List the assets generated by services in this world — the durable record that survives restarts (the in-flight job records in services.jobs do not). Reads the provenance stamped on each produced asset. Each row: { asset, service, operation, prompt, generatedAt }, where asset is the spawnable asset path. Use this to find what you've generated across sessions.
Returns { GeneratedOutput }
tools/services/status
services.status(id: string) -> JobStatus?
Read one generation job's current status by its handle id (the id from services.generate). Returns { id, service, operation, prompt, status, progress, asset, error } or nil if the id is unknown. status moves through the operation's stages to "completed" or "failed"; asset is the spawnable asset path once "completed". Poll this across turns — the generation keeps running in the background regardless, so each call is quick.
Parameters
idstring
Returns JobStatus?
g.id
tools/skills/invoke
skills.invoke(name: string) -> string
Open a skill and return everything needed to act on it: its instructions, the assets and guides it depends on (each marked present or missing in this world), the toolboxes and tool names it runs through with their live signatures, and the subskills available under it. Address a top-level skill by name ("scenes") and a subskill through its parent ("scenes/player-setup"). A name that matches nothing returns the available names. The skill stays open afterwards — it and its subskills ride your tool responses, each marked as you open it — until skills.release puts it down.
Parameters
namestring
Returns string
"scenes"
tools/skills/list
skills.list(scope?: ("builtin" | "world" | "library")) -> SkillListing
List the skills this world knows — every top-level skill with the one-line description it advertises itself by, ordered built-in first then by name. Each row's name is the address skills.invoke takes, and open says whether you already have that skill open. Subskills are not listed: they are reachable only through their parent, and the parent's invoke result names them. open at the top level carries every address you have open, subskills included.
Parameters
scope("builtin" | "world" | "library")(optional)
Returns SkillListing
tools/skills/release
skills.release(name?: string) -> ReleaseResult
Put a skill down: stop it and its subskills riding your tool responses. Releasing a parent releases the subskills opened under it, since those are passes within the same job. Pass no name to release everything you have open. Naming a skill that is not open returns the ones that are.
Parameters
namestring(optional)
Returns ReleaseResult
"scenes"
tools/temp/run
temp.run(code: string, duration?: number) -> RunResult
Execute Luau code and auto-cleanup the entities it creates after a timeout. Snapshots existing entity IDs via ecs.entities() before running the code, compiles the code with loadstring, executes it under pcall, diffs the post-execution entity set against the pre-set to find newly-created entities, then schedules a deferred cleanup pass that despawns each created entity. Useful for temporary visualizations, test setups, and previews that should not leak.
Parameters
codestringdurationnumber(optional)
Returns RunResult
'sc.spawnModel("test", "sphere", {0, 3, 0})'
[[ sc.spawnGrid("cube", "preview", 3, 3, { spacing = 2, y = 1 }) ]], 10
tools/tests/compare
tests.compare(before: string | Report, after?: (string | Report)) -> Diff
Diff two test reports and report NEW failures (regressions), fixes, and added/removed tests. The headline is newFailures — tests failing now that passed (or didn't exist) in the baseline.
Parameters
beforestring | Reportafter(string | Report)(optional)
Returns Diff
"/source/before.json"
"/source/before.json", "/source/after.json"
tools/tests/list
tests.list(opts?: (ListOpts | string)) -> ListResult
List discovered test suites (registrable .testSuite assets). Scoped to scope = "user" (world-authored) by default — the same default as tests.run; pass scope = "all" to include the baked @builtin library. Optionally load each suite to also report its test count.
Parameters
opts(ListOpts | string)(optional)
Returns ListResult
{ filter = "vfs", details = true }
tools/tests/loadHttp
tests.loadHttp(opts?: LoadOpts) -> (LoadResult | LoadHandle)
Fetch engine .testSuite assets over HTTP and materialize them into the @builtin library so tests.list / tests.run discover them. For the browser/WASM test runner, where the suites are not embedded in the binary.
Parameters
optsLoadOpts(optional)
Returns (LoadResult | LoadHandle)
-- browser default: async task
{ async = false } -- block, return counts
{ suites = { "effects_core" } } -- just this suite
tools/tests/run
tests.run(opts?: (RunOpts | string)) -> (RunResult | AsyncHandle)
Run the engine test suite. A bare tests.run() runs every world-authored suite (scope = "user", the default) — never the baked @builtin library a content session can't edit; tests.run("*") runs EVERYTHING (scope "all"); tests.run("name") runs one suite (an explicit name bypasses scope); a table form { suite?, suites?, scope?, save?, format?, quiet?, async?, measurements? } gives full control. Each suite runs through its own AssetRef:run() inside a task, so the engine stays responsive. Always writes /source/tmp/test_results.{md,json} (under /source/tmp/, which is excluded from world saves, so reports never sync). With async = true the whole run is spawned as one task and the call returns { taskId, reportMd, reportJson } immediately — watch /runtime/tasks/{completed,failed}/<taskId> and read the report when it lands. Refused in play mode.
Parameters
opts(RunOpts | string)(optional)
Returns (RunResult | AsyncHandle)
"*"
"vfs"
{ async = true } -- watch /runtime/tasks/<h.taskId>
tools/voxelConfig/all
voxelConfig.all() -> { [string]: any }
Return a deep copy of the fully-merged voxel-engine config — built-in defaults, overlaid with WORLD_SETTINGS.[voxel].*, overlaid with runtime overrides installed via voxelConfig.set. Mutating the result has no effect on live state.
Returns { [string]: any }
tools/voxelConfig/get
voxelConfig.get(key: string) -> any
Read a voxel-engine config value by dotted key. Resolution order: runtime overrides first, then WORLD_SETTINGS.[voxel].*, then built-in defaults. Raises if the key is unknown.
Parameters
keystring
Returns any
"paths.shapes"
"chunkSize"
tools/voxelConfig/reset
voxelConfig.reset(key?: string)
Clear one or all runtime overrides installed via voxelConfig.set. With a key, clears just that key; with no argument, clears every runtime override. Raises if key is a string but not a known key.
Parameters
keystring(optional)
"paths.shapes" -- revert a single override
-- revert every runtime override
tools/voxelConfig/set
voxelConfig.set(key: string, value: any)
Install a runtime override for a voxel-engine config key. Runtime overrides take precedence over WORLD_SETTINGS.[voxel] and built-in defaults. Raises if the key is unknown.
Parameters
keystringvalueany(optional)
"paths.shapes", "/my/game/voxel/props"
"chunkSize", 16
tools/voxelEngine/composite
voxelEngine.composite(opts: CompositeOpts)
Kitbash new props from existing templates. Builds a composite with per-child offset + rotation, then saves in one of three forms: (a) flattened .voxbin via op='saveBaked', (b) live entity hierarchy via op='explode', (c) reusable asset bundle via op='saveAsBundle'. Handles are plain Lua tables returned by op='new'; they carry their own methods (:addChild, :saveBaked, :explode, ...) so callers can compose fluently or route every step through this tool.
Parameters
optsCompositeOpts
{ op = 'new', name = 'car' }
{ op = 'addChild', handle = c, template = 'car_body' }
{ op = 'addChild', handle = c, template = 'car_wheel', offset = {2, 0, 2} }
{ op = 'saveBaked', handle = c }
{ op = 'saveAsBundle', handle = c, bundleName = 'my_vehicle' }
tools/voxelEngine/destroy
voxelEngine.destroy(target?: (Handle | string))
Despawn a voxel entity (world or shape). Single-purpose generic tool — target is a handle, "world", an entity id, or an entity name. Shape handles return their componentName = "VoxelShape"; world target "world" resolves the singleton.
Parameters
target(Handle | string)(optional)
'world'
myShapeHandle
'ent_42'
tools/voxelEngine/edit
voxelEngine.edit(opts: EditOpts) -> any
Generic mutating block operations on worlds or shapes. One tool dispatches to every write op — setBlock, fills, mirror, translate, pasteRegion, flush, clear. Pick behavior via opts.op, target via opts.target. Target is resolved via shared.resolveTarget and defaults to the singleton VoxelWorld.
Parameters
optsEditOpts
Returns any
{ op = 'set', target = 'world', x = 0, y = 0, z = 0, block = 'stone' }
{ op = 'fillBox', target = 'myShape', min = {0,0,0}, max = {3,3,3}, block = 'red' }
{ op = 'mirror', target = shape, axis = 'x' }
tools/voxelEngine/palette
voxelEngine.palette(opts: PaletteOpts) -> any
Palette operations for voxel worlds or shapes. One tool dispatches to set / add / list. Target is resolved via shared.resolveTarget and defaults to the singleton VoxelWorld.
Parameters
optsPaletteOpts
Returns any
{ op = 'set', target = 'world', palette = { grass = {0.2,0.7,0.2}, dirt = {0.5,0.3,0.15} } }
{ op = 'add', target = myShape, name = 'gold', color = {1.0,0.9,0.2} }
{ op = 'list', target = 'world' }
tools/voxelEngine/persist
voxelEngine.persist(opts: PersistOpts) -> any
Voxbin + template lifecycle ops. One tool dispatches to save / bake / saveAsTemplate / deleteTemplate / copyTemplate / captureArea. Target is resolved via shared.resolveTarget where applicable.
Parameters
optsPersistOpts
Returns any
{ op = 'saveAsTemplate', target = myShape, name = 'redcube' }
{ op = 'captureArea', box = { min={0,0,0}, max={16,4,16} }, saveAs = 'scene1' }
{ op = 'deleteTemplate', name = 'obsolete' }
tools/voxelEngine/query
voxelEngine.query(opts: QueryOpts) -> any
Generic read-only query operations on voxel worlds or shapes. One tool dispatches to every read op — block lookup, neighbors, region iteration, count by name, raycast, sphere/line queries, region copy, bounds, list templates. Pick behavior via opts.op. Target is resolved via shared.resolveTarget and defaults to the singleton VoxelWorld.
Parameters
optsQueryOpts
Returns any
{ op = 'getBlock', target = 'world', x = 0, y = 0, z = 0 }
{ op = 'raycast', target = 'world', origin = {0,10,0}, dir = {0,-1,0}, maxDist = 100 }
{ op = 'listTemplates' }
tools/voxelEngine/spawn
voxelEngine.spawn(opts: SpawnOpts) -> Handle | { Handle }
Generic voxel entity creation + placement. One tool, many scenarios — pick behavior via opts.op. Covers world creation, single-shape creation, template instantiation, handle cloning, bulk scatter across a region, and single stamped placement. Returns a handle (or array of handles for scatter).
Parameters
optsSpawnOpts
Returns Handle | { Handle }
{ op = 'world', size = {32,16,32}, palette = { grass = {0.2,0.7,0.2} } }
{ op = 'shape', size = {4,4,4}, palette = { red = {1,0,0} }, position = {0,2,0} }
{ op = 'template', name = 'redcube', position = {10,0,0} }
{ op = 'scatter', template = 'redcube', region = { box = { min={0,0,0}, max={20,0,20} } }, count = 8, seed = 42 }
tools/voxelEngine/transform
voxelEngine.transform(opts: TransformOpts) -> any
Generic entity-transform + snap operations for voxel entities (worlds or shapes). One tool dispatches to setPosition / setRotation / setScale / setParent / snapToWorld plus getter ops getPosition / getRotation. Target is resolved via shared.resolveTarget and defaults to the singleton VoxelWorld.
Parameters
optsTransformOpts
Returns any
{ op = 'setPosition', target = myShape, x = 5, y = 0, z = 5 }
{ op = 'setRotation', target = myShape, x = 0, y = 1.57, z = 0 }
{ op = 'snapToWorld', target = myShape }
tools/wld/edit
wld.edit() -> EditResult
Return to edit mode. Leaving play would lose live changes the scene doesn't carry, so the engine's safeguard refuses while any of them are unaccepted, and names what they are. List them with the sceneAuthoring.changes tool and settle them with sceneAuthoring.acceptChanges or sceneAuthoring.rejectChanges — either takes everything or a selection — then call this again.
Returns EditResult
tools/wld/hideLayer
wld.hideLayer(name: string) -> ()
Hide a scene layer. Keeps the layer's entities in the world (they are not despawned) but stops them rendering. Use wld.showLayer to make them visible again, or wld.unloadLayer to despawn them.
Parameters
namestring
Returns ()
"ui"
tools/wld/info
wld.info() -> WorldInfo
Return a snapshot of the current world's high-level state — the bound world name, current mode ("edit" / "play"), the active scene layer, and the list of saved scene snapshots in this world.
Returns WorldInfo
tools/wld/listWorlds
wld.listWorlds() -> { string }
List worlds saved on disk. Reads /zero/worlds via the VFS and returns each directory entry's name. Empty array if no worlds have been persisted.
Returns { string }
tools/wld/loadLayer
wld.loadLayer(name: string, opts?: LoadLayerOpts) -> SceneLayer
Load a scene as an additive layer alongside the current scene. Thin wrapper over layers.load(name, opts).
Parameters
namestringoptsLoadLayerOpts(optional)
Returns SceneLayer
"ui"
"props", { origin = {0, 0, 10} }
tools/wld/mode
wld.mode() -> "edit" | "play"
Get the engine's current mode. Returns "edit" when in edit mode (scripts + physics paused), "play" when in play mode. Inverse pair: wld.play / wld.edit.
Returns "edit" | "play"
tools/wld/play
wld.play() -> ()
Enter play mode. Delegates to engine.mode = "play".
Returns ()
tools/wld/promoteAndSwitch
wld.promoteAndSwitch(mode: "edit" | "play") -> PromoteAndSwitchResult
Promote pending play-mode source-file edits (everything accepting the "edits" group would promote), then switch the engine to mode — one call replacing the changes() / acceptChanges("edits") / wld.edit()-or-wld.play() sequence. With zero pending edits the promote is a no-op and the mode switch still happens. Entity changes are untouched: a flip to edit with unaccepted entity changes stops at the leave-play safeguard, exactly as wld.edit() does.
Parameters
mode"edit" | "play"
Returns PromoteAndSwitchResult
"edit"
"play"
tools/wld/showLayer
wld.showLayer(name: string) -> ()
Show a previously-hidden scene layer. Restores rendering of the layer's entities. Inverse of wld.hideLayer.
Parameters
namestring
Returns ()
"ui"
tools/wld/unloadLayer
wld.unloadLayer(name: string)
Unload an additive scene layer. Despawns the layer's entities entirely — to keep the entities but stop rendering them, use wld.hideLayer instead.
Parameters
namestring
"props"
tools/workflow/answer
workflow.answer(request: string, value: any) -> boolean
Answer a request a workflow is waiting on. The run resumes from where it parked and carries on with whatever it decides comes next. Pass the value the request asked for — where the request carried a schema, that is the shape it expects back.
Parameters
requeststringvalueany(optional)
Returns boolean
{ request = "call-f6641ff8", value = { title = "Tornado Run", ready = true } }
tools/workflow/fail
workflow.fail(request: string, reason: string) -> boolean
Tell a workflow that a request cannot be answered, and why. The run decides what that means — a stage may stop, or carry on without it. Use this rather than leaving a claim unanswered: an abandoned claim leaves the run parked until the claim expires.
Parameters
requeststringreasonstring
Returns boolean
{ request = "call-f6641ff8", reason = "the world it names does not exist" }
tools/workflow/list
workflow.list() -> { { [string]: any } }
List the workflows in this world: what each one is for, when to reach for it, the phases it moves through, and what it expects in args. Start one by name with workflow.start. A job with a workflow is a job whose shape somebody already worked out — look here before deciding to do a multi-step job by hand.
Returns { { [string]: any } }
tools/workflow/next
workflow.next(runner: string, run?: string) -> { [string]: any }?
Take the next request a workflow run is waiting on, claimed for you in the same call. Returns nothing when no workflow is waiting.
Parameters
runnerstringrunstring(optional)
Returns { [string]: any }?
"me"
tools/workflow/start
workflow.start(name: string, args?: { [string]: any }) -> { [string]: any }
Start a workflow by name. The workflow decides what happens and in what order; your job is to answer the questions it stops to ask, with workflow.next and workflow.answer. Returns the run id — the run is already going, and its first request arrives on your next tool response.
Parameters
namestringargs{ [string]: any }(optional)
Returns { [string]: any }
"lighthouse"
{ name = "lighthouse", args = { instance = "wfrt" } }
tools/workflow/status
workflow.status(run?: string) -> any
Report what workflow runs are doing: which phase each is in, what it is waiting on and who claimed it, how many questions it has asked and had answered, and what it returned once it is done. Give a run id for one run, or nothing for all of them.
Parameters
runstring(optional)
Returns any
"run-098c453d"
tools/worldValidation/check
worldValidation.check(opts?: CheckOpts) -> ZmToolResult
Validate YOUR world's authored content — every script and asset under /source/ EXCEPT /source/libs/. The cargo-check equivalent for a Zero world. Imported libraries and engine builtins are NOT scanned by default (they're not yours to validate, and walking the whole builtin tree is slow); pass opts.scope to widen — "libraries", "library:<name>", or "all" (world + libraries). Read-only. The report is on .data; a one-line health summary is on .stdout.
Parameters
optsCheckOpts(optional)
Returns ZmToolResult
-- your world only
{ severity = "error", includePlaceholders = false }
{ scope = "all" } -- world + imported libraries
tools/worldValidation/checkLibraries
worldValidation.checkLibraries(opts?: FilterOpts) -> ZmToolResult
Validate every imported library under /source/libs/. World content is skipped — the report's world bucket is nil. The libraries map carries one entry per library directory.
Parameters
optsFilterOpts(optional)
Returns ZmToolResult
{ severity = "warning" }
tools/worldValidation/checkLibrary
worldValidation.checkLibrary(name: string, opts?: FilterOpts) -> ZmToolResult
Validate one named library under /source/libs/<name>/. If the library does not exist the report carries a single library.missing error. Use when you want to isolate the health of one dependency.
Parameters
namestringoptsFilterOpts(optional)
Returns ZmToolResult
"@builtin"
"@mylib", { severity = "error" }
tools/worldValidation/checkWorld
worldValidation.checkWorld(opts?: FilterOpts) -> ZmToolResult
Validate ONLY the world's authored content (everything under /source/ except /source/libs/). Use to verify your own code without library noise. The report's world bucket is populated; the libraries map is empty.
Parameters
optsFilterOpts(optional)
Returns ZmToolResult
{ severity = "error" }
tools/worldValidation/report
worldValidation.report(opts?: ReportOpts) -> ZmToolResult
Generate a validation report with structured filtering + formatting options. opts.scope selects which bucket to scan (default "world" — YOUR content, never the imported libraries / engine builtins); opts.format picks the .stdout rendering; opts.savePath writes the rendered report to a VFS path (format inferred from extension when not set).
Parameters
optsReportOpts(optional)
Returns ZmToolResult
{ scope = "world", format = "markdown" }
{ savePath = "/source/.validation/last-run.md" }
{ severity = "error", includePlaceholders = false, format = "human" }
tools/worldValidation/summary
worldValidation.summary(opts?: CheckOpts) -> ZmToolResult
Validate your world and return ONLY the one-line health summary — no problem list. Cheap to call when all you need is a yes/no health gate. Same scope rules as worldValidation.check: YOUR content under /source/ (excluding /source/libs/) by default; pass opts.scope to widen. Most useful filter here: { includePlaceholders = false }.
Parameters
optsCheckOpts(optional)
Returns ZmToolResult
{ includePlaceholders = false }
tools/zm/add
zm.add(paths: string | { string }, opts?: { stage: string? }) -> boolean
Stage one or more paths' manifest rows for the next commit via world.add(path). Pass a single VFS path string for the single-arg form, or an array of path strings to stage in one call. The "stage everything" sentinels . / -A / --all (matching the shell zm add) route to world.add_all(). Empty array / empty string raise an argument error. To stage the caller's whole slice of the dirty set, see zm.addAll. Naming a path takes it whoever holds it, which is how a path another staging area holds is handed over. A world.add runtime error raises with a clean zm.add:-prefixed message. opts.stage names one of the caller's own staging areas to stage into, so a commit naming that area carries these paths and leaves every other caller's staged. Omitted, the call stages into the default area every unnamed call shares.
Parameters
pathsstring | { string }opts{ stage: string? }(optional)
Returns boolean
"/source/foo.luau"
{ "/source/a.luau", "/source/b.luau" }
"." -- stage every dirty path
tools/zm/addAll
zm.addAll(opts?: { stage: string? }) -> boolean
Stage the dirty manifest rows this caller can claim, via world.add_all(). Equivalent to zm add --all / zm add .. Use when the caller's whole slice of the dirty set should land in the next commit; for selective staging, call zm.add(paths). The working tree is one per branch and staging areas are not, so a path another area already holds stays with that caller — zm.add(path) names one and takes it. opts.stage names one of the caller's own staging areas to stage into. A runtime error raises with a clean zm.addAll:-prefixed message.
Parameters
opts{ stage: string? }(optional)
Returns boolean
tools/zm/branch
zm.branch(name: string, opts?: ZmBranchOpts) -> string
Create a branch — git branch <name> [<start>]. The branch starts at opts.from (defaults to the session branch's HEAD) and gets its own working tree, materialized from that commit. The session stays on its current branch; move onto the new one with zm.checkout, which returns once the branch's content has landed. zm.branches lists what a world has. Merge it back later with zm.merge.
Parameters
namestringoptsZmBranchOpts(optional)
Returns string
"feature"
tools/zm/branches
zm.branches() -> { { branch: string, commit_id: string, current: boolean } }
List the world's branches — git branch --list. Each row carries the branch name, the commit its head names, and whether this session is on it. Sorted by name. A branch exists for everyone in the world; which one you are on is yours alone, so at most one row is marked current and it says nothing about where anybody else is working.
Returns { { branch: string, commit_id: string, current: boolean } }
tools/zm/checkout
zm.checkout(branch: string) -> string
Switch this session to another branch — git checkout <branch>. The branch must already exist; zm.branch creates one and zm.branches lists what a world has. The tree is replaced by the branch's own content. Which branch this session is on is this session's alone; the branch itself is shared. Uncommitted work is not at risk — it already has its row on the branch it was written against. Returns once the branch's content has landed, so a commit made straight afterwards targets the branch you asked for rather than the one you left. A branch that does not finish loading leaves the session back on the branch it came from.
Parameters
branchstring
Returns string
"feature"
tools/zm/commit
zm.commit(message: string, opts?: { stage: string? }) -> string
Materialize the staged tree as a new commit via world.commit(message). Git semantics — commits ONLY what's already staged. The reducer auto-deletes the stage row on success so a subsequent zm.commit opens a fresh stage. A failure (empty stage, dangling dep, …) raises with a clean zm.commit:-prefixed message. opts.stage materialises one of the caller's own staging areas, so the commit carries the paths staged under that name and leaves every other caller's staged.
Parameters
messagestringopts{ stage: string? }(optional)
Returns string
"add character controller component"
tools/zm/contribute
zm.contribute(opts?: ZmContributeOpts) -> { worldVcs.ContributeOutcome }
Send improvements to installed content back upstream — git subtree push ending in a pull request. For each targeted origin world: the diverging subtree is remapped to the origin's canonical paths, three-way merged against the origin's CURRENT content (a region the origin also changed becomes a local conflict with markers to resolve first), pushed as a contrib-<id> branch in the origin world, and opened as a pull request there. By default the pull request is merged immediately when you have write access (otherwise it is left open for review), and the local fork re-syncs so the asset no longer reads as ahead. Discover what is ahead first with zm.forkStatus.
Parameters
optsZmContributeOpts(optional)
Returns { worldVcs.ContributeOutcome }
tools/zm/create
zm.create(title: string, opts?: ZmCreateOpts) -> ZmCreateResult
Creates a new world owned by the caller.
Parameters
titlestringoptsZmCreateOpts(optional)
Returns ZmCreateResult
"Test Library A"
"Combat", { visibility = "private" }
tools/zm/deleteBranch
zm.deleteBranch(branch: string) -> boolean
Delete a branch — git branch -D <name>. Drops the branch and the working tree it owns; its commits are left alone, since deleting a branch drops the name and the tree under it rather than rewriting history. Uncommitted work on that branch goes with it and is NOT recoverable from trash, so the first call refuses and hands back the affirmation needed to go through with it — affirm with zm.affirm. Refuses the branch this session is on (check out another first) and the world's last branch.
Parameters
branchstring
Returns boolean
"feature"
tools/zm/discard
zm.discard(paths: string | { string }) -> ZmDiscardResult
Discard the working edits on one or more paths, taking each back to what it was staged or committed as — the git restore <path> shape, and the shell zm discard. The stage is the baseline where the path is staged, the last commit where it is not, and where it is neither there is nothing to come back to, so the path goes away. Staging is left exactly as it was: zm.unstage is the verb that changes it. A path that goes back to a committed version snapshots the discarded bytes to trash first and is recoverable via zm.restore. Pass a single VFS path, an array of paths, or . / -A / --all for every dirty path — which leaves nothing dirty behind. A path with no working edits has nothing to discard and is reported under skipped. Returns { reverted, skipped } path lists. A runtime error raises with a clean zm.discard:-prefixed message.
Parameters
pathsstring | { string }
Returns ZmDiscardResult
"/source/foo.luau"
"." -- discard every working edit in the world
tools/zm/fetch
zm.fetch(branch?: string) -> worldVcs.FetchResult
Update the origin/<branch> remote-tracking ref — git fetch. Mirrors the world's ZeroMind branch head into local commit history (no working-tree change) and reports how the session branch relates to it: behind origin commits to pull, ahead local commits to push, diverged when both. A stale pin or an out-of-band ZeroMind change shows up as behind — reconcile with zm.pull().
Parameters
branchstring(optional)
Returns worldVcs.FetchResult
tools/zm/forkStatus
zm.forkStatus() -> { worldVcs.ForkStatus }
Per-asset "ahead of origin" — the fork analogue of git status against an upstream. Every installed (pulled) asset whose content diverges from its pinned origin is listed, partitioned by the TRUE origin world it was pulled from (a nested dependency carries the world that authored it, not the intermediary it arrived through). Use this to decide what belongs upstream, then zm.contribute.
Returns { worldVcs.ForkStatus }
tools/zm/installAsset
zm.installAsset(guid: string, opts?: ZmInstallAssetOpts) -> worldVcs.InstallAssetResult
Installs the specified asset into the world.
Parameters
guidstringoptsZmInstallAssetOpts(optional)
Returns worldVcs.InstallAssetResult
asset_guid
asset_guid, { path = "/source/imported/foo" }
tools/zm/installLib
zm.installLib(guid: string, opts?: ZmInstallLibOpts) -> worldVcs.InstallLibraryResult
Installs the specified world as a library. After install, it is reachable as @<name>::<path> from this world's scripts (e.g. require("@combat::weapons.sword")).
Parameters
guidstringoptsZmInstallLibOpts(optional)
Returns worldVcs.InstallLibraryResult
world_guid
world_guid, { as = "combat" }
world_guid, { as = "@combat", version = commit_id }
tools/zm/list
zm.list() -> { ZmWorldEntry }
List every world the authenticated user has access to (owner / maintainer / contributor / viewer). Calls world.list() which wraps the spacetime list_my_worlds procedure (forwards ZeroMind's GET /v1/me/worlds). Returns the array of ZmWorldEntry records, sorted by title. A runtime error raises with a clean zm.list:-prefixed message.
Returns { ZmWorldEntry }
tools/zm/log
zm.log(opts?: ZmLogOpts) -> { worldVcs.CommitRow }
List commits on the active branch, newest first. Returns the raw commit-info array from world.log (each entry carries commit_id + message). Pass opts.limit to cap the count; omit for default 50, pass 0 for unlimited. A runtime error raises with a clean zm.log:-prefixed message.
Parameters
optsZmLogOpts(optional)
Returns { worldVcs.CommitRow }
{ limit = 10 }
tools/zm/merge
zm.merge(sourceBranch: string) -> worldVcs.MergeResult
Merge another branch into the session branch — git merge <source>. The merge runs locally in the world and is abortable with zm.mergeAbort; nothing reaches ZeroMind until the result is pushed. Requires a clean working tree (commit or stash first). A clean merge lands a two-parent merge commit and the merged content appears in the working tree. On conflicts, git-style markers are written into each conflicting text file and the cleanly-merged remainder is applied as working-tree changes; zm.status lists the unmerged paths. Resolve each path (edit out the markers / rewrite / remove the file), then zm.add + zm.commit — that commit records the merge (second parent = the source head) and clears the unmerged set. Push with zm.push to land the merge in ZeroMind as a two-parent commit.
Parameters
sourceBranchstring
Returns worldVcs.MergeResult
"feature"
tools/zm/mergeAbort
zm.mergeAbort() -> string
Abort the in-progress merge — git merge --abort. Clears the unmerged set and restores the working tree to its pre-merge state (the branch head never moved during a conflicted merge). Errors when no merge is in progress.
Returns string
-- back out of a conflicted zm.merge
tools/zm/prConflicts
zm.prConflicts(worldGuid?: string, number: number) -> any
Read what stands between a pull request and a merge. Returns the mergeability verdict, the merge base, both heads, and one entry per conflicting path. A conflicting TEXT path carries marked_text: the same <<<<<<< / ======= / >>>>>>> rendering a merge leaves in a working tree, source and target laid against their common ancestor. Resolve a path by writing the settled bytes back to it and committing on the source branch — the request re-analyses on the next read. A binary path carries the two sides' hashes and no text, so pick a side. A mergeable request returns an empty conflict list. zm.prView says HOW MANY conflicts there are; this says WHAT they are.
Parameters
worldGuidstring(optional)numbernumber
Returns any
nil, 1
"11111111-2222-3333-4444-555555555555", 3
tools/zm/prList
zm.prList(worldGuid?: string, number?: number) -> any
List pull requests in a world. To read ONE request — its diff, mergeability and conflict count — use zm.prView. A fork's outgoing pull requests live in the world they target, so read them there.
Parameters
worldGuidstring(optional)numbernumber(optional)
Returns any
"11111111-2222-3333-4444-555555555555", 3
tools/zm/prMerge
zm.prMerge(worldGuid: string, number: number, strategy?: ("merge" | "squash" | "fast_forward")) -> any
Merge a pull request — the agent-side merge button. Read the request with zm.prView first: it reports what the request changes and whether it merges cleanly.
Parameters
worldGuidstringnumbernumberstrategy("merge" | "squash" | "fast_forward")(optional)
Returns any
"11111111-2222-3333-4444-555555555555", 3
"11111111-2222-3333-4444-555555555555", 3, "squash"
tools/zm/prOpen
zm.prOpen(opts: any) -> any
Open a pull request — propose the work on one (world, branch) pair to another. From a fork the target defaults to the world it was forked from, so opening one with just a title proposes your work upstream; in an ordinary world the target is the same world, giving a branch → main request. The pull request lives in — and is numbered by — the world it targets, and that is the world zm.prList reads.
Parameters
optsany(optional)
Returns any
{ title = "fix the door hinge" }
{ title = "ship the HUD", sourceBranch = "hud", targetBranch = "main" }
tools/zm/prView
zm.prView(worldGuid?: string, number: number) -> any
Read one pull request in full — gh pr view. Returns the record plus a LIVE re-analysis against the current branch heads: mergeability (clean / conflicts / fast_forwardable / up_to_date / unrelated), conflict_count, and diff — every path the request adds, modifies or deletes, with its checksums. Read this before merging: it is what tells you WHAT the request changes.
Parameters
worldGuidstring(optional)numbernumber
Returns any
nil, 1
"11111111-2222-3333-4444-555555555555", 3
tools/zm/preview
zm.preview(guid: string, opts?: ZmPreviewOpts) -> worldVcs.PreviewResult
Preview what installing an asset WOULD write, without writing anything. Returns the resolved closure tree: every file and dependency with its computed dest_path, size, content hash, and dependency reason, plus rollup totals and a truncated flag. Use it to vet a package before zm.installAsset.
Parameters
guidstringoptsZmPreviewOpts(optional)
Returns worldVcs.PreviewResult
asset_guid
asset_guid, { path = "/source/imported/foo" }
tools/zm/pull
zm.pull(ref?: string, opts?: ZmPullOpts) -> worldVcs.PullResult | worldVcs.PullAssetResult
With no ref: fetch + reconcile the session branch with its ZeroMind origin — git pull. Strictly behind fast-forwards; diverged three-way merges the origin head with the same conflict/marker flow as zm.merge (resolve, then zm.add + zm.commit; abortable with zm.mergeAbort). Returns { status, commit?, conflicts? }. With a ref: pull upstream updates into a previously-installed asset, three-way merging every file against your local edits. Files you never touched fast-forward to the upstream version; files where your edits and the upstream edits don't overlap merge cleanly; files that clash land as conflicts (markers written) that block staging until you resolve them with zm.resolve(path, "ours"|"theirs"). Discover what has updates first with zm.updates. Returns { merged, conflicts, added, pruned }.
Parameters
refstring(optional)optsZmPullOpts(optional)
Returns worldVcs.PullResult | worldVcs.PullAssetResult
asset_guid
"/source/greeter.module"
tools/zm/push
zm.push() -> ZmPushResult
Publish unpushed commits to ZeroMind via world.push(). Mirrors git push semantics — no-args form walks the parent chain from HEAD back to the most-recently-pushed ancestor and pushes the entire unpushed stack oldest-first. The engine reads the caller's JWT from UserCredential / ZERO_USER_TOKEN; Luau scripts never see the token. A refused publish (parent-chain gap, ACL denial, network error) raises with a clean zm.push:-prefixed message.
Returns ZmPushResult
tools/zm/reset
zm.reset(commit: string) -> string?
Rewind the branch HEAD to commit in one shot. Non-destructive — the commits rewound past stay in storage and each becomes a trash entry recoverable via zm.restore (in chain order, oldest first). Errors when commit is not an ancestor of the current HEAD. Returns a summary listing what was rewound. Use zm.log to discover commit ids. A runtime error raises with a clean zm.reset:-prefixed message.
Parameters
commitstring
Returns string?
"01HABC..."
tools/zm/resolve
zm.resolve(path: string, choice: "ours" | "theirs") -> string
Resolve a conflicted pulled path by choosing a side. "theirs" rewrites the file to the upstream version and advances the origin pin; "ours" keeps your local bytes. Applies to conflicts zm.pull left behind (listed by world.conflicts() and held back from staging). For a text conflict you can also just edit the <<<<<<< / ======= / >>>>>>> markers out of the file by hand — staging it then counts as resolved. Branch-merge conflicts from zm.merge resolve by editing the marker'd file, not through this tool.
Parameters
pathstringchoice"ours" | "theirs"
Returns string
"/source/zerojs.package/init.luau", "theirs"
tools/zm/restore
zm.restore(handle: number | string) -> boolean
Recover a trashed entry by its handle — the row_id shown by zm.trash. This is the undo for the destructive verbs: orphaned commits from zm.reset, discarded file edits from zm.discard, dropped stashes, and removed files. Reset orphans must be restored in chain order (oldest first). A runtime error raises with a clean zm.restore:-prefixed message.
Parameters
handlenumber | string
Returns boolean
42
tools/zm/status
zm.status(opts?: { stage: string? }) -> ZmStatusResult
Show staged + dirty paths in the active world. Returns the raw { dirty, staged, untracked } table from world.vcsStatus, with branch and head folded in. blockers is world.publishBlockers(): every reason a push would refuse, one entry per class (script errors, unmet content requirements, references that can't be statically pinned), each naming its offending subjects and the one remedy for them. It reads the whole world, the scope a push gates on, so a blocker committed earlier is named here too; zm push publishes once the list is empty. A runtime error from the underlying world.* reads raises with a clean zm.status:-prefixed message. opts.stage reports one of the caller's own staging areas in place of the default one every unnamed call shares. The dirty and untracked sets are the world's working tree and read the same whichever area is named.
Parameters
opts{ stage: string? }(optional)
Returns ZmStatusResult
tools/zm/swap
zm.swap(guid: string, opts?: ZmSwapOpts) -> string
Switches the engine over to a different world. After this returns, every vfs.* / zm.* call targets the new world.
Parameters
guidstringoptsZmSwapOpts(optional)
Returns string
"11111111-2222-3333-4444-555555555555"
guid, { version = "passthrough:01HXAMPLECOMMITID" }
guid, { mode = "play" }
tools/zm/trash
zm.trash() -> { worldVcs.TrashRow }
List recently-destroyed items in the world's trash: orphaned commits (from zm.reset), discarded file edits (from zm.discard), dropped stashes, and removed files. Trash is a shared, world-wide safety net with a retention window. Each row's row_id is the handle you pass to zm.restore. A runtime error raises with a clean zm.trash:-prefixed message.
Returns { worldVcs.TrashRow }
tools/zm/uninstallLib
zm.uninstallLib(name: string) -> string
Removes a previously installed library from this world.
Parameters
namestring
Returns string
"combat"
"@combat"
tools/zm/unstage
zm.unstage(paths: string | { string }, opts?: { stage: string? }) -> boolean
Remove one or more paths from the staging area via world.unstage(path), leaving live manifest dirty state untouched — the inverse of zm.add. Pass a single VFS path string, or an array of path strings to unstage in one call. The "unstage everything" sentinels . / -A / --all drop the whole staging area via world.discard(). Empty array / empty string raise an argument error. A world.unstage runtime error raises with a clean zm.unstage:-prefixed message. opts.stage names one of the caller's own staging areas to act on. Omitted, the call acts on the default area every unnamed call shares.
Parameters
pathsstring | { string }opts{ stage: string? }(optional)
Returns boolean
"/source/foo.luau"
{ "/source/a.luau", "/source/b.luau" }
"." -- drop the whole staging area
tools/zm/updates
zm.updates() -> { worldVcs.UpdateReport }
List installed content that has upstream updates available. Every asset you installed keeps a live link to the world it came from; this reports which of them the origin has changed since your pinned version, so you know what has a fresh version without guessing. Each entry names the origin root asset and the local paths whose upstream content moved. Empty result = everything installed is up to date. Apply an update with zm.pull.
Returns { worldVcs.UpdateReport }
-- what needs updating?