# generating
The services-toolbox procedure: discover what can be generated, start a
generation from a prompt, track the job across turns, and put the produced
asset into the slot a placeholder was holding.
A subskill of `authoring-assets` — open it with
`agent_skill { name = "authoring-assets/generating" }`.
# agentSkill
A skill packages what an agent needs to do one job well: the instructions,
the assets and toolboxes the job runs through, and — for jobs with distinct
branches — subskills that go deeper.
Skills advertise themselves. Every registered skill's name and one-line
description rides the agent's tool responses, so an agent discovers what this
world knows how to do without being told to look. Invoking one returns the
instructions plus the exact identities and tool names the job needs.
## Shape
```
<name>.agentSkill/
skill.yaml # the manifest: description, when, checks, dependencies, order
instructions.md # the prose an agent receives on invoke
README.md # what this skill is, for a human reading the tree
.metadata # tags
<sub>.agentSkill/ # optional subskills, reachable as <name>/<sub>
```
## The manifest
```yaml
$schema: zero/agent-skill/v1
description: Making a character a person can actually play.
when: >
Reach for this when the player should look like something else, or when
you are about to build a camera and a body and wire them together by hand.
checks:
- check: Holding a movement key in play moves the body across the ground.
observe: Read its position before and after holding the key; the numbers differ.
- check: The body animates while it moves.
observe: A time-sampled collage from a camera outside the player, while the key is held.
dependencies:
- "@builtin::tools.scene"
- "@builtin::tools.scene.player"
- "@builtin::components.PlayerPrototype"
- "@builtin::docs.core.multiplayer"
order: [spawn-points, camera-rig]
```
`description` is the line the roster carries, and the whole basis on which a
skill is chosen. Name the job the skill makes a reader competent at, in the
outcome they are after — "getting a scene's player and view right", "making
content the engine will load". A reader scanning the roster is matching it
against the job in front of them, so the closer it reads to their own
sentence about that job, the sooner they open it.
`when` is the longer trigger — the situations that should send a reader here,
in the words they would use for their own task.
`kind` says what the skill is to its reader, and `job` is assumed when it is
left out. A `job` is work taken on and finished: opening it holds it, so it
rides the reader's tool responses until it is released, and its `checks` say
when it is done. A `reference` is knowledge for the whole session, such as how
the engine fits together: opening it does not hold it, it is never released,
and it states no `checks`, `stages` or `verdicts`, because it describes no job
whose end they would mark. A check on a reference turns it into a step that one
pass completes, after which its reader stops applying it.
`checks` is what the reader of a job is answerable for at the end, and it leads the
rendered skill, ahead of the procedure. Each entry pairs a **condition that
must hold** with **where it is seen**, and both halves are required — a
condition alone is satisfied by any glance, and the wrong vantage is how a
job passes while every one of its faults sits outside the frame. Write them
as observations someone else could repeat and disagree with: a number that
changed, a thing visible in a named shot, a count that came back as one.
A job publishes only once it carries at least one.
`stages` models a skill whose passes run in order, when the order is
load-bearing. Each stage is `{ name, detail?, checks? }`, and a check that
sits in a stage is answered after that pass has run — which is the difference
between a criterion that means something and one that misleads. A skill with
no meaningful order leaves `stages` out and lists its checks flat; a skill
that has one may do both, and the flat checks then read as the final gate.
`verdicts` names the fixed set of outcomes a reader reports — at the end of
each stage when the skill has stages, and once at the end when it does not.
Each is `{ verdict, means }`: the word, and what reporting it asserts. Both
halves are required for the reason both halves of a check are. A verdict is a
check one level up, deciding a whole pass rather than one condition, and a
bare `refine` is a label each reader fills in privately — so two runs report
the same word about different states and nothing downstream can tell them
apart. Saying what it means settles the boundary in the skill instead of in
whoever happens to be reading it.
A check marked `perRun: true` is one whose content is written for the job at
hand: the manifest can say *keep a list of every feature this object must
have and answer against it*, but not what is on that list, because that is
per run. It renders with that instruction attached.
```yaml
verdicts:
- verdict: continue
means: Every check in the stage held, and the next stage can build on it.
- verdict: refine
means: A check failed on something this stage can still fix. Run it again.
- verdict: stop
means: A check failed on something an earlier stage decided. Go back to it.
stages:
- name: blockout
detail: Mass and proportion only.
checks:
- check: Every part on the identity list has a stand-in at roughly the right size.
observe: A capture beside the reference, at the same angle.
perRun: true
- name: material
checks:
- check: Metal reads as metal and cloth as cloth.
observe: The metallic and roughness passes, after the lighting stage has run.
```
`dependencies` is one flat list of asset **identities**: a toolbox, a single
tool, a guide, a component, a module, another skill. Each entry is resolved
when the skill is opened and reported with what it turned out to be, so a
skill never declares "this one is a toolbox" — the asset says so, and the
reader groups by that.
Identities are what make a skill portable. They pin like any other reference,
so installing a skill into another world brings the toolboxes and guides it
runs through along with it. A bare name (`scene`) or a guide path
(`core/multiplayer`) resolves to nothing and is refused before publish.
A toolbox entry lists the tools it carries, and a tool entry carries its own
signature, so the instructions never restate a call contract that can drift.
An identity this world lacks is reported as missing rather than dropped.
Write instructions that name the toolboxes and tools a job runs through, and
leave the call form to the reader — an agent may be holding the MCP tool
surface, the shell, or Luau, and the signatures are listed for it either way.
## Subskills
A `<sub>.agentSkill/` folder nested inside a skill is a subskill. It has the
same shape as its parent and is invoked as `parent/sub`.
Subskills are reachable only through their parent: they are absent from the
roster and from `skills.list`, and appear in the parent's invoke result. A
skill can therefore carry a dozen narrow procedures while costing the roster
one line.
`order` in the manifest names the subskills that lead. Any subskill it omits
still appears, sorted by name, after the ordered ones — so adding a subskill
folder is the only step needed to publish it.
## Publishing one
Make a skill and it is live: it joins the roster agents read and it opens by
name from the moment it exists. Edit its description and that is the line
agents see next.
A skill lives anywhere under `/zero/source/`. `asset.create` hands you a
valid one to fill in:
```luau
asset.create("agentSkill", "waveSpawning", { folder = "skills" })
```
A skill's scope — `builtin`, `library`, or `world` — comes from where it
lives rather than from anything it declares, so a world's own skill always
reads as the world's.
## Opening one
```
agent_skill {} -- the roster
agent_skill { name = "scenes" } -- one skill's instructions + deps
agent_skill { name = "scenes/player-setup" } -- a subskill
```
The same three through the toolbox — `skills.list`, `skills.invoke` — and
from Luau via `tools.use("skills", "invoke", "scenes")`. `asset.inspect` on a
skill ref returns the structured record all of them render.
Skills authored in a world are the world's own, and are labelled as world
content wherever they surface beside built-in ones.
## In the Inspector
Skill: what the skill is for and when it applies, how many checks and verdicts it declares, its stages, a link to each asset it depends on (and a problem for each that does not resolve), its subskills, and **Open** for instructions.md.
# services
Generate real content — 3D meshes, textures, audio — from a prompt, by spending credits. This is the whole surface for it: discover what you can make, start a generation, track it, and find what you've made. You never touch the underlying asset type or task plumbing.
When a build needs a recognizable thing — a treasure chest, a snowboard, a character, a sound — you **generate** it. You don't assemble it from boxes and spheres.
## The loop
Discover what you can make: each service lists its operations, and each operation is self-describing: its inputs, the kind of asset it produces, and its credit cost. Check your credit balance if you want. Start a generation with a service and a prompt; this returns immediately with a handle and runs in the background, because a generation takes minutes and blocking would trip the execute time limit. When the run ends it posts a notice, `generation finished` naming its id and the job's `asset` (the spawnable asset path, which you can drop straight into a blocked-out slot) or `generation failed` naming its id and the error, and the notice reaches you on your next tool call; `services.status` reads a run's progress by its id meanwhile. A run files its output where your writes landed when you started it: one started in edit keeps its asset on source even when it finishes after you entered play, and one started inside your play session leaves it on the play shadow, where a play-exit discard takes it back; `durable` in the input states it outright. The notice and `services.status` both say where the output stands (`durable`, and the shadowed paths when the play shadow holds any of it). Alongside the live per-session job view, a durable outputs record survives restarts by reading the provenance stamped on each generated asset.
## Describing your assets
`describe` is the one tool here that makes text, not content. It finds the assets in your world that lack the description their type asks for (for the built-in types that ask, a README, or the `about.yaml` texts of a type) and fills them in: `{ mode = "find" }` lists what is missing and what filling it costs, `{ mode = "all" }` fills it, and `{ assets = { ... } }` fills the assets you name. A call costs 1 credit and covers many assets, and `find` reports the number of calls before anything is spent. What it writes is an ordinary file in the asset, there for you to edit.
# Module (asset type)
A module is a reusable, stateless (or globally-stateful) Luau library
that other code pulls in with `require()`. Modules are the unit of
factored-out logic in a world: helpers, math libraries, format
parsers, registries, shared algorithms.
## When to use one
- You have logic you want to share across multiple components,
services, scenes, or tools.
- You have a registry, lookup table, or shared cache that callers all
read from / write to.
- You want a stable API surface (`M.fn`) other code can depend on.
If the logic is bound to one entity, use a `.component`. If you want a
named UI of fields rather than functions, use a `.preset`.
## Where it lives
- Source: `/zero/source/.../<name>.module/`
- Identity: the source path with `/` written as `.` and each segment's
type suffix dropped — `/zero/source/demo/beats.module` is
`demo.beats`, and `/zero/source/kit.package/beats.module` is
`kit.beats`, the container's name kept and its `.package` suffix
gone. `asset.create` returns this string, `asset.resolve(...).identity`
reads it back, and the `require` forms below write it as
`<identity>`.
- Folder shape:
- `init.luau` (or `init.lua`) — module body. **Required.** Returns
a table (conventionally named `M`).
- `README.md` — instance-level documentation. **Required.**
- `.metadata` — agent-editable tags + free-form fields. **Required.**
## How to create one
```luau
asset.create("module", "<name>")
-- Creates: /zero/source/<name>.module/
-- init.luau (canonical `local M = {} … return M`)
-- README.md (instance README template)
-- `folder` places it in a subfolder of /source, and that subfolder
-- leads the identity: `{ folder = "lib" }` produces `lib.<name>`.
-- `into` authors it inside a resolved container (a package, a toolbox),
-- whose own identity leads the same way:
asset.create("module", "<name>", { folder = "lib" })
asset.create("module", "<name>", { into = asset.resolve("myPkg.package") })
```
## How it operates
1. **Registration.** Writing `init.luau` into a `.module/` folder
indexes the asset and registers the module path with the resolver.
2. **Loading.** The first `require("<identity>")` runs
the file's body and caches the returned table. Subsequent requires
from anywhere return the **same table** — this is the
shared-state lever for "module-local" registries. The body runs in
an environment of the module's own: a name it declares without
`local` is readable by the module's own functions and by no other
script, so an export reaches its callers through the returned table
and nothing else. Requiring a component's folder runs its chunk the
same way, once, and its `update`, `start` and `public` stay in that
environment; a component runs on an entity through
`entity(id).component.add(name)`.
3. **Hot reload.** Editing the file re-runs the module body and folds
what it returns into the table already loaded, so that table keeps
its identity: a caller that captured it with `require` reads the new
source's functions and values through the reference it already
holds, and a later `require` returns that same table. A key the new
source drops goes away; a key something set at runtime stays. The
body's own `local`s start again from their initial values, so state
that must outlive an edit lives in `modules.state()`
(`guides { path: "core/scripting-and-tasks" }`).
4. **Identity resolution.** A module is reached by its `<identity>`,
and where the caller lives decides which spelling of it resolves.
For a module in a **world**, from a caller in that same world —
another module, a component, a scene entrypoint, or an `execute`
chunk:
- `require("<identity>")` — the identity on its own, resolved
against the caller's own root. It carries every folder segment,
so a module created with `{ folder = "demo" }` is
`require("demo.beats")`.
- `require("@root::<identity>")` — the same root-relative
resolution, written out.
- `require(".sibling")` — a module in the same folder, by its bare
name; `..name` steps up one folder, `...name` two.
Logs, stack traces and hot-reload notices name that same module
`@local.source.<identity>` — its key under the world source root.
The `<identity>` tail of that key is the form above, so a trace
reading `@local.source.demo.beats` is `require("demo.beats")`.
For a module in a **library** — content under
`/zero/source/libs/@<lib>/` — `@<lib>::` addresses that library's
root from anywhere: `require("@builtin::modules.transform")`.
Mounting a world as a library moves its files under such a root and
flips the caller root to `@<lib>::`, which is what keeps the
root-relative forms above resolving across the move.
## Discovery
- `asset.list("module")` — every registered module.
- `asset.inspect("<name>")` — public functions, source path, this
type README.
- `ls /zero/source/<name>.module` lists the files the folder holds.
## Authoring conventions
- Return a single `M` table from `init.luau`. Top-level statements
with side effects run on first require — useful for one-time
initialization, dangerous if they touch the engine before it's
ready.
- Write exported functions as `typed function`, not `function`: the
argument types are enforced at the call and the checker knows the
signature, so a caller's mistake is reported where they made it.
- Annotate exported functions with `--!desc` / `--!arg` / `--!return`
/ `--!example` so the LSP, `tools.list`, and `asset.inspect`
surface them. A function with no `--!desc` is reachable only by
someone already reading this file.
- Declare a VALUE with `Field.<kind>(default, mode, description)`, and
a table whose members are reached through a metatable with
`--!members <TypeName>` — the latter gives the surface its accepted
member set, its type, and its documentation from one table. See
`guides { path: "core/authoring" }` for all four rules and
`modules/api/engine/entity.module/members.module` for a worked
member table.
- Keep module state in module-local upvalues or in `modules.state()`.
A name the body declares without `local` lives in the module's
environment, which every run of the module shares: a function the
previous run left in place reads what the reload assigned. Module-locals start again from their
initial values with each hot reload; the table `modules.state()`
returns is the same table on both sides of every reload.
- Prefer focused modules over kitchen-sink modules. If two halves of a
module have no shared state, split them.
## Common pitfalls
- **Cyclic requires.** A `require` chain that loops will return the
partial table (the half built before the cycle was detected).
Design for one-way dependencies.
- **Engine-time side effects.** Don't `entity.spawn` at module
top-level — the engine may not be ready. Expose an `M.init` and
call it from a scene entrypoint.
- **Hot-reload + module-local cache.** A module that caches
expensive computation in upvalues loses that cache on reload —
fine for dev, but be aware in performance work.
- **`init.lua` vs `init.luau`.** Either works, `.luau` is canonical.
- **Writing one while play runs.** A `/source` write made while play is
running lands on the play shadow, and a guarded play-exit discards it
unless it is kept. One `.module` write shadows `init.luau`, the
`README.md` and the `.metadata` together.
`vfs.write(path, bytes, { durable = true })` skips the shadow, and
`vfs.promotePlayShadow` keeps what is already on it — the `core/vfs`
guide has the model and what each route costs.
## Related types
- `.component` — for entity-bound state + lifecycle hooks.
- `.service` — generates content you don't have yet (a mesh, sound, texture, …) via a metered provider.
- `.tool` — for a single agent-callable function with a YAML schema.
- `.package` — to group several modules + components + scenes into
one shippable folder.
## In the Inspector
Exports: what the module exports (name, kind, signature), parsed from its source without running it, with **Check** (the LSP's diagnostics for its entry file) and **Open**.
# Material (asset type)
A material is a named, configured instance of a `.shader`. The shader
defines the surface look (lighting model, available property fields,
texture slots); the material picks values for those fields and a
shader to bind to. Renderable entities reference materials, not
shaders, so the same geometry can wear different materials and the
same material can be reused across many entities.
Entities sharing one material can still differ in a value: every drawn
object carries four `vec4` lanes of its own that a surface shader reads
as `input.shader_data[lane]` and `renderer.instanceData.set` writes. Use
that for a dissolve at its own progress per subject, a hit flash at its
own age, a fill level per instance — a material per entity is for when
the LOOK differs, not when a number does.
> New to how a material relates to its shader, its textures, and the live GPU
> material behind it? Read `core/resource-model` first. It also covers the
> difference between an authored `.material` asset (this type) and a runtime GPU
> material made with `renderer.material.create`.
## When to use one
- You want a named surface configuration an entity wears by referencing it
from its `Model` component (`material = "@.../<Name>"`).
- You want runtime property tweaks (`asset.resolve(ref):setProperty(...)`) to
affect every entity using the material at once.
- You want to publish a curated look (`Gold`, `RedGlow`,
`BrickWall`) other authors can reuse without re-typing shader
bindings.
If you need a brand-new lighting model or a custom visual effect, you
need a `.shader` first; the material wraps it.
## Where it lives
- Source: `/zero/source/.../<Name>.material/`
- Identity: `<Name>` (the `.material` suffix strips).
- Folder shape:
- `mat.yaml` — canonical material spec (shader + property values).
**Required.**
- `README.md` / `.metadata` — optional prose + tags, indexed when an
author writes real ones.
- `textures/` — optional folder for embedded textures. Validator
allows the folder; its interior is unrestricted.
- `shaders/` — optional folder for a co-located custom shader.
Same rule.
## How to create one
```luau
asset.create("material", "<Name>")
-- Creates: /zero/source/<Name>.material/
-- mat.yaml (shader binding + property scaffold)
-- `folder` places it in a subfolder of /source instead of the root:
asset.create("material", "<Name>", { folder = "materials" })
```
Pass the shader and initial values through `asset.create`, then tune with
`setProperty` and keep what you tuned with `saveDefinition`:
```luau
local mat = asset.create("material", "<Name>", { shader = "@builtin::shaders.pbr" })
mat:setProperty("base_color", { 0.8, 0.6, 0.1, 1 })
mat:setProperty("roughness", 0.3)
mat:saveDefinition() -- writes the tuned values into mat.yaml
```
This mints the `.material` asset and returns a handle you tune with
`setProperty`. A set is a runtime change: it reaches the GPU material and every
entity wearing it, and `saveDefinition` is the call that writes it into
`mat.yaml`, so a property driven every frame costs no file write and leaves the
authored file as its author wrote it. A purely runtime `asset.create` lives only
for the session.
For an ephemeral, runtime-only GPU material that is not an asset at all, use
`renderer.material.create` instead (see `core/resource-model`).
## How it operates
1. **Property reflection.** The bound shader's uniform struct fields
become the material's configurable properties — no manual property
declarations needed.
2. **Property values.** `mat.yaml` groups values by type, each block
keyed by the shader's field names: `properties:` holds scalars and
colors/vectors, `ints:` holds integer-typed fields, `bools:` holds
boolean-typed fields, and `textures:` holds texture slots. Omitted
fields take the shader's default. A scalar is a bare number
(`roughness: 0.5`); a color or vector is an array
(`base_color: [1, 1, 1, 1]`). `shader:` is the shader's identity
string. This is the shape every material the engine writes uses. A
`saveDefinition` writes the changed values into the file where they
already stand and leaves the rest of it — its comments, the order of
its keys and blocks, the spelling of its literals, untouched.
3. **Render state.** `render:` holds how the mesh is drawn rather than
what the shader reads: `blend` (the equation the fragment composites
with, also spelled `type`), `cull`, `depth_write`, `depth_compare`,
`topology` and `queue` (the draw order, a whole number: `2000` opaque,
`3000` transparent, `4000` overlay). The `.shader` type README's "The
render block" section lists every value each key takes, and
`asset.create`'s `render` argument takes the same keys and values.
4. **Application.** An entity wears a material by referencing it from its
`Model` component (`material = "@.../<Name>"`). Materials affect every entity
that references them; runtime `asset.resolve(ref):setProperty(...)` changes
propagate to all of them immediately, and last until the next mode change
unless `saveDefinition` writes them into `mat.yaml`.
5. **Texture binding.** Texture slots accept `color:r,g,b,a` for
solid colors, `default:white` / `default:black` / `default:normal` for the
engine's own fallback texels, `@builtin::textures.<name>` for builtins,
`path/to/image.png` for file textures, or a render-target handle
for camera renders. The GPU texture cache reads the `color:` and
`default:` forms from the string itself, so a slot written in one names no
asset and carries nothing with the material when it travels; every other
form names a texture asset the slot depends on.
A texture in `base_color_texture` is multiplied by `base_color`: every
built-in surface shader declaring both draws the texture's colour times
`base_color`, channel by channel. Under the default white `base_color` the
texture shows as itself, and a coloured `base_color` tints it. The two
read a colour differently: `base_color` is linear, and the slot reads its
texels as sRGB-encoded, a `color:` value's included, the way it reads an
image. A colour stated in both places is therefore multiplied by a darker
copy of itself: `color:0.62,0.14,0.05,1` in the slot decodes to
`0.34, 0.017, 0.0039`, so beside `base_color` `0.62, 0.14, 0.05` the
surface draws `0.21, 0.0024, 0.0002`, far darker than either. State a
solid colour once, as `base_color`, and leave the slot at its default
white.
6. **Hot reload.** Editing `mat.yaml` repacks the GPU uniform buffer
on the next frame; visible immediately.
**Properties that follow a named value.** `values:` binds a property to a
named value of the entity wearing the material (`values.set(entity, name,
v)`, the `topics/values` guide): `property: valueName`, or
`property: { value: "name", scale: s, offset: o }`, and the property draws
`value * scale + offset` on each entity holding the value, read on the draw.
A boolean reads as 1 or 0, and a number fills every channel of a colour or
vector property. The wearer's own value is read, else its nearest
ancestor's, so a lamp mesh under a car lights from the car's `brake`; a
wearer holding none draws the material's own value. A scalar, vector,
colour or bool property binds (at most 32 per material); a texture slot
does not. `asset.create("material", name, { values = { ... } })` and
`renderer.material.create({ ..., values = { ... } }, key)` take the same
map.
```yaml
values:
emissive_intensity: { value: "brake", scale: 3 }
```
7. **Inspector.** `type.yaml` names `inspector: { type: "inspector.luau" }`,
and `inspector.luau` declares the material's sections beside the generic
ones: `shader` (the bound shader, swappable), `properties` (each shader
field as its typed control) and `textures` (each texture slot as an asset
chip). They read the material through `mat:getShaderRef()` (the bound
shader's ref, `~` expanded) and `mat:getTextures()` (slot name →
`{ ref, name }`: the bound texture's guid and the id the material names
it by). An edit there is an authoring write: `mat:writeProperty(name,
value)` and `mat:writeTexture(slot, texture)` write the value into
`mat.yaml` (and drop the session's own value for it, so the material
reads what the file holds), and `setShader` rewrites the file; inside an
undo operation each file write is recorded, so undo puts `mat.yaml` back.
`setProperty` / `setTexture` stay runtime writes that last until the next
mode change.
## Discovery
- `asset.list("material")` — every registered material.
- `asset.inspect("<name>")` — shader binding, property values,
source path, this type README.
- `ls /zero/source/<Name>.material` lists the files the folder holds.
- `asset.resolve("<name>"):getProperties()` — the property fields exposed by
the bound shader, each at the value it currently holds: what `mat.yaml`
authored until something writes to the material, and the written value from
then on, whichever call wrote it. That is the value the surface is drawn
with. Read them back rather than guessing names; which fields exist comes
from the shader.
- `asset.resolve("<name>"):getDefinition()` — the authored `mat.yaml` bytes,
the value the material rests at.
- `asset.resolve("<name>"):getShader()` — the shader the material binds.
## Authoring conventions
- Use lowercase, descriptive names (`brushed_steel`, `red_glow`,
`tarmac_wet`). Avoid shader-name suffixes in the material name —
the binding lives in `mat.yaml::shader`.
- Drive every property the shader exposes; defaults are a fallback,
not a contract for which fields exist.
- Embed textures alongside the material (`textures/` subfolder) when
they're used only by this material; reference shared textures from
`@builtin::textures.*` or a library.
## Common pitfalls
- **Property names must match the shader's uniform struct exactly**
(case-sensitive). Misnamed properties silently get the shader's
default.
- **`base_color_texture` multiplies `base_color`.** A `color:` texture holding
the same value as `base_color` multiplies the colour by its own
sRGB-decoded value, so the surface renders far darker than the value
written. Write a solid colour as `base_color` alone.
- **Broken shaders break the material.** A material whose shader has a
parse error renders as the magenta checkerboard. Inspect the shader
first if the material looks wrong.
- **`setProperty` targets the material, not the entity.** Changes affect
every entity that references the material.
- **`setProperty` is a runtime change.** It reaches the GPU, not the file:
call `saveDefinition` to write the current values into `mat.yaml`. For a
per-entity override that no other entity sees, give the entity its own copy
with `Model:applySessionMaterial(renderer.material.create(...))`.
- **Renaming the folder changes the identity.** A `Model` component or
other reference to the material is recorded against the material
itself, so renaming or moving it does not break them.
## Related types
- `.shader` — defines what properties exist; required before a
material can bind to it.
- `.preset` — captures a component configuration; conceptually
similar to a material, but for components instead of shaders.
# assets
Find content and trace how it's used — search the project's assets, see what an entity is made of, and find who uses a given asset.
Search the whole asset index by substring and get every match grouped by category, without knowing where anything lives. Point at one entity, a list, or the whole scene and get back the unique set of asset references in use — materials, meshes, textures, bundles, and anything else a component field holds — each enriched with its identity, path, and the entities that use it. Or go the other way: name an asset and find every entity in the active layer that references it, down to the component and field.
# asset-system guide
Reference docs surfaced through `guides { path: "..." }` and `man asset-system`.
The content lives in `guide.md`; this README is just the asset summary.
# assetType (asset type)
An asset type says what a kind of asset is: the files an asset of the kind is
made of, the code that gives it behaviour, the interface a person uses to work
with it, and how it is found, pictured and checked. A `<typename>.assetType/`
folder declares a type called `<typename>`, and every `<name>.<typename>/`
folder is an asset of it. `assetType` is itself a type, so a type is an asset
like any other: it is created, versioned, installed and contributed the same way.
## Making a type
`asset.create("assetType", "<typename>")` scaffolds a type. The type registers
the moment its folder is written, anywhere under `/zero/source/`, and from then
on `<name>.<typename>/` folders are its assets. An asset of a type that is not
registered yet is not an asset, so make the type first.
A type folder is a root: `~.x` in any file inside it names the type's own `x`,
wherever the type is installed. Keep the type's whole implementation inside its
folder and reach it that way.
What a type folder must hold is set by this type's own `type.yaml`, the same way
every type's `type.yaml` sets what its assets hold. `asset.validate` on a type
reports anything missing, and the scaffold produces a folder that passes.
## What sets your type's schema (`type.yaml`)
`type.yaml` is the structural contract every asset of the type is checked
against. The keys below are the ones the engine reads; a key not listed here is
ignored.
| Key | What it controls |
|---|---|
| `suffix` | The folder suffix of the type's assets. Defaults to `.<typename>`. |
| `primary` | The file or files that are the asset's content: what the asset resolves to when it is used. |
| `entry_files`, `primary_aliases` | Further files that load as the asset or share its identity. |
| `is_container` | Whether the suffix stays in the identities of what the asset holds. |
| `is_opaque_walker` | Whether a walk of the tree stops at the asset and registers only its primary. |
| `plural_dir`, `classify_exts` | Loose files in a folder of this name, or with these extensions, are assets of the type. |
| `is_text_default` | Whether the asset's files are text unless stated otherwise. |
| `broadcasts_subassets` | Whether assets nested inside an asset of the type are found by their own short name. |
| `root` | `true` makes every asset of the type a root, so `~.x` inside one names that asset's own `x`. |
| `contract_ref: { field, category }` | A key of the primary file that names another asset the asset is built on. |
| `files: { required, optional }` | The files an asset holds (below). |
| `allow_unlisted` | `false` reports every file the type does not declare. |
| `aliases_from: [{ file, key }]` | Further names an asset answers to, read from its own files. |
| `settings:` | Typed per-asset settings (below). |
| `inspector:` | The interface people use for the type and its assets (below). |
| `preview:` | How an asset is pictured (below). |
| `references:` | Names of other assets the type's files carry (below). |
| `indexing:` | What search reads for each asset (below). |
| `describe:` | How a missing description is generated (below). |
### The files an asset holds
Each entry under `files.required` or `files.optional` is
`{ path, kind: file | folder, one_of_group?, content? }`. `*` in a path matches
within one segment. Entries that share a `one_of_group` are satisfied by any one
of them. `content` lists checks on the file's bytes:
| Check | Passes when |
|---|---|
| `non_empty` | the file has content |
| `json_field { key, type?, non_empty?, accept? }` | the JSON holds the key (a dotted path) with that shape |
| `yaml_field { key, type?, non_empty?, accept? }` | the same for YAML |
| `described { keys?, headings?, message? }` | the file says what the asset is (see "Ask your assets to say what they are") |
Each check takes a `message`, which is what the author is told when it fails.
### Settings
`settings: { values, schema: { type | instance, export? } }` gives every asset
typed settings. `values` names the file an asset's values live in; `schema.type`
declares one schema for the whole type, and `schema.instance` names a file each
asset declares its own in. `asset.create` writes the resolved values, and a write
to the values file reaches `onChange` with what changed.
## The human side (`inspector.luau`)
A type is used by people through its Inspector, and without one the type is
usable only from code. The Inspector is the type's interface for a person: for a
procedural graph it is the graph editor, for a kit it is the tracking view of
what the kit holds and what is still missing. Writing it is part of writing the
type.
`inspector: { type, instance? }` in `type.yaml` names the files. `type` is the
interface for the type itself and for its assets; `instance` names a file an
asset may ship to add its own. Each returns:
```luau
return {
sections = { { id = "graph", title = "Graph", build = function(ctx) ... end } },
replace = { contents = { ... } }, -- replace a default section by id
order = { "graph", "contents" }, -- section order
}
```
`build(ctx)` receives the asset and builds the section. A section's `title`, and
the text its rows carry, is a string or an `i18n.text` value read from a catalog
(`i18n.text("inspectors.mesh.title")`), which follows the editor's language. `asset.create("assetType",
...)` scaffolds a working `inspector.luau` to start from. `asset.validate` on a
type that names no inspector, or names one that does not load, reports it.
## Say what your type is for (`about.yaml`)
Every type carries an `about.yaml` that says what the type is for, in three
plain-language texts:
```yaml
intent: "Sets the typeface of the text in your game: UI, labels, signs and 3D lettering."
function: "A font, from a TTF, OTF or WOFF file, made available to all of Zero's text, including 3D text."
usage: "You choose it by name for your UI or your text, and the text appears in that typeface."
```
- **`intent`** is what the type is for in someone's game, in the words a game maker uses. Up to 800 bytes.
- **`function`** is how it works today: what an asset of it is made of and what the engine does with it. Up to 1500 bytes.
- **`usage`** is what a person or an agent does with one. Up to 1200 bytes.
Write about the type as a whole, not one asset of it, and say what it does now.
Agents read these texts to learn when to reach for the type and how; people
searching find the type by them. The scaffold starts each text with
`[[describe]]` and a note on what to write; a text that still starts that way
counts as not written. `services describe` can write them.
They show up in `asset.inspect` as `detail.about`, in
`assetTypeRef:getAbout()`, at the end of the type's `types/<typename>` guide,
and at the top of the type's Inspector.
## Ask your assets to say what they are
A type can require each asset to carry a description: a file, written by the
author or generated, saying what the asset is and what it is for. Declare it with
a `described` check:
```yaml
files:
required:
- path: "README.md"
kind: "file"
content:
- { check: "described", message: "Say what this widget is for and where it goes." }
```
A file that is missing, empty or still has a line starting with `[[describe]]`
fails the check, and so does a YAML file whose named `keys` are missing or over
their `max_bytes`, or a Markdown file missing a named heading. `asset.validate`
names the asset, and a push that carries it is refused with `message`.
`services describe` finds such assets and writes their descriptions.
`describe:` steers what is generated:
```yaml
describe:
guidance: "Say what the widget shows, what input it reacts to, and where it fits in a UI."
examples:
- "<identity of an asset of this type whose description reads well>"
```
An example is an asset of your type whose description is a true, written one,
never a placeholder and never the asset being described; with none, leave
`examples` empty. A type whose assets the model cannot perceive (a sound, a
motion) says so in `guidance`, and the description is drawn only from what the
asset's own files state.
Start the file unwritten in your `template/`: a heading and one line starting
with `[[describe]]`, never example content, which every new asset would carry
verbatim. A type whose assets say what they are through their own content (a
mesh, a texture) declares no check.
## The type's code (`behavior.luau`)
`behavior.luau` returns one table. These are the keys the engine reads from it;
anything else on the table is the type's own.
### What the type's assets can do
| Key | Shape | What the engine does with it |
|---|---|---|
| `ref` | `{ [name] = function(self, ...) }` | Every function becomes a method on every asset of the type: `ref:name(...)`. |
| `modules` | `{ [name] = require(".x") }` | Code the type's assets reach with `asset.containing(__FILE__).modules.<name>`. |
| `events` | an events schema | Events an asset fires. Readers subscribe through `ref.events.<name>:connect(...)`; the type fires them. |
| `refShapes` | `{ [method] = function(self) -> (typeExpr, source?) }` | The result type of a `ref` method for one particular asset (below). |
| `namePattern` | a Lua pattern | The names `asset.create` accepts for the type's assets. Default `^[A-Za-z][A-Za-z0-9_]*$`. |
| `derive` | `{ output, module, compute, fingerprint, matches?, required?, timeout? }` | A file computed from the asset when it is committed; a `required` one holds the commit until it lands. |
Some `ref` names mean the same for every type, and the engine calls them:
- **`instantiate(self, target?, opts?) -> (root, idMap)`**: puts the asset in a scene. Defining it is what makes the type scene-instantiable. Its return is a contract (below).
- **`inspect(self) -> detail`**: what `asset.inspect` reports for the asset beyond the common record (below).
- **`preview(self, opts)`**: renders the asset's picture; it wins over the type's `preview:` recipe.
- **`previewMaterial(self) -> { material }`**: the material a `surface` preview paints.
- **`saveDefinition(self)`**: writes the asset's in-memory edits back to its files in edit mode.
Some names are answered for every asset before the type is asked, so a `ref`
entry of the same name is never reached: `getSource`, `getBytes`, `getText`,
`exists`, `deps`, `canInstantiate`, `canPreview`, `preview` (which calls yours
when you define one), `meta`, `runtime`, `events`, `typeRef`, `modules`, and the
residency flags `has_backing_asset`, `has_runtime_changes`, `cpu_resident`,
`gpu_resident`.
### When the engine calls the type
| Hook | Signature | When it runs |
|---|---|---|
| `onCreate` | `(name, opts) -> { [path] = contents }` | On `asset.create`. Returns the files of the new asset, laid over the type's `template/`. Annotate `opts`: its type is the schema `asset.create` checks the caller's arguments against. A `""` key writes a single loose file. |
| `onReplace` | `(dest, files) -> files` | On an `asset.create` over an asset already at `dest`, after `onCreate`, so what the author added can be carried into the replacement. |
| `onRegister` | `(self)` | Once per asset, when it first registers: when the world loads, on a live `asset.create`, and when an asset arrives later (installed, pulled, synced). This is what makes an asset take effect the moment it is written. |
| `onChange` | `(ref, change)` | On every write to a file inside an asset of the type. `change` is `{ path, asset, type, kind, origin }`, plus `settings` and `settingsChanged` for a write to the settings values file; `kind` is `edited` or `seeded`, `origin` is `local` or `remote`. Filter on `change.path`. Writes to one asset arrive in order, each after the hook for the one before has finished, and the hook may wait on what it reads. A write the hook makes inside its own asset arrives later like any other, so keep the hook convergent. |
| `onDependencyChange` | `(ref, change)` | When a module the asset's code requires is re-run, or a name the asset carries starts resolving. `change.kind` is `dependency`. |
| `onDelete` | `(ref)` | When an asset of the type leaves the world: deleted, or the world it is in is swapped away. |
| `validate` | `(ref) -> { { code, message, severity?, path? } }` | On `asset.validate`, after the `type.yaml` checks, for the type's own rules. `severity` defaults to `error`. Runs on every push, so a type's rules hold at publish. A hook that fails reports `validate.hook_failed`. |
### Settings hooks
| Hook | When it runs |
|---|---|
| `settingsDefaults(name, opts, asked) -> table` | Once per `asset.create`, for values the schema's defaults do not give. |
| `settingsCorrections(values, name, opts) -> (corrections?, reasons?)` | On every settings write. A correction to a field the writer named refuses the write, giving the reason. |
| `settingsDerivedDefaults(values, explicit, changed)` | On a settings write, for fields derived from others. |
| `normalizeSettings(values, stored?)` | On every read and write, to map an earlier spelling of a field to the current one. |
| `settingsHealed(values, stored)` | On a read, to repair stored values. |
| `settingsSchemaProblem(schema, ref) -> string?` | When an asset's own schema is read; a returned string refuses the schema. |
`creationInputs` (`{ [opt] = true | selectorOpt }`) names `asset.create` options
that are inputs to the type rather than settings.
### Reloading
An edit to `behavior.luau` reaches existing assets with no restart.
## How an asset is pictured (`preview:`)
| `kind` | The picture is | The type provides |
|---|---|---|
| `instantiate` | the asset placed in the preview studio through its `instantiate` | `ref.instantiate` |
| `image` | a file the asset holds, fitted to the frame | `source: { files }`, the candidate files in order |
| `surface` | the asset painted on a mesh | `mesh`, and `ref.previewMaterial` |
| `custom` | whatever `ref.preview` returns | `ref.preview` |
```yaml
preview:
kind: surface
mesh: sphere
```
`angle: [yaw, pitch]` turns the camera. The studio owns the light, the backdrop
and the framing, so an asset pictures the same in every world. A type with no
`preview:` that defines `instantiate` still answers `ref:preview()`.
A picture is rendered when something asks to view it (the editor showing the
asset, `ref:preview()`), when previews are generated on request
(`assets.generatePreviews`), and when the asset is staged. An edit alone
renders nothing. The picture records what it was made
from, so a stale one is told from a current one.
## The names your type's assets hold (`references:`)
Where your type's own format or API carries the name of another asset, declare
it here:
```yaml
references:
keys: # a top-level key of the type's files that holds a name, or a table of them
- { key: "shader", type: "shader" }
args: # an argument of a call, counting from 1
- { call: "g:node", position: 2, type: "procNode" }
fields: # a field of the options table a call takes
- { call: "fx.beam", field: "material", type: "material" }
```
`type` is the category the name resolves in. `asset.declareReferenceKey`,
`asset.declareReferenceArg` and `asset.declareReferenceField` declare the same
from a script.
## What search reads (`indexing:`)
`indexing:` is how assets are found. Search reaches an asset only through what
its type's `indexing:` names, and adds nothing of its own. That includes you: an
asset indexed badly is one you cannot find later, when you are making something
else and need it. A type whose assets are never looked for says `indexing: none`.
```yaml
indexing:
data: # text, read from the asset's files
- role: identity
extractor: verbatim
source: { file: "README.md" }
- role: interface
extractor: symbols
source: { files: ["init.luau"] }
- role: implementation
extractor: verbatim
source: { files: ["init.luau"] }
content: # the picture
- role: identity
modality: image
source: { file: "preview.png" }
facets: # what a query filters and re-ranks by
file: ".metadata"
rigged: { type: bool, from: { components: { contains: SkinnedModel } } }
roughness: { type: number, filter: range, from: { file: "mat.yaml", field: "floats.roughness" } }
```
**Roles.** Each `data` and `content` entry has a `role`, and a query matches in
two slots:
- **`identity`**: what an asset is. For anything you look at, the picture; for anything else, the text that says what it is.
- **`capability`**: what it does and how it is made. `interface` (the signatures or shape a caller uses) and `implementation` (the body) both count as capability, and a type with code usually declares both.
Ask what a person would recognise an asset of the type by, and what it does, and
point each role at the file that says it. For text, that is the asset's
description: the file your template starts with a `[[describe]]` block, which the
author writes or `services describe` fills in (see "Ask your assets to say what
they are").
**`data`.** `extractor: verbatim` embeds a file's text as it is; `symbols` embeds
the declared signatures of code. `source` is `{ file }` or `{ files: [...] }`.
Never index a file name (`source: { field: name }`): one
or two words carry nothing to match.
**`content`.** `modality: image` embeds the picture itself, so a text query
matches what the asset looks like. A type whose assets are looked at makes the
picture its identity.
**`facets`.** What a query narrows and re-ranks the matches by. `file` is embedded
whole, so keys nobody declared in it are still found. Each further key is
computed for every asset and states a value for every one, including `false`: a
file can imply "not rigged" but never say it, and a query separates rigged from
unrigged only because the unrigged ones say `rigged: false`. A key is
`{ type: bool | number | enum, filter?: range, from }`, where `from` is
`{ file, field }` (a field of one of the asset's files, dotted for nesting),
`{ metadata: key }`, or `{ components: { contains | any_outside } }` (what the
asset's entities carry).
## What blocks a push
A push checks every asset it carries against its type. These refuse it:
- a required file that is missing, a `content` check that fails, a `described` file not written yet;
- an error from the type's `validate`.
Unlisted files and warnings do not. A `described` file blocks only a push that
changes the asset. When a push is refused, the message names each asset and what
it lacks. `world.publishBlockers()` and `zm status` read the same list without
pushing.
## Typing a result from the asset (`refShapes`)
A `ref` method whose result depends on the asset (one entry per child folder, per
row of a config, per control a map declares) can only be annotated as
`{ [string]: Thing }`, which accepts any key. `refShapes` states the result for
each asset instead: a function returning a Luau type expression for that asset,
and the module whose types the expression uses. Read the asset; never run it.
```lua
-- inputMap.assetType/behavior.luau
M.refShapes = {
activate = function(self): (string, string)
local names = {}
for _, control in ipairs(M.ref.controls(self)) do
table.insert(names, control.name .. ": Handle")
end
if #names == 0 then return "", "" end
return "{ " .. table.concat(names, ", ") .. " }", "@builtin::modules.zinput.scheme"
end,
}
```
With it, `map.jump:onPressed(fn)` is checked and `map.noexisting` is reported.
A per-asset type applies where the call site names which asset: a component
field whose default names it, or an annotation `AssetRef<"inputMap", "<identity>">`.
`asset.resolve(name, category)` carries only the category, and a name computed
at runtime carries nothing, so calls on those keep the declared return. The type
belongs to the call's result: index it there, or bind what you need from it
there, and the names stay checked.
`require("@builtin::assetTypes.assetType.shared.refShapes").published()` shows
what was published: `categories` (each type's method surface) and `returns`
(per-asset results by identity, then method). An identity missing from
`returns` was never published. Results are recomputed when an asset is written;
at most 64 assets of one type are read.
## Putting an asset in a scene (`ref.instantiate`)
A caller writes one piece of code against every instantiable type, so what
`instantiate` returns is fixed:
```lua
local Instantiable = require("@builtin::assetTypes.assetType.shared.instantiable")
M.ref = {
instantiate = function(self, target, opts)
local root = Instantiable.root(self, target, opts)
-- compose the asset under `root`, now
return Instantiable.result(self, root, idMap)
end,
}
```
**In.** `target` is the entity the asset lands under, or onto for a hierarchy
type; with none, the type spawns a new root. `position`, `rotation` (three
numbers in degrees, or a quaternion), `scale`, `name` and `temporary` mean the
same for every type: `Instantiable.root` applies them to a root you make,
`Instantiable.place` to one you adopt. Document any options of your own in the
hook's `--!arg` docs.
**Out.** `(root, idMap)`. `root` is the composed root, live when you return:
build synchronously, never defer to a component's `awake`. `idMap` maps each
original id to the spawned one, so a component that composes the asset again on
every load can pass it back in and keep references into the composition. Every
call goes through `Instantiable.result`, which refuses a root that is not live
and turns a missing `idMap` into `{}`.
## Inspecting an asset (`ref.inspect`)
`asset.inspect(ref)` builds a record every asset shares (`identity`, `name`,
`guid`, `source`, `typeName`, `typeDefinitionPath`, `scope`, `origin`,
`description`, `tags`), then calls `ref.inspect(self)` for the type's own
`detail`:
```lua
M.ref = {
inspect = function(self)
-- read the asset's own files, never runtime state: inspect works on an asset
-- that has not been compiled, uploaded or registered
return { --[[ what makes this asset what it is ]] }
end,
}
```
Without it, the record is the shared part alone, and an agent inspecting the
asset learns nothing of what it is. A hook that raises leaves `detail` empty and
sets `record.warning`.
## Discovery
- `asset.list("assetType")`: every registered type.
- `asset.inspect("<typename>")`: the type, its README and its `about.yaml` texts.
- `asset.resolve("<typename>", "assetType")`: the type itself.
# Component (asset type)
A component is a reusable behavior + data unit attached to an entity.
The component file declares public fields, methods, and lifecycle
callbacks; the engine instantiates one per attachment and drives its
hooks every frame. Components are the primary unit of gameplay code —
movement, AI, animations, UI panels, network sync, and per-entity state
all live here.
## When to use one
- You need per-entity state that survives across frames.
- You need per-entity behavior driven by engine lifecycle hooks
(`start`, `update`, `onDestroy`).
- You want to expose configurable fields to designers / `entity.spawn`
callers / scenes without code changes.
If you need a stateless pure-function library, use a `.module`. If you
need to generate content declaratively, use a `.service`; compute work
belongs in a `.computeShader`.
## Where it lives
- Source: `/zero/source/.../<Name>.component/`
- Identity: `<Name>` (the `.component` suffix strips from the
identity, but the folder retains the suffix on disk).
- Folder shape:
- `init.luau` (or `init.lua`) — declares the component. **Required.**
- `README.md` — instance-level documentation. **Required.**
- `.metadata` — agent-editable tags + free-form fields. **Required.**
The structural contract is enforced by `asset.validate` against this
type's `type.yaml`.
## How to create one
Always start with the scaffold — it guarantees the folder shape
matches `type.yaml` from the moment it's on disk:
```luau
asset.create("component", "<Name>")
-- Creates: /zero/source/<Name>.component/
-- init.luau (canonical declare + public block + hooks stubbed)
-- README.md (instance README template)
```
After scaffolding, edit `init.luau` to fill in the actual fields and
behaviour. The component registers as soon as the file is written —
hot-reload is automatic; no engine restart required.
## How it operates
1. **Registration.** Writing `init.luau` into a `.component/` folder
indexes the asset, mints a guid (in the `.meta` sidecar), and
surfaces the type to the engine's component registry. Same-frame
visible to `entity.component.add`.
2. **Attachment.** `entity(id).component.add(name, data)` (or the
batch form `tools.use("entityOps", "addComponents", targets,
{ [name] = data })`) attaches an instance.
The `data` table's fields override the component's declared
defaults; omitted fields take the declared default. The `data`
values are copied into `public.*` BEFORE `awake()` runs.
3. **Lifecycle.** The hooks run in this order:
- `awake()` — same frame as attach, immediately after the data
copy. Initialise state, set up native bridges, register with
other systems. Other components on the same entity may not have
awoken yet.
- `start()` — the next frame. Every component on the entity has
completed `awake()` by then, so cross-component setup is safe
here.
- `update(dt)` — same frame as `start()`, then every frame after,
for as long as the gameplay clock is running. `fixedUpdate(dt)`
runs once per physics step, straight after it, with the step's
`dt`, so a frame can run it several times or not at all;
`editorUpdate(dt)` runs
while the clock is held. Which of them ticks is decided by
`engine.mode` AND `engine.paused` together — see **Per-frame
hooks and the two axes** below, and `man modes`.
- `onDestroy()` — when the component is removed via
`component.remove` or when the owning entity is despawned.
Release resources, drop native ECS components.
For the full list (`onEnable`, `onDisable`, `fixedUpdate`,
`editorUpdate`, `onPropertyChanged`, `onAssetReload`,
`onModuleReload`,
`onBeforeSave`, …) and the timing details, **read `man components`
— that is the canonical lifecycle reference. Generic component
patterns (declare blocks, public fields, sync, ECS bindings,
typed methods) all live there.**
4. **Public surface.** Fields declared in `public` are inspectable,
editable in the inspector, and persist with the entity. Methods are
declared on that same table with a colon —
`typed function public:method(args)` — and every one of them takes
the component ref as `self`, so a caller invokes it with a colon
too: `entity(id).component.get(name):method(args)`. That is the one
calling convention a component ref answers to: the reflection
methods `ref:fields()` / `ref:getField(name)` /
`ref:setField(name, value)` take the same spelling. A function the
chunk declares at file scope (`function helper()`) is the
component's own, reachable from its hooks under its bare name;
`public` is what the ref carries.
5. **Settings and the Inspector card.** `type.yaml` names the component's
settings schema as `settings: { schema: { instance: "init.luau",
export: "public" } }`: the `public` table IS the schema, and its values
live on each entity the component is on, so `asset.schema(componentRef)`
answers the Field descriptors and no values file exists. On an entity
the Inspector shows the component as a card whose generic `settings`
section edits those fields as typed controls, each edit checked against
the schema and written to the live component as one undoable operation.
`type.yaml` also names `inspector: { instance: "inspector.luau" }`: a
component that ships an `inspector.luau` beside its `init.luau` shapes
its card — the file returns `{ sections, replace, order }` (see
`assetTypes/README.md`, "`inspector:`"), each section's `build(ctx)`
reading `ctx.component` (the live component) and `ctx.selection.id` (the
entity). `SceneModule.component/inspector.luau` replaces `settings` with
the bound module's declared inputs.
6. **Events.** An `events` block declares typed moments the component
fires from inside — `events = { onHit = Event({ dmg =
Field.number(0, NoSync) }) }`. The owner fires with
`events.onHit:fire({ dmg = 10 })`; outside code subscribes through
the proxy's subscribe-only facade,
`entity(id).component.get(name).events.onHit:connect(fn)`
(`:once` / `:wait` too). Pass `Sync` as the event's second argument
to replicate a fire to other peers. Events let one component react
to another's moments without polling. Full reference: **`man
components`**, Events section.
7. **Hot reload.** Editing `init.luau` reloads the component
definition; live instances are reconciled with the new declaration
in-place, and each crosses one lifecycle boundary: `onDestroy()`
ends the running life, then `awake()` and `start()` construct the
one that replaces it, and `update()` resumes on the next frame.
`public` and `private` values carry across; module-level `local`s
belong to the chunk, so the new life starts with them at their
initial values and its `awake`/`start` are what fill them in.
Whatever the old life handed elsewhere — a subscription, a shared
registration, a spawned entity — is released in `onDestroy`, the
call that pairs with the construction about to run. An edit that
leaves the source byte-identical crosses no boundary at all.
A field carries its value across when something stated it: a key of
the `data` table the instance was attached with (a scene record's
data is that table), or a write made since. Every other field takes
the edited source's declared default, so changing a default reaches
every instance that left the field to it. An instance that states
its own value for a field whose default the edit changed keeps that
value, and the reload logs a `[HotReload]` warning naming the field,
the entity, the value kept and both defaults.
## Discovery
- `asset.list("component")` — every registered component (engine +
world + libraries).
- `asset.inspect("<name>")` — identity, scope, public fields,
lifecycle methods, source path, and this type README. `detail`
carries the component's declared `fields`, `methods`, and `events`,
parsed from its source — `tools.use("assets","describe", "<name>")`
renders it as markdown, so a component's events are visible without
opening its source.
- `ls /zero/source/<Name>.component` lists the files the folder holds.
- `entity(id).component.list()` — components attached to an entity.
## Per-frame hooks and the two axes
`engine.mode` (`"edit"` / `"play"`) and `engine.paused` (the gameplay
clock) are independent, and the per-frame hooks read them separately:
- `update(dt)` / `fixedUpdate(dt)` tick while **the gameplay clock is
running** — whichever mode the engine is in.
- `editorUpdate(dt)` ticks while **the clock is held** — because the
mode is `edit`, or because play is paused — and also alongside
`update` whenever edit is running.
Which gives four states, and this table is the contract:
| `engine.mode` | `engine.paused` | hooks that tick |
|---|---|---|
| `edit` | `true` | `editorUpdate` |
| `edit` | `false` | `update`, `fixedUpdate`, `editorUpdate` |
| `play` | `false` | `update`, `fixedUpdate` |
| `play` | `true` | `editorUpdate` |
Entering edit sets `paused = true` and entering play sets it back to
`false`, so a session that never writes `engine.paused` only ever sees
rows 1 and 3 — which is where the shorthand "`update` is the play tick,
`editorUpdate` is the edit tick" comes from. It is the two default rows
of a four-row table, not the rule.
**Row 2 is the one that surprises.** With `engine.paused = false` in
edit — set by hand, or by a tool that advances the gameplay clock to
sample it, such as a `capture.collage` given a `duration` — `update` and `editorUpdate`
both fire, once each, every frame. A component that declares both and
routes them to one body therefore runs that body **twice per frame**:
```luau
local function drive(dt) ... end
function update(dt) drive(dt) end
function editorUpdate(dt) drive(dt) end -- twice per frame in row 2
```
Nothing reports the doubling; it reads as an animation running at twice
its intended speed. To get exactly one tick per frame in every row, ask
which axis you meant and answer it in the body:
```luau
function update(dt) drive(dt) end
function editorUpdate(dt) if engine.paused then drive(dt) end end
```
A scene `entrypoint.luau` declaring the same two hook names is dispatched
by the scene loader, not by this table: there `update` needs play AND
running, `editorUpdate` needs edit, and play-paused ticks neither. Read
`man scenes` before carrying a component's rule to an entrypoint.
## Authoring conventions
- Annotate public fields with `--!desc` so the inspector + LSP show
documentation alongside the field name.
- Declare lifecycle hooks (`start`, `update`, `onDestroy`) explicitly
rather than relying on engine defaults — readers can see at a glance
what runs when.
- Keep `update(dt)` cheap — it runs every frame per instance. Move
heavy work to `start`, to a `.computeShader`, or behind a job (`man jobs`).
- For multi-entity batching, prefer
`tools.use("entityOps", "addComponents", targets, components)` over
a loop of `entity(id).component.add` — it takes many targets and
many components in one call.
## Common pitfalls
- **Naming.** The component identity is the folder stem with
`.component` stripped (e.g. `<Spinner>.component/` → identity
`Spinner`). Don't repeat the suffix in the identity passed to
`component.add`.
- **`init.lua` vs `init.luau`.** Either is accepted (it's a
`one_of_group: "entrypoint"`), but `.luau` is canonical. Mixing both
in one folder is invalid.
- **`update` in edit mode.** It runs whenever the gameplay clock is
running, and entering edit stops that clock — so `update` is idle in
a freshly-entered edit session and ticks again the moment anything
sets `engine.paused = false`. Use `tools.use("wld", "play")` to test
update logic under the mode it ships in; `tools.use("wld", "edit")`
to return. See **Per-frame hooks and the two axes** above.
- **Renaming.** Renaming the folder changes the identity. References
to the component are recorded against the component itself, so
renaming or moving it does not break them.
## Related types
- `.module` — pure-function library, no per-entity state.
- `.service`: a declarative content generator, no per-entity state.
- `.preset` — a captured component configuration, replayable into
`component.add` without re-typing fields.
- `.bundle` — entity hierarchies that may carry component attachments
as part of the bundle.
## In the Inspector
Fields: its execution order, hooks, and how many methods and events it declares, then the fields its `public` table declares as read-only typed controls holding their defaults, with **Check** and **Open** for its entry file. Instances: a link to every entity carrying it. What its card shows on an entity comes from the component's own `inspector.luau`.
# assets.describe
Agent-facing inspect surface. Resolves `assetName` the same way
a search hit's name resolves, calls `asset.inspect` for the structured
record, and renders it to markdown via `modules/asset_inspect_render`.
Pairs with `assets.search` (search -> describe).
Named `describe` — never `inspect` — so it is never confused with the
`asset.inspect` code API (one letter apart is a trap).
# capture
Render a screenshot of the scene from anywhere — an explicit position, a named camera station around an entity, a specific camera, or a mirror of the active viewport — and a grid of them in one image; or read back the frame the engine actually presented, exactly as the player saw it.
The picture of the machine's own camera (a webcam, a phone camera) is `cameraInput`, covered by the `topics/camera-input` guide; this toolbox photographs the scene.
Every source but `presented` and `window` (see [The presented frame](#the-presented-frame)) spawns an ephemeral offscreen camera, triggers one render, and returns the picture as an image value — returned from a tool, a probe or `execute`, it reaches you as a picture, with what the capture has to say about it (its size, the frame it is of, the cells of a grid) beside it. A PNG is written as well when you pass a `path`. The convenience wrappers render from a world-space position (with optional look-at), frame a target entity, mirror the active viewport camera including its layers, or render a specific camera by name from its exact authored pose and settings (`fromCamera`, with optional per-capture overrides that never touch the camera); each waits for the render and the GPU readback, cleans up its camera, and returns only once the picture is read back. The low-level oneshot spawns the camera, triggers one render, and returns the handles at once without waiting, leaving the readback and cleanup to the caller; the `capture` MCP tool is built on it. A cleanup tool clears the capture output directory.
## Where a shot lands
A capture given a `path` writes the PNG there and says where those bytes
stand, beside the path it wrote: `durable` is true when the PNG is on the path
it names and the next session reads it back, and false when the play shadow
took it (live in this session and discarded on a guarded play-exit unless kept,
with `survivesRestart` saying whether a restart keeps it and `playShadow`
naming the routes that keep it) or when the path is in `/source/tmp/` scratch
(gone when the engine stops or restarts, with a `warning` opening `SCRATCH`).
Every capture carries it on the picture it returns as `durability`. Shooting
gameplay means shooting in play, which is exactly when a `/source` destination
is the shadow's, so this is the field that says whether a shot survives leaving
play — `vfs.promotePlayShadow(path)` keeps it with play still running.
## The presented frame
`source = "presented"` reads back the frame the engine presented to the
player instead of rendering one. Nothing is drawn for it: it is the frame the
viewport drew, copied out as it was finished, after the post chain, the
upscale, the display transform, the content UI and every post-scene pass, at
the size it was composed at. So it is the one capture whose temporal
antialiasing, motion blur, camera motion, auto exposure and render scale are
the real ones, and the source to verify any of them with. `source = "window"`
is the whole presented surface (the window, the browser canvas or the
headless framebuffer) with everything painted on the window: the editor's
chrome and every window-level UI pass. Where no Scene panel shows the
viewport, the two are the same image: the scene fills the window and the
editor's chrome is painted over it, and each such frame says so in its
`note` (a cell of a `presented` capture, a frame `renderer.presentedFrames`
returns) for as long as that chrome is on the window. Show the
Scene panel (the editor toolbox's `layout` tool) to read the game view alone.
`frames = n` (1-32) takes n presented frames, one every `interval` frames
(default 1: consecutive), at real gameplay speed, and lays them out as a grid
in the order they were presented. Each cell states what its frame was drawn
with: `frame` (the renderer's count, so consecutive frames carry consecutive
numbers), `rasterWidth` / `rasterHeight` (the size the scene rasterized at),
`compositeWidth` / `compositeHeight`, `renderScale` and `scaleMode`, `jitter`
(the projection's sub-pixel offset in raster pixels), `exposure` (what the
display transform multiplied the scene by) and `historyReset` (the viewport
camera's first frame, a cut, or a hand-over to another camera). A single frame
comes back at the size it was presented at; `width` / `height` resample it, or
size the sheet a grid is laid into.
A presented frame is drawn through the camera, layers, lens and chain that
drew the screen, so the options that aim, light or shape a rendered frame
(`camera`, `entity`, `position`, `viewpoint`, `fov`, `pass`, `renderLayers`,
`postProcessing`, `environment`, `mode = "collage"` and the collage axes) are
refused for it; aim the camera holding the viewport instead, or take an
offscreen capture. A source that renders refuses `frames` and `interval` the
same way, since they count presented frames. `deterministic` and `clearAir`
hold the clock and the air for the whole engine, so they reach the presented
frame too.
A diagnostic buffer of the viewport itself, under its real camera motion and
history, is read the same way: put the camera holding the viewport on that
pass (the camera toolbox's `set` with `debugPass = "motion_vectors"`) and take
`source = "presented"`. The viewport's velocity buffer is the frame's travel
against the frame before it, so a turning camera reads in it, where an
offscreen `pass = "motion_vectors"` holds the objects' own motion alone.
The `motion_vectors` picture is a visualization: each channel is the square
root of 100 times the stored displacement's size on its axis (R horizontal, G
vertical, B the length), the sign is dropped, and R reads 255 from
`width / 200` px of horizontal travel, G from `height / 200` px of vertical
travel (raster pixels). The rendering guide's part on motion
(`topics/rendering-motion`), "What the `motion_vectors` picture encodes", states it in full.
The same frames, with their pixels as `TextureCpuHandle`s, come from
`renderer.presentedFrames({ scope = "viewport" | "window", frames, interval })`,
and `velocity = true` adds the viewport's velocity buffer from each frame as
numbers (`frame.velocity`, a `VelocityField`): each pixel's travel in pixels
per frame, x right and y down, signed and unclamped, read with
`field:at(x, y)`, `field:row(y)` and `field:region(rect)`.
## The feed
Every capture publishes the frame it made to `@builtin::tools.capture.feed`,
so a viewer can show the newest one without taking a shot of its own.
`Feed.latest()` is that frame: a `src` an image widget draws (the render
target's guid while the engine still holds it, the written path when the
capture produced a file), with the size, the source and the pass beside it.
`Feed.watch(fn)` calls `fn` as each new frame arrives. The editor's Capture
panel is built on it, and the `watch` layout puts that panel under the scene.
A viewer calls `Feed.shown()` to say it is drawing frames from the feed. From
then on the feed holds the newest capture's render target so the picture stays
up after the capture that made it is finished with it, and lets it go when a
later frame replaces it; `Feed.shown(false)` stops. With nobody drawing, a
capture's target is released the moment its capture is done, and nothing is
held.
## Viewpoints
A **viewpoint** names where the camera stands and which way is up in the image: `front`, `back`, `left`, `right`, `top`, `bottom`, `iso`. It replaces reverse-engineering a `{yaw, pitch}` pair, and names the seven stations that answer "is this built correctly".
`basis` says which axes the name is measured against, and it is **required** alongside a viewpoint when a subject is being framed:
- **`"local"`** — the subject's own axes. `front` is the side it faces however it is turned, and the frame is fitted to its own extents rather than to the world-axis box around them.
- **`"world"`** — the world axes. `front` is whatever faces world +Z.
For a crate turned 40 degrees those are different pictures, so the call says which one it wants. Framing a world position rather than an entity, both values mean the world axes and either is accepted.
## Passes
`pass` names **what** the frame holds, by name. `"final"` is the lit picture; a built-in diagnostic pass (`albedo`, `normal`, `depth`, `motion_vectors`, …) replaces the shaded image with the buffer behind it; and any capture **view** a render feature has published (`renderer.captureView.list()` enumerates them) is selectable by its own name. A name that is neither is refused against the list of what is registered.
The name carries two decisions: which channel the camera renders, and which render layers that channel is legible under. A diagnostic buffer is read flat, so the sky behind it and the post-process chain over it come off; a view that draws its own geometry declares the layers it needs and is rendered under them. Stating `renderLayers` answers that second question yourself, for the one call.
`passes` spreads the same vocabulary across the cells of a collage, and each cell reports the layers it was read under.
## Projection
`projection = "orthographic"` covers a fixed world-space height at every distance. Parallel edges stay parallel and two equal-size objects at different depths cover equal pixels, which is what makes it the projection to check a shape or compare sizes under — a perspective frame's convergence hides both. `orthoHeight` sets that height; omit it and it is fitted to what is being framed.
Any camera can be orthographic, not only a capture: it is a field on the `Camera` component. A capture through a camera the scene holds (`fromCamera`, `viewport`, and `source = "camera"`, `"screen"` or the scene camera) takes that camera's `projection` and `orthoHeight` with the rest of its lens, and its lighting `environment` with an `isolated` camera's background and ambient; a `projection` or `orthoHeight` stated for the call stands over the camera's.
## Which entity a capture framed
A capture names an entity, or a camera to render from, by its id or by its name.
A name exactly one entity carries means that entity. A name several entities
carry means none of them on its own, so the capture is refused, and the refusal
says how many entities carry it and names each by name and id; pass the id of
the one you mean. A player is drawn as its body, so a capture of a player
identity (an entity carrying `UserIdentity`, named `user`) frames the body its
`avatar` names. A player prototype is absent from the live scene while it
plays, so its name reaches no entity there: a capture of that name is refused,
and the refusal names each player spawned from the prototype, by its identity
and its body, with their ids.
Every cell of a capture aimed at an entity reports what it framed under
`subjects`: one `{ id, name }` per subject, carrying
`playerIdentity = { id, name }` when the picture is the body of the identity
the capture named.
## Isolate
Framing a subject fits the **frame** to it; it does not decide what is drawn in that
frame. Everything else standing there is still rendered — and a scene's default player
spawn sits at the world **origin**, which is exactly where a prop usually gets built, so
a tight shot of a new prop at the origin can come back showing the player instead.
`isolate = true` draws the subject without the other geometry. It changes exactly that — lighting, sky and post-process are untouched, so a final frame stays a final frame, and a flat read of the geometry is a `pass`. Excluded geometry still casts shadows onto the subject and still bounces light into it, the same as any render-layer exclusion. In a `setups` collage each cell shows the entity its own set-up frames alone, and a set-up can state `isolate` for its own cell; a set-up aimed from a position or a camera frames no entity and is refused when it is to be isolated.
## The tone the frame carries
Every capture reports `luma` on its cell record: the tone of the pixels it just
encoded, in code values on the 0-255 scale the image is delivered at.
```
luma = { pixels, min, max, mean, p1, p5, p50, p95, p99, span, spread, crushed, clipped }
```
`span` is `max - min` — the whole range, a single stray pixel included. `spread`
is `p95 - p5` — the range the body of the picture occupies, which is the reading
that answers whether a shot is legible. `crushed` and `clipped` are the shares of
the picture sitting at code 0 and at code 255.
A frame's mean cannot answer that question: half a picture at 40 and half at 200
carries the same mean as a flat field at 120, and only one of them has anything
in it. A subject can be modelled, shaded, lit and drawn and still arrive inside a
handful of code values, in which case the image looks like a wash and every other
field here reports success. `spread` is the number that says so.
The same reading is available anywhere pixels are: `cpu:tone()` on a
`TextureCpuHandle` — from `renderer.texture.readback(rtHandle)` on a capture
handle, from a render-to-texture camera's target, from a CPU canvas — and
`cpu:histogram()` under it, the per-code-value counts for luminance and for each
of the three channels, which every reading above is a sum over.
`cpu:histogram(x, y, w, h)` counts one rectangle of the picture alone, so a
reading per region of the frame — how much of each cell is dark, lit or a
given colour — is one call per region.
`cpu:flip(reference)` holds the picture against another of its size by NVIDIA
FLIP, the comparison a golden is judged by; `@builtin::modules.postMetrics`
carries it with the golden runner, the pixel metrics (banding, temporal
variance, halo overshoot, ring energy, hue drift) and the scopes, each
reading a presented frame or a capture result as it comes.
## The grade an offscreen frame carries
A capture spawns its own camera and renders offscreen, and the post-process
chain runs over that frame the way it runs over the viewport's — for a station,
for an orbit around an entity, for a named camera, for a render-to-texture
camera alike. Inside an effect, `engine.view_proj` / `engine.prev_view_proj` /
`engine.inv_view_proj` are the camera THAT render was drawn from and
`engine.resolution` is that target's own size, so a grade that reconstructs
world space from scene depth reconstructs against the station and lens the
capture asked for. That makes an offscreen capture an oracle for an authored
look: it photographs a chosen station and lens without taking the on-screen
camera away from whoever else is driving the scene, and moving that camera
somewhere else does not move the frame this one returns.
An offscreen render keeps no view history of its own, so `engine.prev_view_proj`
there holds that same matrix rather than the frame before it, and a pass taking
camera motion from the two reads none. On the viewport the pair is a frame
apart, so a camera's motion (and the blur and temporal reconstruction built on
it) is read from the presented frame: `source = "presented"`.
A render feature's passes reach an offscreen render the same way, against the
same camera — the `renderFeature` assetType README carries that half, including
`@frame.camera`, its own per-target camera input. `render(ctx)` itself runs once
a frame for the main viewport, so `ctx.viewport` is the window's size rather
than the captured target's; a feature sizes its scratch with `screen = true` and
lets the engine track the drawn target.
`postProcessing = false` takes the chain off the frame a capture returns, and
with it the film a render feature draws — the flare and grain, a short focus's
bokeh, the shutter's smear. It is a `Camera` property rather than a render
layer, so `renderLayers` does not reach it. A capture naming a diagnostic `pass`
drops the chain by default, so the buffer comes back as the value it encodes.
## Collage
`collage` renders a grid of frames into one image. What varies across its cells is what you pass — `setups` for a cell per whole camera set-up, `viewpoints` for a cell per view of one subject (or `"sides"` for all six), `passes` for a cell per render pass, `duration` for a cell per sample over time. A collage with no axis is refused rather than quietly sampling time.
`setups` is the shot list: each entry is written the way a single capture is aimed (`source`, `camera`, `entity`/`entities`, `position`, `lookAt`, `rotation`, `distance`, `angle`, `margin`, `viewpoint`, `basis`, `projection`, `orthoHeight`, `fov`, `near`, `far`), plus `label` to name the shot on the result, and a field an entry leaves out comes from the collage's own options. `setups` and `viewpoints` are one axis — both say where the camera stands — so a request names one of them; either crosses with `passes` into a 2D grid, a row per station and a column per pass.
The shot most of this exists for is one call: six sides of one object, each an orthographic elevation, nothing else in frame.
Authoring the cameras themselves — creating, listing, aiming, framing, and setting their parameters — is the `camera` toolbox.
# services.attach
Reads a generation's gateway job by the `jobId` a run recorded, and returns its state and, once succeeded, its result — the provider's response, the URL or bytes the run would have downloaded. 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. A job whose provider refused the request (a 429 while it rate limits) reads `failed`, with the provider's HTTP status in `upstreamStatus`, its reason in `error`, and its answer in `result`. Costs nothing; the charge happened at submit.
# services.balance
Reads your current credit balance — the pool every generation spends from. Check it against an operation's cost before starting a generation. Returns the credit count when signed in, or a reason when not.
# services.describe
Describe your assets: finds the descriptions (READMEs) they are missing and
writes them, many at a call. A description is the text an asset's type asks it
to carry: for the built-in types that ask, its README, or the `intent`,
`function` and `usage` texts of a type's `about.yaml`. Each call costs 1 credit
and covers many assets. What it writes is an ordinary file in the asset, there
for you to edit.
```luau
tools.use("services", "describe", { mode = "find" }) -- what is missing, how many calls, what they cost
tools.use("services", "describe", { mode = "all" }) -- fill what that listing names
tools.use("services", "describe", { assets = { "Spinner", "Door" } })
```
## Modes
- `find` (the default) lists the assets that lack the description their type
asks for, the file and keys each needs, the number of calls filling them makes
and the total price. It writes nothing.
- `all` fills every asset `find` lists.
- `assets` fills the assets you name.
By default the assets are the ones that block the current push. `scope = "world"`
takes every asset your world holds that a type asks to describe.
## What it does
The assets are packed whole into as few calls as fit, in the order of their
names, and the calls run several at once. Each asset comes back `written`,
`failed` (with the reason) or `skipped` (with the reason). An answer that is
missing or unusable for one asset fails that asset and no other. The text is
written into the description file or keys the type declares. Text you wrote is
never replaced: it fills only what is missing or still starts with `[[describe]]`.
Library and installed assets, `/tmp` and `/local` are never touched, and a
sub-asset is left to its enclosing asset unless that asset's type makes its
sub-assets discoverable.
Add `instructions` to say more to the model, and `examples` to name assets whose
descriptions show the shape you want. The model also reads the type's own
guidance and example descriptions, and each asset's files, whole.
## Price
A call costs 1 credit, whatever it carries, and a call covers many assets.
`find` reports the number of calls the fill would make, which is also the total
in credits, before anything is spent. The credits for the whole fill are checked
before the first call, and a fill you cannot cover starts nothing. An asset whose
files alone are more than one call carries is answered `failed`, since the files
are never cut down.
# services.generate
Starts a generation on a service: the step that turns a prompt into a real asset (a 3D mesh, a texture, a sound) by spending credits. You name a service and pass that operation's inputs, including the prompt. It returns immediately with a handle because a generation takes minutes and runs in the background; when the run ends it posts a notice, `generation finished` with the handle's id and the asset to spawn, or `generation failed` with the id and the error, which reaches you on your next tool call. `services.status` reads the run's progress in the meantime.
# services.jobs
Lists the generation jobs this session has started, active and finished, newest first: the live view of what is running and where the finished ones landed. The answer is bounded. It carries at most `limit` rows (20 by default; `limit = 0` answers the counts alone) and says how many jobs matched (`total`), whether rows were left out (`truncated`) and how to read them (`more`). `status` keeps the jobs in a status (`"active"` for the ones still running, `"finished"` for completed and failed, or an exact status) and `service` keeps one service's jobs (a name that is no service and ran no job is refused, naming the services); `byStatus` and `byService` count the matching jobs.
Each row carries the job's id, service, operation, status, progress, resulting asset path and any error, with `promptPreview` (the prompt's first 60 characters) in place of the full prompt. `prompts = true` carries the full prompt on every row, and the status tool reads one job with its full prompt. For the durable record that survives restarts, use the outputs listing instead.
# services.list
Lists the generation services you can use — the things you can generate (3D meshes, textures, audio) by spending credits — and is the entry point to the whole flow. Each service is self-describing: its operations, and for each operation the inputs it takes, the kind of asset it produces, and its credit cost. Call this first, then start a generation with what you learn here.
# services.outputs
Lists the assets generated by services in this world — the durable record that survives restarts, read from the provenance stamped on each produced asset (the in-flight job list does not persist). Each row carries the spawnable asset path alongside the service, operation, prompt, and when it was generated. Use it to find what you've generated across sessions.
# services.shared
The implementations behind the `services.*` tools. They wrap the `service` asset
type (`asset.resolve(name, "service")`) so the tools stay thin and an agent never
has to resolve or invoke a service directly.
- **discover** — `list` enumerates available services with fully-described
operations; `balance` reads credits.
- **start** — `generate` resolves the service and invokes it, returning a handle.
- **track** — `status` / `jobs` read the in-flight run records a generation
mirrors into `/zero/source/tmp/services/<id>.json`; `outputs` reads the durable
provenance stamped on each produced asset (the run records aren't synced and
vanish on restart — the asset stamp is what survives).
# services.status
Reports one generation job's current status by the handle id returned when it was started. The generation runs in the background regardless, and posts a `generation finished` or `generation failed` notice when it ends; this reads the same outcome on demand, and each row carries progress and, once completed, the spawnable asset path. A call the provider refused for the moment (a 429 while it rate limits) is sent again after a wait; while it waits, `status` names the refusal and the attempt coming next (`generate: retrying after upstream 429: ... (attempt 2 of 6)`), and `jobId` names the gateway job of the latest attempt. A run whose attempts all end refused reads `failed`, with the provider's status and reason in `error`. Returns nil for an unknown id.
# toolbox (asset type)
A toolbox is an asset grouping set of `<tool>.tool` assets around one area of work. Each tool in
it becomes callable under the toolbox `<toolbox>.<tool>`, and is a shell command,
`zero <toolbox> <tool> <args>`; `guides core/tools`, "A tool's command line", covers its
flags, paths, `render.luau` and exit status.
## When to group tools together
When creating tools for the engine, a common theme should be identified and
and a toolbox created to house them, this allows each individual operation
to be easily accessible instead of being scattered in a million places.
## Sharing functionality between tools
A toolbox features ability to define functions that code inside each individual tool
inside it can access, this is usefull when establishing helpers etc so you dont have
to duplicate code for each new tool you write.
A `shared.module` inside the toolbox root is where this code lives,
each tool inside a toolbox can access its functions by requiring it with `require(".shared")`.
```luau
-- <toolbox>.toolbox/shared.module/init.luau
local M = {}
-- One step a tool reports in its result.
export type Step = { name: string, ok: boolean, detail: string? }
--!desc Check that a numeric argument lies within bounds.
--!arg arg The argument's name, for the error.
--!arg value The value passed.
--!arg min The lowest value allowed.
--!arg max The highest value allowed.
--!return The value; raises naming the argument and the range when it lies outside.
--!example "count", 12, 1, 100
typed function M.range(arg: string, value: number, min: number, max: number): number
if value < min or value > max then
error(string.format("%s must be between %s and %s, got %s", arg, tostring(min), tostring(max), tostring(value)))
end
return value
end
--!desc A byte count as a readable size.
--!arg bytes The count.
--!return The size in B, KB, MB or GB.
--!example 2681107
typed function M.size(bytes: number): string
local units = { "B", "KB", "MB", "GB" }
local i = 1
while bytes >= 1024 and i < #units do
bytes /= 1024
i += 1
end
return if i == 1 then string.format("%d %s", bytes, units[i]) else string.format("%.1f %s", bytes, units[i])
end
--!desc A step for a tool's result.
--!arg name What the step did.
--!arg ok Whether it succeeded.
--!arg detail What happened, when there is more to say.
--!return The step.
--!example "copy textures", true
typed function M.step(name: string, ok: boolean, detail: string?): Step
return { name = name, ok = ok, detail = detail }
end
--!desc The asset of `kind` that a caller named, looked up by that name.
--!arg kind The asset type to look in.
--!arg name The name the caller passed.
--!return The asset; raises naming the kind and name when nothing matches.
--!example "material", "gold"
typed function M.named(kind: string, name: string): AssetRef
local ref = asset.tryResolve(name, kind)
if ref == nil then
error(string.format("no %s named '%s'", kind, name))
end
return ref
end
return M
```
This shared code should be limited to being used by the tools inside it,
dont put general functionality you might need otuside the toolbox into its shared module.
Instead reference those in the shared module should you wish to utilize something.
A benefit the shared module shares with the tools own code is that it allows
the author to write code that has computed references, meaning eg resolving
an asset from a computed variable `asset.resolve(<type>,name)` that is normally
not allowed in other parts of the engine surface, due to said references not being
recorded.
This is usefully for utilizing tools to handle operations that are called by users/agents
and are dynamic in nature, such as finding/inspecting/testing assets, or content based on an input etc.
When writing a shared module, you should follow standard module practices, please see `module.assetType` readme.md
for more indepth information.
## Describing the toolbox
When writing the readme for your toolbox, ensure you focus on the overall
area what the toolbox focuses on, rather than listing individual tools inside it,
discovery surfcaces already group tools and toolboxes together so a toolbox can be discoverd
by the tools it hosts and doesnt require separately maintaining a list of the tools in it's readme.
## What are tools and when do i create ones?
Tools are typed operations with a known serialized output format, made to enable efficient operation in the engine and provide an easily callable interface for operations around a topic.
They are also useful in that they can be called from a multitude of surfaces, from `execute`, to `bash` to `tool_exec`
to users Tools portion in the edui.
A toolbox and its tools differ from eg a module that has multiple callable functionm in that they might return anything,
and come with no guarantee that the output is meant to be inspected directly, where as tools are meant to perform an self contained operation and return results/info/relevant data in a readable format, including eg. image data.
Whenever you identify an operation that requires specific orchestration and or is something that could be a repeated operation and makes it easier to manage the project/task or when you want to expose logic written in `luau`
to the other call surfaces like `bash` under its `zero` command,you should reach for creating a toolbox.
Toolboxes also come in handy for limiting control/inputs, exposing a limited set of functionality in a clean manner or ensuring the caller doesnt have to understand the entire code.
For comparison, you could think of toolboxes as programs, and the tools inside them as commands.
A package might contain tens of tousands of lines of code, dozens of modules, components etc,
a toolbox turns that surface into a system that any agent/user can then utilize without having
to read or understand the code inside.
Its the UI equilevant to operations.
For the indepth explanation of what `tool.assetType` is and does, read it's readme.
# backfillSearchMeta
Re-derive the search `.metadata` for existing assets whose type records facet
data there — a bundle's component set and humanoid classification, an
animation's humanoid classification.
New and edited assets get this from their type's `onChange` hook. Assets with
no recorded `.metadata.components` / `.metadata.humanoid` have search facets
that read a false negative (an unrecorded asset looks the same
as one that genuinely lacks the trait). Run this to populate them.
```lua
tools.use("assets", "backfillSearchMeta", {}) -- bundle + animation
tools.use("assets", "backfillSearchMeta", { types = { "bundle" } })
```
Idempotent: re-running recomputes the same values. Only assets whose type
exposes a `reindexSearchMeta` ref method are touched; everything else is counted
as scanned and left alone. Returns `{ total, byType = { <type> = { scanned,
reindexed } } }`.
# assets.create
Create an asset of a type from that type's own template, and answer where it went: its identity, guid, folder and primary file, and whether what it wrote is kept. The type decides which files the asset holds, and `assets.typeDoc <type>` reads what its creation takes. An asset that already exists is refused unless `overwrite` re-authors it in place, keeping its guid.
# assets.fetchInUse
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. It scans each entity's components, walking their public data recursively for asset references, dedupes by identity, and reports each reference enriched with its identity and on-disk path plus the entities that use it. Target one entity, a list of entities, or the whole scene; roots are scanned through their children. The one-call inventory of what a piece of content is made of.
# assets.fromEntity
Capture everything an entity is carrying at **runtime** as durable assets, and
repoint the entity at them.
A procedural mesh (`renderer.mesh.create`) has no asset behind it, so anything
keyed to that geometry — a GI bake, a saved scene — is session-only: reload and
the mesh regenerates with a fresh identity, and the bake is gone.
`assets.fromEntity` freezes every live runtime resource an entity's components
hold — across the whole hierarchy, every resource field — into persistent assets
in one call, and rewrites each field to point at the durable asset. Persistent
references (already assets) are left untouched.
Resource fields are discovered generically from each component's registered
schema, so any component carrying runtime content participates — it reifies
**all** of an entity's runtime content, not one hard-coded type. A runtime
resource shared across several fields is reified once and every field is
repointed at the one asset.
Reify in **edit mode** to make content durable — in play mode the new assets land
in the ephemeral runtime store.
```
assets.fromEntity() -- freeze every runtime resource in the scene
assets.fromEntity("cornellRoom") -- freeze the cornellRoom hierarchy
assets.fromEntity(nil, { dryRun = true }) -- report what would be reified, write nothing
```
Returns `{ reified, skipped, failed }`. The single-resource primitive is
`assets.new`.
# assets.generatePreviews
Queue a render of the missing or stale `preview.png` for every world asset whose type declares a `preview:` recipe in its `type.yaml` — the batch pass that brings an existing world up to the previews-carry-the-description contract. Each asset is pictured by its type's recipe (a material on a sphere, a shader on the shader ball, a texture as its own pixels, anything scene-instantiable stood in the preview studio) and the picture is persisted inside the asset folder, where it syncs, publishes, drives image-based search, and gives browsers a thumbnail. The call itself only scans and enqueues, so it returns in seconds regardless of world size; the renders drain serially on the preview work queue in the background — poll `assets.previewStatus` until `pending` reaches 0. Re-running the tool is safe at any point: assets whose preview is current, one that records the content the asset holds now, are skipped unless `force` re-renders them, so an interrupted backfill resumes where it left off. `root` narrows the pass to a path prefix. Empty leftover `README.md` files inside visited assets are removed — an empty file can never sync, so each one otherwise re-enters the outgoing queue on every boot.
# assets.installedTypes
The asset types installed in this world, the engine's own included.
Types are content: a world has the ones the engine ships and the ones it
installed, and every other type is published content like any asset, found by
searching for what you are making and installed with it. This lists the first
group only, which is why its name says so.
Each row carries the type's name, the lead line of its own README, and whether
it declares `instantiate`, the contract `entityOps.fromAsset` dispatches
through, and therefore whether the type can become an entity in a scene.
# assets.new
Persist a **live runtime GPU resource** — a mesh or texture handle from
`renderer.mesh.create` / `renderer.texture.create`, which has no asset behind
it — into a durable, guid-anchored asset.
A runtime resource lives only on the GPU: nothing references it by a stable
identity, so anything keyed to it (a GI bake, a saved scene, a material slot) is
session-only and regenerates with a fresh identity on the next reload.
`assets.new` reads the resource back from the GPU, encodes it, and writes a real
asset via `asset.create`, so it survives a reload.
`asset.create` itself is disk-only — it writes supplied bytes or pixels and never
reads the GPU back. So for raw in-hand geometry or pixels, call
`asset.create(<type>, name, data)` directly; `assets.new` is the tool for
freezing a resource that only exists as a live handle.
Reify in **edit mode** to make content durable — in play mode the asset lands in
the ephemeral runtime store.
```
local h = renderer.mesh.create({ positions = p, indices = i })
assets.new(h, "tree") -- persist the runtime mesh
assets.new(meshGuid, "terrain", { category = "mesh" }) -- a bare guid string needs opts.category
```
Reifiable categories: `mesh`, `texture`. See `assets.fromEntity` to reify every
runtime resource an entity carries in one call.
# assets.preview
Render one asset through its type's own preview — a mesh, material, texture or scene seen on its own, without placing it in the world — and hand the picture back as an image. An asset whose type renders no preview answers `available = false` with the reason.
# assets.previewStatus
Read the preview work queue — the serial drain that renders every queued `preview.png` (from `generatePreviews` backfills, from staging an asset whose preview is missing or stale, and from views that ask for a picture). Returns how many renders are still pending, which asset is rendering right now, the session's completed/failed totals, and the most recent failure records. Poll it after `generatePreviews` until `pending` reaches 0 and `active` is empty; a non-empty `failures` list names each asset that could not render and why.
While `pending` is above 0 and nothing renders, `held` names what the queue is waiting for: the engine in play mode (the queue renders in edit) or a render device being rebuilt after a loss. A render that cannot finish does not wait: when the render device is abandoned, a subject draws nothing, or the drain rendering it ends part way, the entry moves to `failures` with the reason, and the `zm push` refusal for an asset that needs its preview.png says where the queue stands on it. `activeFrames` counts the engine frames the batch under way has run, which tells a slow batch from one that is not moving, and `drainer` is the handle of the task draining the queue (`task.status(drainer)` reads it). Reading the status starts a drain when work is queued and none is running.
# assets.reconcileReferenceNames
Write the current name of every moved reference into the world sources that still spell an old one. A source records the guid of each asset it references, so when an asset sits at another path the reference keeps resolving to it and nothing breaks; the source's stored text still spells the old name, and a notice names the source each time the world becomes ready. This call writes the name the asset has now. It changes no behaviour: every reference resolves to the same asset before and after. Each write is an edit of that source, synced to the world like any other. Only the author's sources are written: the world's own files and installed assets the author has modified, never `/source/deps/` or an installed asset still as it was published. `dryRun` lists what would change and writes nothing; `paths` narrows the call to those sources.
# reify
Shared helper for the assets toolbox: turn a **live runtime GPU resource** into a
persistent asset.
A resource made with `renderer.mesh.create` / `renderer.texture.create` lives
only on the GPU — no asset, no stable identity. This module reads it back from
the GPU, encodes it into the engine-native payload, and writes a real asset via
`asset.create` (the GPU→CPU→encode→create path `asset.create` does not do on its
own, since it is disk-only).
The reify recipes are a table keyed by asset category, so a new resource category
is one entry rather than a code fork. Current recipes: `mesh` (ZMSH via
`renderer.mesh.readback`) and `texture` (ZTEX via `renderer.texture.readback`).
Consumed by `assets.new` (one resource) and `assets.fromEntity` (every runtime
resource an entity carries). Not an agent-facing tool.
# search
Searches for assets by what they are or do, across everything published, this world's own and installed content included, and says for each hit whether this world has it installed.
# assets.typeBehavior
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.
Knowing a type exists is not the same as knowing what it gives you. A type that
already evaluates, instantiates, validates and recompiles itself is scaffolding
to build inside. This is how you see that before deciding to write your own.
Pairs with `assets.typeDoc` for the prose; `assets.installedTypes` lists the
installed types.
# assets.typeDoc
One asset type's own documentation — what it IS, the shape of its folder, how it
is authored, and what it does for you.
A type's README says things no signature can: which file is the source, what
regenerates when you write it, what the type handles so you do not have to. Read
it before building anything that resembles what the type describes; that is the
moment a project turns into a few lines.
It reads an installed type: `assets.installedTypes` lists those, and a type not
installed yet is found by searching. `assets.typeBehavior` gives the
operations the type answers to.
# assets.users
Find every entity in the active layer that uses a given asset. Name the asset as a word — the project is combed case-insensitively and the first match is used — or pass an asset reference you already hold. Every entity's component data is scanned recursively, and each use site is reported with the entity, component, and field it lives in.
# guide
A guide is a single-file documentation asset: a `<name>.guide/` folder whose body is `guide.md`, surfaced through the `guides` MCP tool, the `man` shell builtin and the Docs panel. Drop one anywhere under `/zero/source/`, a `.package` included, and it shows beside the built-in guides.
## Linking guides together
A markdown link in a guide is followed when it is clicked in the Docs panel: `[Bindings](controls-guide)` opens another guide by any name `guides read` takes, `[Slots](#save-slots)` a heading of the same guide (its text in lower case, punctuation removed, spaces turned into `-`), `[Slots](save-kit#save-slots)` a heading of another, and `[the module](../saveKit.module)` a file or asset, relative to the guide's own folder. A link the panel cannot follow says why above the guide.
## In the Inspector
Guide: the guide's word count, its headings, and **Open in Docs**, which opens it in the Docs panel.
# assetType runtime
The machinery that reads a `<typename>.assetType/behavior.luau` and turns its
declarations into live behaviour.
A `behavior.luau` is a table of declarations — `ref` methods, `modules` shared
code, an `events` schema, a `namePattern`, `refShapes`, and the `onCreate` /
`onRegister` / `onChange` / `onDelete` / `validate` / `settingsDefaults` /
`settingsCorrections` hooks. None of it runs itself. This is the code that
runs it, and it lives here because reading a type's declarations is the
**assetType type's own behaviour** — the same way `material.assetType` owns
what a `.material` does.
## Sub-modules
| Module | Reads | Responsibility |
|---|---|---|
| `ref.module` | `ref`, `modules` | Builds every `AssetRef` envelope, attaches the shared metatable, and dispatches per-type methods and shared modules onto it. Also owns edit-mode write-behind persistence. |
| `create.module` | `onCreate`, `namePattern`, `validate` | Backs `asset.create`: resolves the type, checks the name against its pattern, runs `onCreate` (or clones `template/`), and writes the instance. `validate.module` beside it checks a creation `opts` table against the type's `createSchema`. Before running `onCreate`, resolves the type's settings chain (below) and hands the result to the hook as `opts.settings`. |
| `settings.module` | `settings`, `settingsDefaults`, `settingsCorrections` | A type's declared settings schema and where its values live (`type.yaml`'s `settings:` block). `Settings.resolveForCreate` layers the schema's own defaults, the type's own `settingsDefaults(name, opts, asked)` hook (a DEFAULT the schema can't express — a name-derived convention, a fact read off a payload the caller handed it, a value following a field `asked` shows a higher layer naming; any later layer overrides it), the Preset Manager, an importer's settings, the caller's `settings` table, then the type's own `settingsCorrections(values, name, opts)` hook (a CROSS-FIELD CONSTRAINT the schema can't express, applied last over the fully merged values). A correction over a default/preset/importer value lands silently; a correction that would override what the caller's own `settings` table explicitly asked for refuses the creation instead, naming the field and why. Neither hook is ever handed a table to mutate; each returns what it wants changed (`settingsCorrections` may also return a `{ [field]: reason }` table for that refusal message). `Settings.cloneValues` deep-copies a values table — `create.module` hands `onCreate` a copy, never the table it stamps. `Settings.valuesOf` / `writeValues` read and write an asset's own values file. |
| `typeYaml.module` | `type.yaml` | The folder a type lives in and its parsed `type.yaml`, decoded once per content version of the file. `settings.module` and `inspector.module` read their blocks through it. `about` / `aboutAt` read a type's `about.yaml` (`intent`, `function`, `usage`), leaving out any key that is empty or still starts with `[[describe]]`; `aboutRows` and `aboutMarkdown` lay the texts out for the Inspector and the `types/<name>` guide. |
| `inspector.module` | `type.yaml`'s `inspector:` block | Which inspector a type declares for its assets (`inspector: { type: ... }` or `{ instance: ... }`), and the declaration that file returns — the sections it adds, the generic ones it replaces, and their order — or the problem that kept it from loading. |
| `onRegister.module` | `onRegister` | Fires an instance's once-only registration hook — on arrival, on world load, and for `@builtin` library instances. |
| `changeDispatch.module` | `onChange`, `onDelete`, `refShapes` | Routes a VFS source write or a folder delete to the enclosing typed asset's type hooks. |
| `refShapes.module` | `refShapes`, `ref` | Publishes what every `AssetRef<category>` answers to, so a member read on an asset-typed value is checked against that category's real surface. |
| `events.module` | `events` | The per-asset event runtime behind `ref.events`. |
| `instantiable.module` | `ref.instantiate` | The instantiation contract an asset type opts into: `root` stands the root entity, `place` applies the base placement opts to one the type adopted, and `result` shapes the `(root, idMap)` every `instantiate` returns. Also registers the `sceneInstantiable` field-constraint validator. |
| `declaration.module` | `type.yaml` | The type's decoded declaration, read once and held until a write inside an `.assetType` folder; what every question about a type as a whole reads. |
| `previewRecipe.module` | `type.yaml` `preview:` | How an instance's `preview.png` is produced: which of the four recipes (`instantiate`, `image`, `surface`, `custom`) the type declares, and what that recipe needs. |
## Reaching it
```luau
local rt = require("@builtin::assetTypes.assetType.shared")
rt.ref.loadTypeBehavior("material")
```
Fields resolve lazily. The prelude requires the sub-modules directly, in
dependency order (`ref`, then `changeDispatch`, `onRegister.install()`,
`refShapes.install()`) — a table that required all six on load would take that
ordering away from it.
# yaml.module
YAML decode + encode for authored engine content. Pure Luau, no engine
dependencies — a general parsing library, not data-system plumbing.
```luau
local Yaml = require("@builtin::modules.yaml")
local doc = Yaml.decode(vfs.read(path))
vfs.write(path, Yaml.encode(doc))
```
## Supported subset
- Block mappings (`key: value`) and block sequences (`- item`), nested
to any depth.
- Compact sequence-of-mapping entries (`- path: x` / ` kind: y`).
- Flow collections — `{ a: 1, b: 2 }` and `[1, 2, 3]` — single line.
- Plain scalars with type inference: `~` / `null` → nil, `true` /
`false` → boolean, numeric literals → number, everything else →
string.
- Single- and double-quoted strings (`''` escapes a literal quote
inside a single-quoted string; `\n` `\t` `\r` `\\` `\"` inside a
double-quoted one). A quote opens a quoted scalar where a scalar
begins — after `key:`, after a `- ` entry marker, or inside a flow
collection — so a plain scalar carries an apostrophe as data
(`note: the world's rules`). Quoted keys are read in flow mappings.
- `#` comments — honored outside quotes, so a `#` inside a quoted
string is kept as data.
- Literal (`|`) and folded (`>`) block scalars, with a chomping
indicator (`-` strips the final newline, `+` keeps every trailing
blank line, none keeps one newline) and an indentation indicator
(`1`-`9`: the content's indent past the parent's), in either order
(`|-`, `>+`, `|2`, `>1-`). A block scalar's content is literal text:
quotes (balanced or not), `#`, `key: value` and `---` inside it are
data.
- An optional leading `---` document marker.
## Loud-error contract
Constructs outside the subset above raise instead of misparsing:
anchors/aliases (`&`, `*`), tags (`!`), directives (`%`),
multi-document streams (a second `---`), a block-scalar header
it cannot read (`|x`, `|-+`, `|0`), tab indentation, and inconsistent indentation (both block structure and
block-scalar bodies). Every error carries the offending line number
(`yaml: line N: ...`), so a bad config points straight at the line to
fix.
Tab indentation is rejected everywhere — including block-scalar body
lines whose leading whitespace contains a tab. A tab after the first
non-space character of a line is content and passes through.
## API
- `Yaml.decode(text: string): any` — decode a YAML document into a
Luau value (table, scalar, or nil for an empty document). Raises on
malformed input or an unsupported construct.
- `Yaml.decodeWithComments(text: string): (any, boolean)` — decode as
`decode` does, and report whether the text carries a comment by the
decoder's own rule: a `#` at a line's start or after whitespace, outside
a quoted scalar and outside a block scalar. Re-encoding the value drops
such comments, so an editor that writes a document back reads this first.
- `Yaml.encode(value: table): string` — encode a Luau table as YAML
(block style, two-space indent, alphabetically sorted keys). Raises
on values YAML can't represent (functions, userdata, non-string
mapping keys).
A top-level empty table encodes to an empty document, which decodes
back to `nil` (the YAML empty-document ambiguity).
# interfaces
The screen-building skill: author a UI as a styled widget tree the way you
write HTML and CSS, register it, and then see it and drive it through the `gui`
toolbox rather than reasoning about the tree.
Covers the layout and paint model, entrance and idle animation, callbacks that
fire in play mode, and the build → capture → play → iterate loop that gets a
screen from blank to built.
# debugging
Finding out **when** something went wrong in a running world.
The job this skill covers is not "what is the current value" — the current
value is downstream of the moment that matters. It is: attach an instrument to
running code, keep working, and be told the instant the thing happens.
It exists because the reflex it replaces does not work. Running something,
waiting, and taking a capture aims at a moment you cannot see: seconds pass
between the call and the frame, and adding a wait only moves the guess. Polling
is the same guess with more steps.
Covers: probing a function call, a source line, or a value; using a condition
to name the moment rather than every step toward it; reading the state a value
was taken in before trusting the value; and cancelling what you armed.
The trap it spends the most words on is the expensive one — a right value read
in the wrong world, where the match already ended or the scene already swapped,
nothing announced it, and every number afterwards means something else.