Worlds
Scaffold, validate, publish and measure worlds, terrain, placements, achievements and products.
analyze_scene_performance
EXECUTES the Creator CLI scene performance analyzer and returns its report to the agent. Call after the first build or imported Scene exists, then again with final snapshot/Package/runtime evidence. It runs canonical Scene/Package validation, and unresolved non-bundle resources keep affected checks unknown. It separates authored mesh-definition triangles from runtime submitted triangles; file-local/slot/unique/source/native/Package/observed Palette materials; declared/GPU texture costs; owned/transitive/cold/warm delivery; and static estimated reference-budget utilization from supplied runtime claims. Runtime proof records compile/stream/GI readiness, grounded route, occupancy, frame count/duration, bundle/dirty-code identity, canvas/DPR, and internal compositor resolution separately. Unknown metrics stay unknown and never improve the result. Read scene-performance for the interpretation and matched visual-QA workflow.
| Argument | Type | Required | Description |
|---|---|---|---|
directory | string | yes | Absolute directory containing the built bundle or Scene-owned resources |
scene | string | no | Exact Scene v2 JSON; supplies authoritative resource roles/integrity, surfaces, and stream groups |
package | string | no | Exact Continuum Package version JSON; supplies semantic material bindings/dependencies and transitive closure |
snapshot | string | no | inspect_world snapshot JSON for visible-scene triangles |
perf | string | no | Runtime receipt. The report treats its values as supplied claims and reports byte/identity binding separately. |
profile | "web-low-mobile" | "web-mobile-balanced" | "web-high-mobile" | "web-desktop-balanced" | "web-pc-ultra" | "web-standalone-vr" | "web-pc-vr" | no | Existing Palette Showcase reference profile; default web-desktop-balanced. It is a comparison reference, not a universal Scene guarantee. |
bake_world_lighting
Bake a world's lighting field from its BUILT bundle and point its visual contract at the artifact (same code path as helix bake-lighting): serves the build headlessly, drives the in-page bake driver across sun keyframes plus the practical lights, audits and repairs the probe field, then writes helix.visuals.bake.json and patches helix.visuals.json to mode "baked" in BOTH the source public/ and the built root. USE IT FOR a bounded, static-geometry world whose visual contract declares — or should declare — mode "baked": interiors, arenas, dioramas, anything whose geometry and lights do not move. A BOUNDED INTERIOR authors probes: { center, size } in helix.visuals.json to fit the bake volume to its room BEFORE baking — the default volume is sized for open ground, and the bake warns when its volume does not fit the play space (heed that warning by authoring the block and re-baking); an authored center also pins the still-dynamic world's runtime volume to the same box, so author the block at build time, not bake time. Run it AFTER npm run build, on the built world, never on an unbuilt source tree, and expect MINUTES of blocking GPU work (a small world is a minute or two — do not interrupt it, and do not call it in a loop). NOTHING IT WRITES SHIPS until the world is re-validated and re-published: build → bake_world_lighting → validate_world → publish_world, and re-bake whenever the geometry, the lights, or any TRANSPORT knob in helix.visuals.json changes (sun, sky, environment, shadows, probes — the bake photographs all of them). LEAVE keys ALONE unless you know better: omitted, the engine plans them — ONE key for a static sun (timeOfDay.cycleSeconds 0), 9 for a cycle — and the pointer it writes records the count as bakedLighting.keys, which validate_world re-checks against the world clock. Pass gpu: true to bake on the machine GPU through a system Chrome/Edge when the wait hurts (measured 41 s against 27 minutes on a 45 MiB world); it is not bit-reproducible across machines, so the software renderer stays the default. It fails loudly rather than write a wrong artifact — an unbuilt world, a world that never publishes the bake handle (nothing called createVisualRuntime, so it is not runtime-lit and has no probe field), a boot slower than timeoutSeconds, or the donor-occlusion gate — and the failure text leads with the page errors, which are the diagnosis. It never installs a browser: a launch failure relays the install instructions for the human. The decision tree (baked vs dynamic vs the opt-out) and the full helix.visuals.json reference: read_doc({name:"lighting-world"}).
| Argument | Type | Required | Description |
|---|---|---|---|
directory | string | yes | Absolute path to the world project directory (its dist/ is baked and its public/ contract patched) or to the built bundle directory itself |
keys | integer | no | Sun keyframes to capture. OMITTED = the engine plans them: ONE for a static sun (timeOfDay.cycleSeconds 0 or absent), 9 for a cycle — which is what you want, so leave it out. A static world baked at 9 keys pays ~9x the artifact bytes for eight hours nobody plays, and keys: 1 on a CYCLING world is refused outright. (min 1) |
gpu | boolean | no | Bake on the machine GPU through a system Chrome/Edge instead of the bundled software renderer — measured 41 s against 27 minutes on a 45 MiB world. NOT bit-reproducible across machines, which is why software stays the default: use it while iterating, and remember the shipped artifact is whatever the last bake wrote. |
timeoutSeconds | integer | no | Boot budget in seconds — the wait for the bake handle, NOT a cap on the bake itself. Omitted, the bake scales it with the bundle size (20 s per MiB, floor 300 s), which is normally right; raise it when the headless boot is slow for some OTHER reason. A world that never publishes the handle fails with that diagnosis instead. |
capture_world_screenshot
Capture headless screenshots of a built world via its in-page screenshot helper (Playwright, in-process — same code path as the helix screenshot CLI command). Returns a compact preview image per capture PLUS a text block listing every file written. LOOK at the returned image(s) and judge the framing before attaching one with set_world_thumbnail. FOR PLACEMENT QUESTIONS USE inspect_world, NOT this tool — do not answer "is it floating / how far apart / does it fit" by looking harder; distances and depth read from an image are measurably unreliable. This is the CLOSING check for how the scene reads (framing, materials, lighting, mood), after the numbers are already right. Prefer a declared shot (world code calls screenshots.register — read_doc "screenshots") over the camera/lookAt/fov override or blindMs, both non-deterministic fallbacks.
| Argument | Type | Required | Description |
|---|---|---|---|
directory | string | yes | Absolute path to the world's built bundle directory (same dir publish_world takes) |
shot | string | no | Capture only this declared shot (default: every declared shot) |
camera | number[] | no | Override pose: camera position [x, y, z] — bypasses declared shots, writes custom.png |
lookAt | any | no | Override pose: look-at point [x, y, z] (only applied together with camera) |
fov | number | no | Override pose: field of view in degrees (only applied together with camera) |
preview | boolean | no | Also capture each shot at 2560x1440 to <shot>.preview.png |
blindMs | number | no | Explicit opt-in fallback for worlds WITHOUT the helper: skip the readiness wait, delay this many ms, then screenshot the canvas. Non-deterministic — a last resort, never a stable thumbnail source. |
out | string | no | Output directory (default: a "screenshots" folder next to directory) |
browser | "bundled" | "chrome" | "msedge" | no | Diagnostic override: force this one launch candidate, no fallback. The default chain (bundled Chromium, then system Chrome, then system Edge) is normally correct. |
previewImages | integer | no | How many downscaled preview image blocks to return (default 3). Pass 0 to skip image blocks entirely — right when capturing a gallery for the human rather than inspecting framing yourself; every written file path is always listed in the text block either way. (min 0) |
clear_world_thumbnail
RETURNS A COMMAND, NOT DATA. This tool hands you a helix … command string to RUN yourself — it executes nothing and returns no results. The answer is in that command’s output. Revert a world thumbnail/preview slot to the auto-generated placeholder.
| Argument | Type | Required | Description |
|---|---|---|---|
slug | string | yes | World slug |
kind | "thumbnail" | "preview" | no | Media slot to clear: thumbnail or preview. Default thumbnail. |
create_world
Mint a world's IDENTITY — the row, as a Draft with no build — so products and achievements can be registered BEFORE the first publish. Both registries live outside the bundle and hang off a world id, so on a brand-new project register_world_product and register_achievement fail with "No world of yours has the slug ..." until this has run; publishing is what adds a build, not what creates the world. Idempotent for a slug you already own — the existing world is reported and nothing is sent; a slug someone else owns is refused by the backend. The slug is globally unique and permanent, so choose it once.
| Argument | Type | Required | Description |
|---|---|---|---|
slug | string | yes | URL identifier: 3-50 chars, lowercase letters/digits/hyphens, starting alphanumeric. Globally unique and PERMANENT. |
title | string | yes | Display name players see, max 80 chars |
description | string | no | Catalog description, max 1000 chars |
serverCreationPermission | "creator" | "collaborators" | "anyone" | no | Who may stand up a persistent server for this world (server default: anyone) |
inspect_world
Measure the layout of a built world as DATA (same code path as helix world inspect): boots it headlessly, snapshots the scene graph — world positions, extents, oriented boxes, visibility, physics colliders — and runs placement checks: floating/sunk/intersecting geometry, collider-vs-visual divergence (including the classic half-extent/full-size slip), mirrored or degenerate transforms, stray coordinates. THE INNER LOOP FOR PLACEMENT: measure here until the numbers are right (this answers "is the bench 0.4 m in the air" exactly — a screenshot measurably cannot), then capture_world_screenshot ONCE before handing to a human. FINDINGS ARE MEASUREMENTS, NOT VERDICTS: check each against your intent. Hovering pickups, ramps embedded in the ground, and walls overlapping at their corners all warn on perfectly good worlds — a warn that matches what you meant needs no fix; a warn that surprises you is the bug. NEVER edit geometry just to silence a finding. WHAT IT CANNOT ANSWER: whether the scene READS as intended — materials, lighting mood, animation, art direction — that is the closing screenshot check. Requires the world to integrate @helix/engine-core/inspect (one line at end of boot: world.ready({ scene, body, spawn }) — read_doc "world-inspect"; the scaffold recipe shows the wiring). EXISTING worlds scaffolded before this integration usually lack the call — check_for_updates({ projectDir }) reports the wiring state up front, cheaper than a 60s timeout. DENSE HAND-BUILT worlds warn a lot on intended decor (hanging lights read as floating): uniform same-parent runs collapse to one line automatically, and after you adjudicate, writeBaseline persists the verdict so later inspects report only what CHANGED. Object paths are canonicalized (content order) — stable across runs; coordinates remain the strongest cross-run handle.
| Argument | Type | Required | Description |
|---|---|---|---|
directory | string | yes | Absolute path to the built world bundle directory (same dir publish_world takes) |
report | ("anomalies" | "roster" | "colliders" | "zones")[] | no | Report sections (default ["anomalies"]). "roster" = the grouped object inventory with sizes/positions; "colliders" = the physics roster; "zones" = the declared multiplayer.json trigger volumes with their rest surfaces and touched geometry. Ask for what you need — a large world renders long. |
json | boolean | no | Return the raw { snapshot, analysis } JSON instead of the rendered report — for programmatic follow-up on exact numbers. CAUTION on large worlds: the raw snapshot is ~350 bytes PER OBJECT (a 5000-object world is ~2 MB) — prefer the rendered report (grouped roster, per-rule capped findings) and request json only when you need every number. |
focusNear | number[] | no | THE SINGLE-OBJECT LENS: report only the object(s) near this world-space point — the fastest way to check the thing you just placed, because YOU wrote its coordinates. Returns exact position, size, AABB, FACING (yaw + world direction, with TOWARD / AWAY from / side-on relations to nearby objects — how you catch the chair pointing away from its table), touching colliders, and only the findings involving it. Analysis still runs whole-world; this scopes the report. No naming needed. |
focusRadius | number | no | Radius for focusNear in meters (default 2). |
focus | string | no | Alternative lens: report only objects whose path contains this substring (case-insensitive) — for named objects and imported GLB assets, whose node names survive export. |
settleMs | integer | no | Settle time between world-ready and the snapshot (default 750 ms). Raise it for worlds whose props animate into place. (min 0) |
browser | "bundled" | "chrome" | "msedge" | no | Diagnostic override: force this one launch candidate, no fallback. The default chain (bundled Chromium, then system Chrome, then system Edge) is normally correct. |
writeBaseline | boolean | no | Accept ALL current findings into inspect.baseline.json beside the world project — after that, inspect reports only what CHANGED (new findings, plus accepted ones that no longer occur). Use AFTER adjudicating the findings against intent (the hanging decor is meant to hang), and tell the user you wrote it. Entries key on rule + world-space anchor, so they survive rebuilds and renumbering. |
showAccepted | boolean | no | Render baseline-accepted findings too instead of hiding them behind the baseline summary line. |
list_abilities
List the abilities published on HELIX Instant (e.g. fly, swim). Abilities plug into a system; optionally filter by the host system id. Returns slug@version + title + target system.
| Argument | Type | Required | Description |
|---|---|---|---|
system | string | no | Only abilities targeting this system id (e.g. humanoid-character) |
list_achievements
List a world's registered achievements. Run it BEFORE publishing multiplayer rules to confirm every key an awardAchievement effect names actually exists — publish cannot check the registry, and a missing key fails silently at runtime.
| Argument | Type | Required | Description |
|---|---|---|---|
worldSlug | string | yes | Slug of the world |
list_my_worlds
List the worlds owned by the logged-in creator (including drafts), with their slugs and build state.
No arguments.
list_templates
List the available multiplayer world TEMPLATES — complete, publish-shaped worked worlds, one per capability cluster (presence, server/owned/shared/physics entities, collections, state machine, time, checkpoints, teams, voice channels). After get_started({kind:"multiplayer"}), pick the closest and read it with read_template({name}).
No arguments.
list_world_products
List what a world sells: each registered product, the pkey:<key> world code buys it by, its price and whether it is still on sale. Run it BEFORE writing purchase code to confirm the key you are about to name exists — an unregistered key fails only at runtime, as ProductNotFound. Omit worldSlug for every PUBLISHED world of the creator's and its registry (that route lists Published worlds only, so name the slug to see a draft world's).
| Argument | Type | Required | Description |
|---|---|---|---|
worldSlug | string | no | Slug of the world; omit for all your published worlds and what each sells |
placements_bake
Bake deterministic instanced placements on authored terrain from a helix.placements-source/1 JSON file: solid records placed exactly, decorative records scattered by rule. The CLI validates prepared prototype LOD/texture closure, masks and stable solid placements, then writes placements.json and pinned binary regions. Local authoring only; this does not publish. Read the terrain recipe first.
| Argument | Type | Required | Description |
|---|---|---|---|
source | string | yes | Absolute path to placements source JSON |
out | string | yes | Absolute output folder |
placements_inspect
Validate a baked placements folder and report prototype LODs, texture families, stable solid counts, region bytes and format caps. Read-only. Run after every bake and before scene publication; inspect runtime residency and visual passes as well.
| Argument | Type | Required | Description |
|---|---|---|---|
dir | string | yes | Absolute folder containing placements.json |
publish_unreal_world
Publish a COOKED UNREAL world to HELIX and make it the world's active build. Takes the candidate directory the Unreal HelixWorldPublish commandlet writes (it contains world-build-manifest.json, its .sha256 receipt, cook-plan.json, and artifacts/<role>/…) — NOT an Unreal project directory, and NOT a web bundle. Verifies the candidate against the bytes on disk, registers the Build, uploads every artifact, has the server re-verify and countersign it, then promotes it. This is the ONLY way a cooked Unreal world reaches HELIX. Requires login (check whoami first) and the world must already exist (create_world). YOU CANNOT COOK FROM HERE: cooking is an Unreal Editor / commandlet step on a Windows machine with UE 5.8 — if there is no candidate directory yet, say so and stop rather than looking for one to invent. Web worlds use publish_world instead; the two are not interchangeable.
| Argument | Type | Required | Description |
|---|---|---|---|
candidate | string | yes | Absolute path to the cook output directory — the one containing world-build-manifest.json. Passing an Unreal project root or a web bundle is refused with the reason. |
world | string | yes | The HELIX world to publish to: its slug, or its UUID. The world must already exist — create_world mints one. |
maxPlayers | integer | yes | Players per instance for the entry activity (1-1000). Ask the human if the world does not imply one. (min 1, max 1000) |
activityId | string | no | Activity id for the entry map. Defaults to 'default'; only set it for a world with several named activities. |
visibility | "private" | "unlisted" | "public" | no | Catalog visibility to set when the build is promoted. Omit to leave the world's current visibility alone. Going 'public' lists it for everyone — that is the human's call, not yours. |
promote | boolean | no | Defaults to true: the uploaded build becomes the active release. Set false to upload and verify WITHOUT changing what players get — use this to stage a build for review. |
verifyDelivery | boolean | no | After promoting, re-read what a client would actually download and check it matches what was uploaded. Cheap; worth it on a first publish. |
publish_world
Publish a validated world bundle to HELIX Instant. Refuses a World whose engine systems are pinned to an exact version (use ^ ranges; there is no agent override — only the human can run the CLI with --allow-exact-engine-pin "<reason>"). Automatically captures a ready world thumbnail unless a custom cover is already selected, uploads it with the files, and activates the Build and cover together; returns the public play URL. Capture failure refuses publication. Requires login (check whoami first). Re-publishing the same slug creates the next build of that world.
| Argument | Type | Required | Description |
|---|---|---|---|
directory | string | yes | Absolute path to the built bundle directory (e.g. the dist/ folder) |
thumbnail | string | no | Selected cover image path. Preserves an existing custom cover; otherwise uses this image, thumbnail.<ext> from the bundle, or automatically captures the ready world. |
uploadSource | boolean | no | Opt in to uploading this world's source alongside the build so it can later be opened and edited in HELIX's website editor. Defaults to false — source is NEVER uploaded unless you set this, and it is the human's decision, not yours: ASK before setting it, because it sends their code to HELIX and some creators deliberately do not want that. The upload excludes node_modules, .git, dist and .vite, and anything the project's .gitignore excludes. It travels on a separate PRIVATE channel, not as part of the public build, and it does not change the published bundle in any way. Without it the world still publishes and plays exactly the same; it simply cannot be edited on the website afterwards, because nothing but the built bundle was ever sent. |
sourceDir | string | no | Absolute path of the PROJECT directory to archive when uploadSource is true. Defaults to the parent of directory, which is right for the usual layout (publish dist/, source lives one level up). Set it only when the project root is somewhere else. Meaningless without uploadSource — passing it alone is refused rather than silently ignored. |
read_template
Read a complete, publish-shaped multiplayer world template: its DSL checklist, the full public/helix.json (incl. the multiplayer block), the client src/main.ts delta, and build/publish steps. Pick the name from the hub (get_started multiplayer) or list_templates — e.g. "hangout".
| Argument | Type | Required | Description |
|---|---|---|---|
name | string | yes | Template name, e.g. "hangout" (see list_templates) |
register_achievement
Register an achievement on one of the creator's worlds — the registry a world's badges live in, which is NOT part of helix.json and cannot be declared in the bundle. REGISTER BEFORE PUBLISHING the rule that awards it: awardAchievement with an unregistered key publishes clean and is a silent runtime no-op, the single most common authoring trap. Needs a 2D icon file (required — an achievement always has a graphic; generate one with generate_image if the human has no art, and note that a blank or sub-16px placeholder is refused). Leave unlockMode alone for a badge a room rule awards (the default); declare signal + value instead for a criteria badge the backend measures with no room at all. Pass dryRun to validate everything locally — no network, works logged out. TEST THE UNLOCK FIRST: the same row seeds .helix/dev-achievements.json for helix dev, where a criteria badge unlocks off simulated evidence as you play — the seed uses the STORED nested shape (criteria: { signal, op, value, key, field, metric }; this tool flattens key/field into criteriaKey/criteriaField because key is already the badge slug) and needs no icon. Registration takes effect immediately, live worlds included — no republish; the backfill pass credits players who already qualify.
| Argument | Type | Required | Description |
|---|---|---|---|
worldSlug | string | yes | Slug of the world that issues the achievement (the creator owns it) |
key | string | yes | Stable slug your rules name: 2-80 chars, lowercase a-z0-9_-, starting alphanumeric. Treat it as a schema — renaming needs a fresh registration. |
name | string | yes | Badge title players see |
icon | string | yes | Absolute path to the 2D graphic (png/jpg/webp/gif/avif/heic) — REQUIRED |
description | string | no | What the player did to earn it |
points | integer | no | Gamerscore points, 0-1000 (default 0) (min 0, max 1000) |
hidden | boolean | no | Show "???" until the player earns it |
trophy | string | no | Absolute path to a bespoke 3D trophy (.glb); omitted, the icon is baked onto the default plaque |
unlockMode | "room" | "criteria" | "platform" | no | Who may award it — the trust marker. Default: criteria when a signal is declared, else room (an awardAchievement rule). platform means FIRST-PARTY code awards it: nothing in a world can, so do not pick it. |
signal | "datastore" | "iwp" | "analytics" | "playtime" | no | Declaring a signal makes this a criteria achievement (the backend measures it, no room needed — works single-player). datastore = a numeric field in a per-user document; iwp = in-world purchases; analytics = a platform event count; playtime = seconds in this world. |
op | "gte" | "lte" | "eq" | no | Comparison against value. Default gte. |
value | number | no | Threshold the measured value is compared against. Required with signal. (min 0) |
criteriaKey | "player:{userId}" | "mp:player:{userId}" | no | datastore only, and this IS the trust decision: "mp:player:{userId}" is the room's flushed persistent playerVars (room-written ⇒ trustworthy), "player:{userId}" is the player's own client-written save (they can write it ⇒ self-reported). |
criteriaField | string | no | datastore only: dot-path to the number (e.g. "stats.summits"). For mp:player:{userId} it names the persistent playerVar directly. |
eventName | string | no | analytics only: the PLATFORM analytics event to count (e.g. world_entered). A world cannot mint its own. |
metric | "count" | "lix" | no | iwp only: purchase count (default) or total LIX spent. |
dryRun | boolean | no | Validate locally and report what would be sent; no network, no login needed |
register_world_product
Register a product one of the creator's worlds SELLS — the registry Helix.marketplace.purchaseProduct buys from, which is NOT part of helix.json and cannot be declared in the bundle or minted at runtime. REGISTER BEFORE WRITING the code that buys it: world code names the creator-authored key (purchaseProduct("speed_boost") buys pkey:speed_boost) and the key is IMMUTABLE once registered, so it is chosen here, once. The PRICE lives server-side only — a world never sends an amount; the shell raises the confirm popup and settles. priceLix 0 is legitimate: it registers a free "Claim". AUTHOR WHAT IT HANDS OVER as a grants effect array — pass | currency; several effects = a bundle, an empty array = a tip jar. The backend fulfils it DURABLY at settle. World Products do not mint universal Items; use the Creator approved Publications workflow for those. The effect list is IMMUTABLE once registered. Pass dryRun to validate everything locally — no network, works logged out. TEST THE LOOP FIRST: the same product row seeds .helix/dev-products.json for helix dev — buy/entitlements/consume answer locally with nothing charged, and the debug menu forces the failure paths. Registration takes effect on a live world immediately — no republish.
| Argument | Type | Required | Description |
|---|---|---|---|
worldSlug | string | yes | Slug of the world that sells it (the creator owns it). Registering on a draft world is allowed; a purchase needs a live world. |
title | string | yes | Name the purchase popup shows the player (max 120 chars) |
type | "consumable" | no | Legacy world-local effect with no durable grant. Universal-item grant types are not creator-facing. |
grants | ({ kind: "pass" | "currency", passKey?: string, durationSeconds?: integer, code?: string, amount?: integer, display?: "wallet" | "uses" })[] | no | The world-local effect array is IMMUTABLE once registered. Max 8 effects; several = a bundle, [] = a tip jar. Universal items use ItemDistribution instead. |
priceLix | number | yes | Price in LIX. 0 registers a free "Claim" — a confirmation, no spend. This is the ONLY place a price lives. (min 0) |
key | string | yes | Required stable slug world code buys as pkey:<key>: max 64 chars of lowercase a-z0-9_-, starting alphanumeric. IMMUTABLE — renaming would break every build naming it. Internal product UUIDs are never purchase references. (min length 1) |
description | string | no | What the player is buying (max 2000 chars) |
maxPerUser | integer | no | Cap on how many one player may own (whole number >= 1). A permanent-pass-only product is capped at 1 and REFUSES a higher value — a re-buy would charge again and deliver nothing. (min 1) |
dryRun | boolean | no | Validate locally and report what would be sent; no network, no login needed |
scaffold_world
RETURNS A COMMAND, NOT DATA. This tool hands you a helix … command string to RUN yourself — it executes nothing and returns no results. The answer is in that command’s output. REQUIRED FIRST BUILD STEP when no project exists yet (a workspace that already has package.json and public/helix.json is scaffolded: skip this) — the command is helix init <dir>, and running it writes a complete, build-ready world project: package.json, vite.config.ts (base: "./"), tsconfig.json, index.html with the import-map markers + loading overlay, .gitignore, public/helix.json (v0.2, pinning humanoid-character AND engine-core), src/helix.runtime.ts, src/loading.ts, and a RUNNABLE src/main.ts — a real character world that builds and renders, with the world-owned InputService pre-wired (register game keys on it — never raw addEventListener), the floor collider, the world.ready() inspect hook, and the ability base resolved against document.baseURI so it survives the sub-path a published world is served from. DO NOT HAND-WRITE THESE FILES from the recipe prose: the recipe is explanation, the scaffold is the source. An agent that typed them out instead got four API calls wrong against the installed .d.ts and shipped a world that built, validated, and rendered nothing. Multiplayer scaffolds include proximity VOICE CHAT by default (the voice.proximity permission + the fail-soft join) — remove both only for a deliberately silent world. A scene scaffold can also come with GROUND: terrain: "<preset>" adds --terrain <preset>, which generates a 4 x 4-tile (1 km x 1 km) heightfield into public/scene/mall/terrain/, paints it from the preset, lifts the starter mall onto the flat spawn plateau, and declares it in the sealed document — public/.helixignore already covers terrain/, so those bytes stay a dev override. Scene worlds ONLY; read_doc({name:"terrain"}). Existing files are SKIPPED (safe to run inside an existing project); the CLI never destructively overwrites. Then: npm install → install_world_packages → npm run build → validate_world → helix verify-subpath dist → publish_world.
| Argument | Type | Required | Description |
|---|---|---|---|
directory | string | yes | Absolute path of the project directory (created if missing; existing files are kept unless overwrite) |
title | string | yes | World title shown in the tab, loading screen, and catalog (min length 1, max length 80) |
slug | string | yes | URL-safe world slug (also the npm package name) (min length 3, max length 64) |
kind | "character" | "multiplayer" | "scene" | yes | character = single-player character world; multiplayer = presence world (grow the multiplayer block per the hub/templates); scene = YOUR CHOICE for a genuinely large or open world that cannot load at once (a city, a landscape, kilometres of ground, thousands of objects), or when the creator names a Scene document / Scene Package to stream — never for an arena, a room, "a scene", "a bare scene" or "no character", which are the plain recipe of get_started({ kind: "scene" }) or a character/multiplayer world with your own geometry. It is a multiplayer world whose ENVIRONMENT the scene system streams and the visual runtime lights — the scaffold seals a starter document into public/scene/, pins it in helix.json, and writes NO floor, props or lights, because the document carries geometry, materials, lights, spawn and collision |
maxPlayers | integer | no | multiplayer and scene worlds only; default 8. Keep modest — the platform clamps per-room (min 2, max 64) |
terrain | "meadow" | "alpine" | "desert" | "coast" | "forest" | "volcanic" | no | scene worlds ONLY: generate a 1 km x 1 km starter terrain from a built-in preset (meadow = gentle rolling grass, alpine = high massifs and snow, desert = dunes and mesas, coast = a shoreline that climbs inland, forest = wooded ridges and glens, volcanic = a crater cone in basalt). Omit for the flat starter scene |
overwrite | boolean | no | Replace existing files (default false: skip them — the into-existing-project mode) |
set_achievement_icon
Replace a registered achievement's ICON ART in place (HYPR-5142). Same UniversalItem, same key — every holder keeps their badge, and the default trophy plaque is re-baked with the new art (a bespoke trophy mesh is left untouched). This is the RIGHT way to change art: do NOT re-register the achievement to swap an icon (that re-keys it and leaves the old one delisted). image is a path to a local icon file (png/jpeg/webp); it is validated (a single flat colour is rejected) before anything is uploaded.
| Argument | Type | Required | Description |
|---|---|---|---|
worldSlug | string | yes | Slug of the world |
key | string | yes | The achievement key |
image | string | yes | Path to the replacement icon image file (png/jpeg/webp) |
set_preview_video
RETURNS A COMMAND, NOT DATA. This tool hands you a helix … command string to RUN yourself — it executes nothing and returns no results. The answer is in that command’s output. Set or clear a published world's YouTube preview. The CLI and the API own what counts as a valid YouTube link — the URL is normalised server-side.
| Argument | Type | Required | Description |
|---|---|---|---|
slug | string | yes | World slug |
url | string | no | YouTube URL or video id |
clear | boolean | no | Clear the existing preview video |
set_world_thumbnail
RETURNS A COMMAND, NOT DATA. This tool hands you a helix … command string to RUN yourself — it executes nothing and returns no results. The answer is in that command’s output. Attach a custom thumbnail or preview image to a published world (fills the generated-placeholder slot). Filling an EMPTY slot is a normal finishing step after a good capture — do it without asking. Pass overwrite ONLY when the human explicitly asked to replace an existing image; an occupied slot without overwrite is refused by the CLI (with the current URL intact) so you can relay the situation instead of silently skipping it.
| Argument | Type | Required | Description |
|---|---|---|---|
slug | string | yes | World slug |
file | string | yes | Absolute path to a local image file (png, jpg/jpeg, or webp) |
kind | "thumbnail" | "preview" | no | Media slot to fill: thumbnail (cover everywhere, 1280x720) or preview (one extra detail-carousel slide, 2560x1440). Default thumbnail. |
overwrite | boolean | no | Replace an already-set slot — only on an explicit human request, never automatically |
terrain_generate
Generate the GROUND a scene world stands on: runs helix terrain generate and writes a terrain source folder (per-tile 16-bit heights, the painted splats, terrain.json). Deterministic — the same preset, seed and size always produce the same bytes, so a re-run is free and a diff is real. USE IT for open ground under a scene world: a scene document declares the folder (the terrain element of a scene-elements document, or scaffold_world({ kind: "scene", terrain }) which already wired one) and the scene system streams the tiles as the world loads. A tile is 256 cells and this generator always writes the 1 m authoring grid, so a tile is 256 m: the default 4x4 grid is 1 km x 1 km (~2.4 MiB) and the cap is 16x16 = 4 km x 4 km (~39 MiB, ~25 s of work). A coarser authoring grid (0.5, 2, 4 or 8 m per sample) is an IMPORT-side choice — terrain_import takes outputMetresPerSample, generate has no spacing flag. Pass tiles OR sizeKm, never both. The preset picks BOTH the landform and the surfaces painted onto it. ALWAYS run terrain_inspect on the folder before publishing — folded layers and over-cap tiles are reported there and nowhere else. Terrain worlds run DYNAMIC lighting: never bake_world_lighting a world whose ground is streamed terrain. Full recipe: read_doc({ name: "terrain" }).
| Argument | Type | Required | Description |
|---|---|---|---|
out | string | yes | Absolute path of the terrain source folder to write (for a scaffolded scene world: public/scene/<folder>/terrain) |
preset | "meadow" | "alpine" | "desert" | "coast" | "forest" | "volcanic" | yes | Built-in preset: meadow (gentle rolling grass), alpine (massifs, snow above the treeline), desert (dunes and mesas), coast (a shoreline climbing inland), forest (wooded ridges and glens), volcanic (a crater cone in basalt) |
seed | integer | no | Landform seed; default 1. Change it for a different world of the same character |
tiles | string | no | Grid size in tiles, e.g. "8x8". A tile is 256 cells — 256 m at 1 m per sample, 1 km at 4 m. Default 4x4 (1 km x 1 km at 1 m); the cap is 16x16 |
sizeKm | number | no | Square extent in kilometres instead of tiles, rounded to whole 256 m tiles (1 km = 4x4) |
seaLevel | number | no | Flood the landform to this height in metres — everything below becomes water-level ground |
holes | string | no | Absolute path to a greyscale PNG mask; pixels above mid-grey become holes in the ground (the hole is visual only — the collider under it stays solid, so do not use it for a pit the player falls through) |
terrain_import
Turn an EXISTING heightmap — a real-world DEM, a sculpted export, ground shaped in another tool — into a terrain source folder: runs helix terrain import, resamples the file onto the authoring grid and paints it with a preset. Use it instead of terrain_generate whenever the shape of the ground is already decided somewhere else; everything after this point (inspect, the scene document, publishing, lighting) is identical. TWO SPACINGS, never confuse them: metresPerSample is the SOURCE posting — metres between two samples IN THE FILE, taken from the source data and never guessed — while outputMetresPerSample is the AUTHORING grid the world is built on, one of 0.5, 1, 2, 4 or 8 m (default 1). Author at 1 m for hand-scale ground a player walks over; go coarser when the extent is the point — a tile is 257 samples, so at 4 m one tile spans 1 km and a city-scale landscape costs a sixteenth of the tiles the same ground would at 1 m. HEIGHTS: png16 and r16 store 0..65535, so heightRange says what the darkest and brightest sample mean in metres (e.g. [0, 900]) and a wrong pair yields a plausible-looking world at the wrong scale; f32 (headerless little-endian float32) already holds absolute metres and REJECTS heightRange. Either way the ground belongs near y = 0 — the sky, the height fog and the numeric precision are all built for it, and the absolute altitude is a fact for the docs, not for the world: with png16/r16 shift heightRange (a 1900..3900 m DEM becomes [0, 2000]), with f32 pass heightOffset, which is added to every decoded height, so a DEM whose sea level reads 507 m re-bases with heightOffset: -507. The extension picks the format when format is omitted (.png, .r16/.raw, .f32). A source posting coarser than the authoring grid is upsampled with a cubic that keeps the slope continuous across the source samples; bilinear (resample) creases along every source row and column and the light draws each one. Three more inputs when the source needs them: origin puts sample (0, 0) at a chosen world position instead of centring the grid, samples gives the row length of a raw r16/f32 file that is not square, and splat + splatRoles paint from an AUTHORED RGBA image covering the grid extent — every role bound to a channel there takes its weight from that channel instead of the preset rule, and the roles left unnamed keep theirs. Then run terrain_inspect. read_doc({ name: "terrain" }).
| Argument | Type | Required | Description |
|---|---|---|---|
file | string | yes | Absolute path to the heightmap: a 16-bit PNG, a raw R16, or a headerless float32 (.f32) holding absolute metres |
out | string | yes | Absolute path of the terrain source folder to write |
preset | string | yes | Built-in preset id (meadow, alpine, desert, coast, forest, volcanic) to paint it with, or an ABSOLUTE path to a preset JSON file |
metresPerSample | number | yes | The SOURCE's ground resolution: metres between two samples IN THE FILE (a 30 m SRTM tile is 30). The grid the world is authored on is outputMetresPerSample |
heightRange | number[] | no | [MIN, MAX] in metres that the darkest and brightest sample stand for, e.g. [0, 900] — shifted so the lowest ground sits near 0. REQUIRED for png16 and r16; REJECTED for f32, which already holds metres (re-base that one with heightOffset) |
outputMetresPerSample | 0.5 | 1 | 2 | 4 | 8 | no | Metres between two samples of the AUTHORING grid: 0.5, 1, 2, 4 or 8, default 1. A tile is 257 samples, so it spans 256 m at 1 m and 1 km at 4 m — stay at 1 m for ground the player walks over, go coarse for a landscape whose extent is the point |
heightOffset | number | no | Metres added to every decoded height: how an absolute-metres DEM is re-based so its sea level or its floor lands on 0 (a source whose sea level reads 507 m takes -507) |
origin | any | no | [X, Z] world metres of sample (0, 0). Omitted, the grid is centred on the world origin |
samples | integer | string | no | Samples per row of a raw r16 or f32 file — a number, or "WxH" — needed only when its byte length is not a square. Ignored for png16 |
tiles | string | no | Grid size in tiles, e.g. "8x8". A tile is 256 cells — 256 m at 1 m per sample, 1 km at 4 m. Default 4x4 (1 km x 1 km at 1 m); the cap is 16x16 |
format | "png16" | "r16" | "f32" | no | Override the format the file extension implies. f32 is headerless little-endian float32 in absolute metres |
resample | "cubic" | "bilinear" | no | How a coarser source posting reaches the authoring grid. Default cubic (slope-continuous across the source samples); bilinear creases at every source sample |
splat | string | no | Absolute path to an AUTHORED RGBA8 PNG covering the grid extent; each channel bound by splatRoles replaces the preset rule for that role. Goes with splatRoles |
splatRoles | string | no | Which role each splat channel carries, e.g. "R=base,G=low,B=slope". Roles: base, slope, peak, low, cavity, accent — the ones left unnamed keep their preset rules. Goes with splat |
terrain_inspect
Report what a terrain source folder would actually ship, as numbers (helix terrain inspect): extent and tile count, authored samples, delivered bytes split into heights and splats, the height range, the coarse layer, which material roles the bake had to FOLD into a kept role on which tiles (a tile carries four layers; a preset may declare six), and every warning — an over-cap tile, an unbaked or splat-less folder, a grid past 16 x 16. THE GATE BEFORE PUBLISHING: none of this is visible in a screenshot and none of it is reported at publish time, so an unbaked or over-cap terrain otherwise ships silently. Read-only — it writes nothing. Returns the human summary followed by the same report as JSON.
| Argument | Type | Required | Description |
|---|---|---|---|
dir | string | yes | Absolute path of the terrain source folder (it holds terrain.json) |
update_achievement
Patch a registered achievement's presentation (name/description/points/hidden/active). The key, unlock mode and criteria are FIXED at registration — rules name the key, and re-labelling how a badge is earned would rewrite what every already-granted copy claims; register a new achievement instead. To change the ICON ART in place, use set_achievement_icon (the key and every holder's badge are preserved — do NOT re-register just to swap art). Set active:false to delist one (existing holders keep theirs).
| Argument | Type | Required | Description |
|---|---|---|---|
worldSlug | string | yes | Slug of the world |
key | string | yes | The achievement key |
name | string | no | |
description | string | no | |
points | integer | no | (min 0, max 1000) |
hidden | boolean | no | |
active | boolean | no | false delists it — nobody new can earn it |
update_world_product
Patch a registered product's presentation, price or ownership cap, or set active:false to take it off sale (the row stays, so past purchases and the items they granted keep resolving). The key and the type are FIXED at registration — world code buys the key, and changing what a purchase hands over would re-label every copy already sold; register a new product and deactivate this one instead.
| Argument | Type | Required | Description |
|---|---|---|---|
worldSlug | string | yes | Slug of the world that sells it |
product | string | yes | The product key (or its uuid, if it was registered without a key) |
title | string | no | |
description | string | no | |
priceLix | number | no | New price in LIX; 0 makes it a free "Claim" (min 0) |
maxPerUser | integer | null | no | New per-player ownership cap (whole number >= 1). Refused above 1 on a permanent-pass-only product; pass null to clear it back to the derived cap. |
active | boolean | no | false takes it off sale — existing owners keep theirs, nobody new can buy it |
validate_world
Validate a built world bundle directory (the Vite dist/ folder containing helix.json) against the exact rules the platform enforces at publish. Run this after building and fix every reported problem before publishing. A World whose engine systems are pinned to an exact version (not a ^ range) is refused: use ranges, and promote an engine fix instead of pinning to it.
| Argument | Type | Required | Description |
|---|---|---|---|
directory | string | yes | Absolute path to the built bundle directory (e.g. the dist/ folder) |
world_metrics
The static traversal/movement reference from a world’s OWN installed character system — read it instead of guessing spatial constraints while building: player capsule (height/radius/crouch), step height (what auto-steps vs blocks), max walkable slope, movement speeds, and the derived jump envelope (air time, max flat-ground gap at run/sprint) plus corridor/ceiling clearances. Values are the system DEFAULTS a world starts from; worlds may override via config. Use it to size gaps, steps, ramps, corridors, and ceilings BEFORE placing geometry, and pair it with inspect_world to verify what you placed.
| Argument | Type | Required | Description |
|---|---|---|---|
projectDir | string | yes | Absolute path to the world project (the dir with helix.json — must be npm-installed + install_world_packages'd so its pinned system is present) |