Log inGet started

tools

Updated 6 September 2026

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

  • identity string — Tool identity ("<toolbox>.<name>", with or without the leading tools.).
  • positional { any } (optional) — Arguments already given by position, filling slots from 1.
  • named { [string]: any } (optional) — Arguments given by name, { [parameterName]: value }.
  • opts BindOpts (optional)order — the order to visit named in, 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 — read named as 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 slots positional fills, for a caller that passed an explicit nil and 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's shared.module/init.luau. The toolbox template ALWAYS ships a shared.module/ with default ok/fail result-envelope constructors so tools in this toolbox can require(".shared") from day one. Supply sharedCode only 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

  • identity string — 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

  • identity string — Tool identity ("<toolbox>.<name>", with or without the leading tools.).

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, 2 adds one-line descriptions, 3 adds the full assembled metadata. Pass a string instead to restrict to that toolbox at tier 1.
  • toolbox string (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
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 name is the tool's <toolbox>.<tool> identity; invoke it with the use_tool MCP tool (toolbox + tool + args, an array in signature order or an object naming the parameters), the form each entry's examples are rendered in.

Parameters

  • query string (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 no toolbox filter 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 (default 10; 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

  • toolbox string — The owning toolbox — an ambient toolbox by its name ("entityOps") or a library toolbox by its scoped identity ("@lib::ns").
  • tool string — 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

  • toolbox string — The owning toolbox — an ambient toolbox by its name ("entityOps") or a library toolbox by its scoped identity ("@lib::ns").
  • tool string — 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

  • folder string
  • opts ComposeFolderOpts (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

  • source string | AssetRef<material>
  • newName string
  • overrides MaterialOverrides (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

  • color Color
  • opts FromColorOpts (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 (*_normalnormal_texture, …), else base_color_texture. Returns the new AssetRef<material>.

Parameters

  • texture string | table
  • opts FromTextureOpts (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

  • animation string | AssetRef<animation>
  • entityArg string | EntityRef
  • opts AnimPlayOpts (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

  • query string

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

  • targets eo.Targets
  • source string
  • opts ReplaceOpts (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

  • root string
  • model string | table
  • opts AppearanceOpts (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 AssetRef envelope, used directly. Targets come from component schemas: every targeted entity's components are enumerated, and every public field declared Field.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

  • material string | AssetRef<material>
  • opts SwapMaterialsOpts (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_TEXalbedobase_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)
  • shader string | AssetRef<shader>
  • opts AppearanceOpts (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

  • opts BackfillOpts (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

  • assetName string
  • opts DescribeOpts (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

  • substring string
  • category string (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)
  • opts FromEntityOpts (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

  • opts GeneratePreviewsOpts (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

  • source string | RuntimeHandle
  • name string (optional)
  • opts NewOpts (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

  • name string

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

  • name string

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

  • query string (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

  • assetArg string | AssetRef

Returns UsersSummary

"pickup_car"
goldMaterialRef

tools/baking/__all

baking.__all(opts: AllOpts, scope?: (string | EntityRef | { string | EntityRef })) -> AllResult

Parameters

  • opts AllOpts
  • scope (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

  • opts AllOpts (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

  • opts ClearOpts (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

  • opts LightmapOpts (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

  • opts MobilityOpts (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

  • opts ProbeOpts (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

  • opts ReflectionOpts (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

  • opts ShowProbesOpts (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

  • entityId string | EntityRef
  • opts FrameOpts (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

  • camIdOrX string | EntityRef | number
  • xOrY number
  • yOrZ number
  • zOrNil number (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

  • name string

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

  • name string
  • position (vec3 | number) (optional)
  • opts CameraCreateOpts (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)
  • target EntityRef | string
  • opts CameraFrameOpts (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

  • camera LookAtRef (optional)
  • target LookAtRef (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)
  • params CameraSetParams (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

  • opts CaptureCollageOpts

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

  • camera string | EntityRef
  • opts (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

  • opts CaptureEntityOpts

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

  • opts CapturePositionOpts

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

  • opts CaptureOneshotOpts (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

  • entityId string
  • opts InstallOpts (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

  • entityId string

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

  • source string
  • paused boolean (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

  • r number

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

  • t number

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

  • target string | { any }
  • opts InspectOpts (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

  • enabled boolean (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

  • opts ProblemsOpts (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

  • category string
  • enabled boolean (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

  • axis string (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

  • subject any (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

  • targets Targets
  • components Components
  • opts AddComponentsOpts (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

  • componentType string
  • opts ComponentInspectOpts (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

  • targets Targets
  • opts DuplicateOpts (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

  • source string
  • opts FromAssetOpts (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

  • targets Targets
  • ops ModifyOps

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

  • targets Targets
  • components Components

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

  • targets Targets

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

  • components SpawnComponents | SpawnBatch
  • opts SpawnOpts (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

  • target string | EntityRef
  • replacement string | EntityRef
  • opts SwapOpts (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

  • targets Targets | { TransformTarget }
  • ops TransformOps (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

  • id ElementId (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)
  • opts CaptureElementOpts (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

  • callbackId string
  • value any (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)
  • y number (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)
  • to string (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

  • screen string | { 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

  • id ElementId (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

  • name string
"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

  • name string

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

  • id ElementId

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

  • name string
  • widgets { 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

  • dx number
  • dy number
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)
  • offsetY number (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

  • name string
"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

  • name string

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

  • id ElementId

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

  • name string

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

  • screenName string
  • widgetId string
  • props { [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

  • typeName string

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

  • of string | EntityRef
  • filter string (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

  • of string | EntityRef
  • chain string
  • target (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

  • of string | 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

  • path string
  • timeoutSecs number (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

  • path string (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

  • path string

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

  • target string | { string }
  • opts RunOpts (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

  • map string

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

  • mapPath string
  • control any (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

  • name string
  • controls { any }
  • opts MapOpts (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

  • opts ClickOpts (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

  • class string (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

  • dx number
  • dy number
  • steps number (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

  • peek boolean (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

  • key string
  • duration number (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

  • key string
"ShiftLeft"

tools/inputSim/keyUp

inputSim.keyUp(key: string)

Release a key. Only the release re-arms the next press edge.

Parameters

  • key string
"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

  • locked boolean

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

  • refOrEvents MacroEventList | 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

  • button number (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

  • x number
  • y number
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

  • dx number
  • dy number
0, 140

tools/inputSim/mouseUp

inputSim.mouseUp(button?: number)

Release a mouse button.

Parameters

  • button number (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

  • name string (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

  • button string
  • slot number (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

  • slot number (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

  • button string
  • duration number (optional)
  • slot number (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

  • stick string
  • x number
  • y number
  • duration number (optional)
  • slot number (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

  • trigger string
  • value number
  • duration number (optional)
  • slot number (optional)
"right", 1

tools/inputSim/padUp

inputSim.padUp(button: string, slot?: number)

Release a canonical pad button.

Parameters

  • button string
  • slot number (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

  • amount number
  • steps number (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

  • opts RecordMacroOpts (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

  • name string
  • events { 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

  • dy number
-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

  • x number
  • y number
  • duration number (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

  • label string
  • duration number (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

  • x number
  • y number
  • duration number (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

  • x number
  • y number
  • pressure number (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

  • id number
  • x number
  • y number
  • pressure number (optional)
id, 260, 580

tools/inputSim/touchUp

inputSim.touchUp(id: number)

Lift the contact id.

Parameters

  • id number
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

  • path string

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

  • assetType string (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

  • name string
  • position Vec3
  • opts AddLightOpts (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

  • name string

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

  • opts AmbientOpts

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

  • name string
  • opts SetLightOpts

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

  • sky string (optional)
  • opts SkyOpts (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

  • opts SunOpts

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

  • opts SetupOpts

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

  • path string

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

  • path string
  • destination string (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"
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

  • key string

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

  • key string

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

  • id string
  • bodyType ("dynamic" | "static" | "kinematic") (optional)
  • opts BodyOpts (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

  • entityId string | EntityRef
  • constraintType ConstraintKind
  • targetEntityId string | EntityRef
  • opts ConstraintOpts (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

  • entityIdA string
  • entityIdB (string | AddJointOpts) (optional)
  • opts AddJointOpts (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

  • entityIdA string
  • entityIdB string
  • ignore boolean (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

  • entityId string | EntityRef
  • lockX boolean
  • lockY boolean
  • lockZ boolean

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

  • entityId string | EntityRef
  • lockX boolean
  • lockY boolean
  • lockZ boolean

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

  • id string

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

  • entityId string | EntityRef
  • index number (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

  • entityId string | 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

  • entityId string | EntityRef
  • bodyType "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

  • entityId string | EntityRef
  • membership number
  • filter number

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

  • name string
  • url (string | AssetRef<mesh>) (optional)
  • x number (optional)
  • y number (optional)
  • z number (optional)
  • opts SpawnOpts (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

  • name string
  • url (string | AssetRef<mesh>) (optional)
  • x number (optional)
  • y number (optional)
  • z number (optional)
  • opts SpawnOpts (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

  • entityId string | 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

  • opts PaletteAdd

tools/pixelArt/addFrame

pixelArt.addFrame(opts: AnimAddFrame) -> number?

Parameters

  • opts AnimAddFrame

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

  • opts AnimOpts

Returns number?

{ op = 'duplicateFrame', target = s }
{ op = 'setFps', target = s, fps = 8 }

tools/pixelArt/bounds

pixelArt.bounds(opts: QueryBounds) -> any

Parameters

  • opts QueryBounds

Returns any

tools/pixelArt/circle

pixelArt.circle(opts: DrawCircle)

Parameters

  • opts DrawCircle

tools/pixelArt/clear

pixelArt.clear(opts: DrawClear)

Parameters

  • opts DrawClear

tools/pixelArt/clone

pixelArt.clone(opts: SpawnClone) -> Handle

Parameters

  • opts SpawnClone

Returns Handle

tools/pixelArt/countByName

pixelArt.countByName(opts: QueryCountByName) -> any

Parameters

  • opts QueryCountByName

Returns any

tools/pixelArt/create

pixelArt.create(opts: SpawnCreate) -> Handle

Parameters

  • opts SpawnCreate

Returns Handle

tools/pixelArt/deleteTemplate

pixelArt.deleteTemplate(opts: PersistDeleteTemplate) -> boolean

Parameters

  • opts PersistDeleteTemplate

Returns boolean

tools/pixelArt/destroy

pixelArt.destroy(target: Handle | string | number)

Despawn a pixel canvas. target is a handle, entity id, or name.

Parameters

  • target Handle | 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

  • opts DrawOpts
{ 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

  • opts AnimDuplicateFrame

Returns number?

tools/pixelArt/ellipse

pixelArt.ellipse(opts: DrawEllipse)

Parameters

  • opts DrawEllipse

tools/pixelArt/fillCircle

pixelArt.fillCircle(opts: DrawFillCircle)

Parameters

  • opts DrawFillCircle

tools/pixelArt/fillRect

pixelArt.fillRect(opts: DrawFillRect)

Parameters

  • opts DrawFillRect

tools/pixelArt/frameCount

pixelArt.frameCount(opts: QueryFrameCount) -> number

Parameters

  • opts QueryFrameCount

Returns number

tools/pixelArt/getPixel

pixelArt.getPixel(opts: QueryGetPixel) -> string?

Parameters

  • opts QueryGetPixel

Returns string?

tools/pixelArt/line

pixelArt.line(opts: DrawLine)

Parameters

  • opts DrawLine

tools/pixelArt/listTemplates

pixelArt.listTemplates(_opts: QueryListTemplates) -> { string }

Parameters

  • _opts QueryListTemplates

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

  • opts PaletteOpts
{ 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

  • opts PersistOpts

Returns string | boolean

{ op = 'saveAsTemplate', target = s, name = 'hero' }
{ op = 'deleteTemplate', name = 'hero' }

tools/pixelArt/pixel

pixelArt.pixel(opts: DrawPixel)

Parameters

  • opts DrawPixel

tools/pixelArt/play

pixelArt.play(opts: AnimPlay)

Parameters

  • opts AnimPlay

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

  • opts QueryOpts

Returns any

{ op = 'getPixel', target = s, x = 0, y = 0 }
{ op = 'listTemplates' }

tools/pixelArt/rect

pixelArt.rect(opts: DrawRect)

Parameters

  • opts DrawRect

tools/pixelArt/removeFrame

pixelArt.removeFrame(opts: AnimRemoveFrame)

Parameters

  • opts AnimRemoveFrame

tools/pixelArt/rows

pixelArt.rows(opts: SpawnRows) -> Handle

Parameters

  • opts SpawnRows

Returns Handle

tools/pixelArt/saveAsTemplate

pixelArt.saveAsTemplate(opts: PersistSaveAsTemplate) -> string

Parameters

  • opts PersistSaveAsTemplate

Returns string

tools/pixelArt/set

pixelArt.set(opts: PaletteSet)

Parameters

  • opts PaletteSet

tools/pixelArt/setActiveFrame

pixelArt.setActiveFrame(opts: AnimSetActiveFrame)

Parameters

  • opts AnimSetActiveFrame

tools/pixelArt/setFps

pixelArt.setFps(opts: AnimSetFps)

Parameters

  • opts AnimSetFps

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

  • opts SpawnOpts

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

  • opts AnimStop

tools/pixelArt/template

pixelArt.template(opts: SpawnTemplate) -> Handle

Parameters

  • opts SpawnTemplate

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

  • name string
  • opts PostEffectOpts (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

  • name string
'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

  • name string
  • enabled boolean
'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

  • name SpawnTarget
  • x number (optional)
  • y number (optional)
  • z number (optional)
  • opts CubeOpts (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

  • name SpawnTarget
  • x number (optional)
  • y number (optional)
  • z number (optional)
  • color shared.ColorInput (optional)
  • size number (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

  • name SpawnTarget
  • x number (optional)
  • y number (optional)
  • z number (optional)
  • color shared.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

  • name SpawnTarget
  • x number (optional)
  • y number (optional)
  • z number (optional)
  • preset TreePreset (optional)
  • seed number (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

  • args AdjustArgs

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

  • args ApplyArgs

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

  • args BakeArgs

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

  • args CancelArgs

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

  • args CollapseArgs

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

  • args DiffArgs

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

  • args ExplainArgs

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

  • args FindArgs (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

  • args FlattenArgs

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

  • args InspectArgs

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

  • args ModifyArgs

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

  • query string (optional)
  • pack string (optional)
  • kind string (optional)
  • limit number (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

  • args PatchArgs

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//latest/.

Parameters

  • args PreviewArgs

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

  • args PromoteArgs

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

  • args ReferenceArgs

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

  • id string

Returns any

"terrain.erode"
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

  • args SearchArgs (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

  • args SpawnArgs

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

  • args StatsArgs

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

  • args TraceArgs

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

  • args TweakArgs

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

  • args ValidateArgs

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

  • before string (optional)
  • after string (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

  • seconds number (optional)
  • mode ("run" | "start" | "stop" | "snapshot") (optional)
  • label string (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

  • source string (optional)
  • which ("worst" | "typical") (optional)
  • minMs number (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

  • n number (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

  • label string (optional)
  • spikeMs number (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

  • n number (optional)
  • source string (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

  • n number (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)
  • label string (optional)
  • spikeMs number (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

  • seconds number (optional)
  • label string (optional)
  • spikeMs number (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)
  • seconds number (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

  • n number (optional)
  • minMs number (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

  • n number (optional)
  • minMs number (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)
  • excludeAgent boolean (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

  • targets Targets
  • filter string (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

  • targets Targets
  • opts GetOpts (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

  • screen string
  • layers LayerNames

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

  • targets Targets
  • layers LayerNames
  • opts SetOpts (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

  • ids IdList
  • typeName string
  • data ComponentData (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

  • ids IdList
"box"
ids

tools/sc/despawn

sc.despawn(ids: IdList)

Despawn one or many entities.

Parameters

  • ids IdList
"box"
ids

tools/sc/move

sc.move(ids: IdList, dx: number, dy: number, dz: number)

Move entities by offset with smooth visual transition.

Parameters

  • ids IdList
  • dx number
  • dy number
  • dz number
"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

  • ids IdList
  • r number
  • g number
  • b number
  • intensity number (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

  • ids IdList
  • opts QuantizeOpts (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

  • ids IdList
  • typeName string
"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

  • target string
  • source AssetRef<bundle|mesh>
  • opts ReplaceWithAssetOpts (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

  • ids IdList
  • parentId string | EntityRef
"hat", "player"
ids, "torso"

tools/sc/spawnCamera

sc.spawnCamera(name: string, ...) -> string

Spawn a camera at position.

Parameters

  • name string

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

  • model AssetRef<bundle|mesh>
  • name string
  • count number
  • opts CircleOpts (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

  • model AssetRef<bundle|mesh>
  • name string
  • cols number
  • rows number
  • opts GridOpts (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

  • model AssetRef<bundle|mesh>
  • name string
  • count number
  • opts LineOpts (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

  • name string
  • source AssetRef<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

  • name string
  • model AssetRef<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

  • name string
  • content string

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

  • ids IdList
  • r number
  • g number
  • b number
  • blend number (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

  • ids IdList
  • opts TransformOpts (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

  • ids IdList
"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

  • opts CleanOpts (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

  • reset boolean (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

  • name string
  • template ("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

  • query shared.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

  • scope SceneListScope (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

  • name string

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

  • opts MigrateOpts

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)
  • roles PlayerRoles (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

  • name string (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

  • targets Targets

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

  • source string
  • opts ReplaceOpts (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

  • name string (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

  • name string (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

  • action SpawnAction (optional)
  • opts SpawnOpts (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

  • target TreeTarget (optional)
  • opts TreeOpts (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

  • scene string (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

  • selection shared.Selection (optional)
  • opts AcceptOpts (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

  • opts ChangesOpts (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

  • selection shared.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

  • jobId string

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

  • service string
  • input GenerateInput (optional)
  • operation string (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

  • id string

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

  • name string

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

  • name string (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

  • code string
  • duration number (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

  • before string | Report
  • after (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

  • opts LoadOpts (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

  • key string

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

  • key string (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

  • key string
  • value any (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

  • opts CompositeOpts
{ 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

  • opts EditOpts

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

  • opts PaletteOpts

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

  • opts PersistOpts

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

  • opts QueryOpts

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

  • opts SpawnOpts

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

  • opts TransformOpts

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

  • name string

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

  • name string
  • opts LoadLayerOpts (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

  • name string

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

  • name string
"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

  • request string
  • value any (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

  • request string
  • reason string

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

  • runner string
  • run string (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

  • name string
  • args { [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

  • run string (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

  • opts CheckOpts (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

  • opts FilterOpts (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

  • name string
  • opts FilterOpts (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

  • opts FilterOpts (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

  • opts ReportOpts (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

  • opts CheckOpts (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

  • paths string | { 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

  • name string
  • opts ZmBranchOpts (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

  • branch string

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

  • message string
  • opts { 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

  • opts ZmContributeOpts (optional)

Returns { worldVcs.ContributeOutcome }

tools/zm/create

zm.create(title: string, opts?: ZmCreateOpts) -> ZmCreateResult

Creates a new world owned by the caller.

Parameters

  • title string
  • opts ZmCreateOpts (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

  • branch string

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

  • paths string | { 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

  • branch string (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

  • guid string
  • opts ZmInstallAssetOpts (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

  • guid string
  • opts ZmInstallLibOpts (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

  • opts ZmLogOpts (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

  • sourceBranch string

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

  • worldGuid string (optional)
  • number number

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

  • worldGuid string (optional)
  • number number (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

  • worldGuid string
  • number number
  • strategy ("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

  • opts any (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

  • worldGuid string (optional)
  • number number

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

  • guid string
  • opts ZmPreviewOpts (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

  • ref string (optional)
  • opts ZmPullOpts (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

  • commit string

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

  • path string
  • choice "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

  • handle number | 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

  • guid string
  • opts ZmSwapOpts (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

  • name string

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

  • paths string | { 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?
  • api
  • reference