HELIX Docs
CLI & AI toolsMCP tool reference

Worlds

Scaffold, validate, publish and measure worlds, terrain, placements, achievements and products.

MCP server version 0.2.16-helix3.209. Your agent also receives each tool's description and schema directly from the server, which is always the authority for the version you run.

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.

ArgumentTypeRequiredDescription
directorystringyesAbsolute directory containing the built bundle or Scene-owned resources
scenestringnoExact Scene v2 JSON; supplies authoritative resource roles/integrity, surfaces, and stream groups
packagestringnoExact Continuum Package version JSON; supplies semantic material bindings/dependencies and transitive closure
snapshotstringnoinspect_world snapshot JSON for visible-scene triangles
perfstringnoRuntime 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"noExisting 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"}).

ArgumentTypeRequiredDescription
directorystringyesAbsolute path to the world project directory (its dist/ is baked and its public/ contract patched) or to the built bundle directory itself
keysintegernoSun 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)
gpubooleannoBake 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.
timeoutSecondsintegernoBoot 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.

ArgumentTypeRequiredDescription
directorystringyesAbsolute path to the world's built bundle directory (same dir publish_world takes)
shotstringnoCapture only this declared shot (default: every declared shot)
cameranumber[]noOverride pose: camera position [x, y, z] — bypasses declared shots, writes custom.png
lookAtanynoOverride pose: look-at point [x, y, z] (only applied together with camera)
fovnumbernoOverride pose: field of view in degrees (only applied together with camera)
previewbooleannoAlso capture each shot at 2560x1440 to <shot>.preview.png
blindMsnumbernoExplicit 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.
outstringnoOutput directory (default: a "screenshots" folder next to directory)
browser"bundled" | "chrome" | "msedge"noDiagnostic override: force this one launch candidate, no fallback. The default chain (bundled Chromium, then system Chrome, then system Edge) is normally correct.
previewImagesintegernoHow 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.

ArgumentTypeRequiredDescription
slugstringyesWorld slug
kind"thumbnail" | "preview"noMedia 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.

ArgumentTypeRequiredDescription
slugstringyesURL identifier: 3-50 chars, lowercase letters/digits/hyphens, starting alphanumeric. Globally unique and PERMANENT.
titlestringyesDisplay name players see, max 80 chars
descriptionstringnoCatalog description, max 1000 chars
serverCreationPermission"creator" | "collaborators" | "anyone"noWho 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.

ArgumentTypeRequiredDescription
directorystringyesAbsolute path to the built world bundle directory (same dir publish_world takes)
report("anomalies" | "roster" | "colliders" | "zones")[]noReport 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.
jsonbooleannoReturn 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.
focusNearnumber[]noTHE 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.
focusRadiusnumbernoRadius for focusNear in meters (default 2).
focusstringnoAlternative lens: report only objects whose path contains this substring (case-insensitive) — for named objects and imported GLB assets, whose node names survive export.
settleMsintegernoSettle 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"noDiagnostic override: force this one launch candidate, no fallback. The default chain (bundled Chromium, then system Chrome, then system Edge) is normally correct.
writeBaselinebooleannoAccept 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.
showAcceptedbooleannoRender 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.

ArgumentTypeRequiredDescription
systemstringnoOnly 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.

ArgumentTypeRequiredDescription
worldSlugstringyesSlug 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).

ArgumentTypeRequiredDescription
worldSlugstringnoSlug 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.

ArgumentTypeRequiredDescription
sourcestringyesAbsolute path to placements source JSON
outstringyesAbsolute 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.

ArgumentTypeRequiredDescription
dirstringyesAbsolute 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.

ArgumentTypeRequiredDescription
candidatestringyesAbsolute 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.
worldstringyesThe HELIX world to publish to: its slug, or its UUID. The world must already exist — create_world mints one.
maxPlayersintegeryesPlayers per instance for the entry activity (1-1000). Ask the human if the world does not imply one. (min 1, max 1000)
activityIdstringnoActivity id for the entry map. Defaults to 'default'; only set it for a world with several named activities.
visibility"private" | "unlisted" | "public"noCatalog 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.
promotebooleannoDefaults 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.
verifyDeliverybooleannoAfter 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.

ArgumentTypeRequiredDescription
directorystringyesAbsolute path to the built bundle directory (e.g. the dist/ folder)
thumbnailstringnoSelected cover image path. Preserves an existing custom cover; otherwise uses this image, thumbnail.<ext> from the bundle, or automatically captures the ready world.
uploadSourcebooleannoOpt 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.
sourceDirstringnoAbsolute 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".

ArgumentTypeRequiredDescription
namestringyesTemplate 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.

ArgumentTypeRequiredDescription
worldSlugstringyesSlug of the world that issues the achievement (the creator owns it)
keystringyesStable slug your rules name: 2-80 chars, lowercase a-z0-9_-, starting alphanumeric. Treat it as a schema — renaming needs a fresh registration.
namestringyesBadge title players see
iconstringyesAbsolute path to the 2D graphic (png/jpg/webp/gif/avif/heic) — REQUIRED
descriptionstringnoWhat the player did to earn it
pointsintegernoGamerscore points, 0-1000 (default 0) (min 0, max 1000)
hiddenbooleannoShow "???" until the player earns it
trophystringnoAbsolute path to a bespoke 3D trophy (.glb); omitted, the icon is baked onto the default plaque
unlockMode"room" | "criteria" | "platform"noWho 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"noDeclaring 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"noComparison against value. Default gte.
valuenumbernoThreshold the measured value is compared against. Required with signal. (min 0)
criteriaKey"player:{userId}" | "mp:player:{userId}"nodatastore 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).
criteriaFieldstringnodatastore only: dot-path to the number (e.g. "stats.summits"). For mp:player:{userId} it names the persistent playerVar directly.
eventNamestringnoanalytics only: the PLATFORM analytics event to count (e.g. world_entered). A world cannot mint its own.
metric"count" | "lix"noiwp only: purchase count (default) or total LIX spent.
dryRunbooleannoValidate 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.

ArgumentTypeRequiredDescription
worldSlugstringyesSlug of the world that sells it (the creator owns it). Registering on a draft world is allowed; a purchase needs a live world.
titlestringyesName the purchase popup shows the player (max 120 chars)
type"consumable"noLegacy 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" })[]noThe world-local effect array is IMMUTABLE once registered. Max 8 effects; several = a bundle, [] = a tip jar. Universal items use ItemDistribution instead.
priceLixnumberyesPrice in LIX. 0 registers a free "Claim" — a confirmation, no spend. This is the ONLY place a price lives. (min 0)
keystringyesRequired 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)
descriptionstringnoWhat the player is buying (max 2000 chars)
maxPerUserintegernoCap 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)
dryRunbooleannoValidate 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.

ArgumentTypeRequiredDescription
directorystringyesAbsolute path of the project directory (created if missing; existing files are kept unless overwrite)
titlestringyesWorld title shown in the tab, loading screen, and catalog (min length 1, max length 80)
slugstringyesURL-safe world slug (also the npm package name) (min length 3, max length 64)
kind"character" | "multiplayer" | "scene"yescharacter = 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
maxPlayersintegernomultiplayer and scene worlds only; default 8. Keep modest — the platform clamps per-room (min 2, max 64)
terrain"meadow" | "alpine" | "desert" | "coast" | "forest" | "volcanic"noscene 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
overwritebooleannoReplace 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.

ArgumentTypeRequiredDescription
worldSlugstringyesSlug of the world
keystringyesThe achievement key
imagestringyesPath 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.

ArgumentTypeRequiredDescription
slugstringyesWorld slug
urlstringnoYouTube URL or video id
clearbooleannoClear 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.

ArgumentTypeRequiredDescription
slugstringyesWorld slug
filestringyesAbsolute path to a local image file (png, jpg/jpeg, or webp)
kind"thumbnail" | "preview"noMedia slot to fill: thumbnail (cover everywhere, 1280x720) or preview (one extra detail-carousel slide, 2560x1440). Default thumbnail.
overwritebooleannoReplace 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" }).

ArgumentTypeRequiredDescription
outstringyesAbsolute path of the terrain source folder to write (for a scaffolded scene world: public/scene/<folder>/terrain)
preset"meadow" | "alpine" | "desert" | "coast" | "forest" | "volcanic"yesBuilt-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)
seedintegernoLandform seed; default 1. Change it for a different world of the same character
tilesstringnoGrid 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
sizeKmnumbernoSquare extent in kilometres instead of tiles, rounded to whole 256 m tiles (1 km = 4x4)
seaLevelnumbernoFlood the landform to this height in metres — everything below becomes water-level ground
holesstringnoAbsolute 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" }).

ArgumentTypeRequiredDescription
filestringyesAbsolute path to the heightmap: a 16-bit PNG, a raw R16, or a headerless float32 (.f32) holding absolute metres
outstringyesAbsolute path of the terrain source folder to write
presetstringyesBuilt-in preset id (meadow, alpine, desert, coast, forest, volcanic) to paint it with, or an ABSOLUTE path to a preset JSON file
metresPerSamplenumberyesThe 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
heightRangenumber[]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)
outputMetresPerSample0.5 | 1 | 2 | 4 | 8noMetres 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
heightOffsetnumbernoMetres 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)
originanyno[X, Z] world metres of sample (0, 0). Omitted, the grid is centred on the world origin
samplesinteger | stringnoSamples 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
tilesstringnoGrid 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"noOverride the format the file extension implies. f32 is headerless little-endian float32 in absolute metres
resample"cubic" | "bilinear"noHow a coarser source posting reaches the authoring grid. Default cubic (slope-continuous across the source samples); bilinear creases at every source sample
splatstringnoAbsolute 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
splatRolesstringnoWhich 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.

ArgumentTypeRequiredDescription
dirstringyesAbsolute 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).

ArgumentTypeRequiredDescription
worldSlugstringyesSlug of the world
keystringyesThe achievement key
namestringno
descriptionstringno
pointsintegerno(min 0, max 1000)
hiddenbooleanno
activebooleannofalse 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.

ArgumentTypeRequiredDescription
worldSlugstringyesSlug of the world that sells it
productstringyesThe product key (or its uuid, if it was registered without a key)
titlestringno
descriptionstringno
priceLixnumbernoNew price in LIX; 0 makes it a free "Claim" (min 0)
maxPerUserinteger | nullnoNew 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.
activebooleannofalse 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.

ArgumentTypeRequiredDescription
directorystringyesAbsolute 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.

ArgumentTypeRequiredDescription
projectDirstringyesAbsolute path to the world project (the dir with helix.json — must be npm-installed + install_world_packages'd so its pinned system is present)

On this page