# 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.
# reference
Make something visual look like a picture of it.
A look described in words cannot be checked: "make it stunning" leaves the
agent doing the work to judge its own frames against an adjective, and it stops
at the first thing that works. This workflow gives the work a picture to be
held to, and holds the loop that closes the gap.
## What it does
1. **Reference.** One request makes the pictures. It generates them with the
`image_gen` service (or takes the images the run was handed), keeps them
under `/zero/source/design/references/<name>/` with a README saying what
each shows and, for work that changes over time, which moment, and reads
each back, regenerating any that does not read as what was asked for.
2. **Match.** Round after round, one request pictures the work the way each
reference shows it, writes down every difference before changing anything,
closes them in the work, and pictures it again. The pictures go under
`/zero/source/design/shots/<name>/`. The differences a round leaves open
travel into the next. The run ends when every reference is matched, or when
a round closes nothing.
## Starting it
zero workflow start --name reference --args '{"subject":"...","look":"..."}'
`subject` names what is being made and where it stands; `look` is what it was
asked to look like, in the asker's words; `references` hands in images to use
instead of generating them; `name` names the folders. The run answers with the
references it worked to, the rounds it took, whether it ended matched or
stalled, and what was still open.
# workflow
Run a workflow, and answer what it asks.
A workflow is a program that holds the shape of a job: its phases, its branches,
what runs at the same time as what, and what each step is given. It executes in
the engine and it decides all of that itself. You do not drive it — you answer
the questions it stops to ask, and it carries on.
That inversion is the point. A job with eight steps written as instructions
becomes eight things an agent has to remember at once, and by the last one the
first is gone. Written as a workflow, the shape lives in the program and no
agent has to hold it.
## The loop
workflow.start begin a run — returns immediately, the run is already going
workflow.next take the next request it is waiting on, claimed as you read it
workflow.renew keep a claim while its work goes on
workflow.answer hand back a result; the run resumes from where it parked
workflow.correct replace an answer the run has not read yet
workflow.fail say why a request cannot be answered
workflow.status what every run is doing, waiting on, and returned
A request that carries a `schema` takes only an answer of that shape:
`workflow.answer` refuses any other, naming the field and the shape it needs,
and the request stays yours to answer again. Give the schema to the subagent
with the prompt.
An answer is final once the run has read it, which it does within a moment
of it being given. `workflow.correct` replaces an answer the run has not read
yet, held to the same schema; once the run has read it, the call refuses and
says the answer was already consumed.
`workflow.answer`, `workflow.correct`, `workflow.fail` and `workflow.renew`
each refuse a request they cannot take by naming it and its state: an id no
run holds (with a pointer at `workflow.status`), an answer already given (and
whether the run has read it), a run that has ended, a request that failed or
was cancelled. The shell exits non-zero on a refusal.
A request is announced on your tool responses once, when the run asks for it,
and once more if a claim on it runs out. It then waits for whoever asks for it:
`workflow.status` lists every request still waiting, and `workflow.next` hands
out the next one, so when you finish one request, take the next. A request you
have claimed is yours to work on in peace: your own calls keep it, and you are
told once when it is about to run out, and once if it does. A run that ends
says so once: a failed run names why, and every request it was still waiting
on is ended with it, so nothing is left to take or answer.
## Every request is a subagent's job
The prompt `workflow.next` hands over is written to be one agent's entire
task. It comes as a file, `prompt_path`, so a claim stays small however long
the job is: give a fresh subagent that file as its task, and answer with what
that subagent returns.
Doing the work yourself instead defeats the workflow: the whole run's context —
every phase, every result, every prompt — ends up in one conversation, which is
the exact failure a workflow exists to prevent. Your context should hold the
prompts you dispatched and the results you got back, and nothing else.
When several requests are waiting at once, the workflow opened them together
because they do not depend on each other. Dispatch them together.
## Tell the subagent how to find things
A request says what to do, not how to work here. Put that in the task you dispatch:
- **Open the skill for the job first.** `agent_skill` with no argument lists what this world knows
how to do. Skills advertise themselves on every tool response — one that covers the job is a job
somebody already worked out, and an agent that builds around it produces something the engine
never loads.
- **Then the tools.** `search_tools` finds one, `describe_tool` gives its exact signature,
and `tool_exec` runs it in a script (`return await tools.use("<toolbox>", "<tool>", ...)`).
`bash` is the engine's own shell, carrying the `zero` CLI over the same
toolboxes and `zm` for committing. `execute` runs Luau for what no tool covers.
- **What it builds is content the world loads**, not a script somebody runs by hand. A file that
only does something when executed manually is a note about the world rather than part of it.
Every one of those was learned by leaving it out: subagents given a plausible-sounding but wrong
discovery flow wrote loose scripts, never opened the skill describing their exact task, and never
had the shell.
## The subagents share one engine
Every subagent works in the same engine, the same world, the same Luau VM and
the same viewport as the others and as you. Nothing about running in parallel
gives any of them a private copy.
So each request owns its own files and writes nobody else's. None of them stops
or restarts the engine, clears the scene, or deletes what it did not create —
the run is not over when one step is, and the next step inherits whatever the
last one left. Anything a step starts in the background it also stops before it
returns, because a leftover driver loop makes the next step's measurements
wrong.
## A run outlives whoever started it, and the engine
Runs live in the engine, not in a session. Close the tab, lose the connection,
hand over to somebody else: the run is still parked exactly where it was, and
whoever asks `workflow.next` gets the request that was waiting. Each run keeps
its record under `/zero/source/local/workflow_runs/`, which this machine keeps
for the world and the user, so when the engine itself restarts on the world the
run comes back in the phase it had reached, with every answer it was given and
every claim under the same request id, and carries on. `workflow.status`
counts how many times an engine read it back as `restored`.
A claim is a lease. `workflow.next` states how long it holds (`lease_seconds`,
and `expires_in` for what is left), counted from the last tool call the session
that claimed it makes on the engine: a session working through one long turn
keeps its claims by working, whatever tools the work calls. `workflow.renew`
starts the lease over before a stretch with no engine calls, and every
`workflow.next` you make renews the claims you already hold. A claim whose
session makes no call for the length of the lease returns to the queue, so a
runner that disappears mid-request costs that request and not the run; its
holder takes it back with `workflow.renew` while nobody else has taken it.
# 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.
# 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.
# workflow
A job whose shape is a program.
Some jobs are one thing an agent does. Others have a shape: a decision, then
several pieces built at once because they do not touch each other, then somebody
looking at the result and saying whether it holds. Written as instructions, that
shape becomes a list an agent has to carry in its head, and by the last step the
first is gone. Written as a workflow, the shape is code — it runs in the engine,
it decides its own phases and branches and fan-out, and nothing that answers it
is told what comes next.
## The shape on disk
<name>.workflow/
workflow.js the program — the primary file, and where all the decisions live
workflow.yaml what it is, when to reach for it, the phases it moves through
README.md
.metadata
## The program
`workflow.js` is JavaScript, run in the engine. Six things are given to it:
phase(name) name the stage the run is in
log(text) say something as it goes
agent(prompt, opts) ask for one agent's whole job; parks until answered
parallel(thunks) several asks that do not depend on each other
pipeline(items, ...) each item through every stage, independently
args whatever the run was started with
`agent()` is the only call that leaves the engine. It parks the run, puts the
request where whoever holds credentials can see it, and resumes with the answer.
Everything else — the loop that built those prompts, the branch on what came
back, the decision to fan out — is the program's, and stays in the engine.
`opts.schema` states the shape the answer must have:
'string' 'number' 'boolean' 'any' a value of that type
'string[]' or [ shape ] a list, every item of that shape
{ field: shape, 'note?': shape } a table with those fields; a name
ending in ? may be left out
`workflow.next` hands the schema over with the prompt. `workflow.answer`
refuses a value of any other shape, naming the field and the shape expected
there, and the request stays open to be answered again. `workflow.correct`
replaces an answer the program has not read yet, held to the same schema; the
program reads an answer within a moment of it arriving, and a correction after
that is refused as already consumed. Every list the schema
names reaches the program as an array, an empty one included. A schema written
outside this notation fails the run at the `agent()` call that carries it.
## Running one
local run = asset.resolve("<name>", "workflow"):start({ ... })
or through the `workflow` toolbox, which is what an agent uses:
`workflow.start`, then `workflow.next` / `workflow.answer` until it is finished.
Each request a run asks is announced on tool responses once, when it can be
taken, and once more if a claim on it runs out; `workflow.status` lists every
request still waiting.
From the engine shell, the same run is driven with `zero workflow`:
zero workflow start --name [name]
zero workflow status
zero workflow next --runner <who you are> (claim the request it is waiting on)
zero workflow renew --request <id> (keep the claim while its work goes on)
zero workflow answer --request <id> --value <result>
zero workflow fail --request <id> --reason "<why>"
`next` names who is taking the request, and `answer`, `renew` and `fail` name
the request they act on by the id `next` returned.
## What a run is
A run lives in the engine, not in a session. Whoever started it can close the
tab, lose the connection or hand over to somebody else; the run stays parked
exactly where it was, and the next agent to ask gets the request that was
waiting. Each request is one subagent's whole job — the point is that no single
context has to hold the entire run.
## In the Inspector
Workflow: what the workflow is for and when to reach for it, the args it takes, its phases, its step budget when it declares one, and the size of workflow.js, with **Open** for workflow.yaml and workflow.js. Nothing here runs it.
# answer
Answers a request a workflow run is waiting on. The run resumes from where it parked and decides for itself what comes next; answering does not steer it. Pass the request id from `workflow.next` and the value the request asked for, as a table. When the request carries a `schema`, the value must have that shape: a value of any other shape is refused with the field and the shape expected, and the request stays open to be answered again. Returns `true` when the run took the answer, and raises naming the request and its state when it cannot (an unknown id, an answer the run already read, a run that has ended, a failed or cancelled request).
```lua
workflow.answer("call-f6641ff8", { title = "Tornado Run", ready = true })
```
# correct
Replace the answer to a workflow request before the run reads it.
`workflow.answer` settles a request with its first answer. When that answer
turns out wrong (an unmeasured figure, a garbled line), `workflow.correct`
puts the right one on record in its place, as long as the run has not read
the answer yet. The value is held to the request's schema exactly as the first
answer was.
A run reads an answer within a moment of it being given, and from then on it
carries on from that answer: a correction after that is refused with a message
naming the request and saying the answer was already consumed. The run's log
records every correction it took.
# fail
Tells a workflow that a request cannot be answered, and why. What that means is the workflow's to decide: a stage may stop the run, or carry on without what it asked for. Use it instead of leaving a claim unanswered, because an abandoned claim keeps the run parked until the claim expires. Pass the request id from `workflow.next` and a one-sentence `reason`, which reaches the workflow. Returns `true` when the run took it, and raises naming the request and its state when it cannot (an unknown id, a request already answered, a run that has ended, a request that already failed or was cancelled).
```lua
workflow.fail("call-f6641ff8", "the world it names does not exist")
```
# list
Lists the workflows in this world, read from each workflow's own manifest: `name`, `description`, `when` it is worth reaching for, the `phases` it moves through, and the `args` it expects. Look here before doing a multi-step job by hand; a job with a workflow is one whose shape is already worked out. Start one by name with `workflow.start`.
```lua
workflow.list()
```
# next
Takes the next request a workflow run is waiting on, and claims it for the caller in the same call, so two agents cannot answer the same question. Pass `runner`, a name recorded on the claim, and optionally `run` to take only from one run (an unknown run, or one that has ended, raises naming its state). Returns nil when nothing is waiting.
The result carries `request`, `run`, `phase`, `step`, `schema` (the shape the answer must have), `prompt_path` and `prompt_bytes`, `lease_seconds`, `expires_in` and `renew_with`. The prompt is kept in a file rather than returned: it is one agent's whole job, so hand the file at `prompt_path` to a subagent and answer with what it returns. `prompt` itself is returned only when the run could not keep it in a file. The claim is held for `lease_seconds` from the claiming session's last tool call on the engine; renew it with `workflow.renew` before a stretch with no engine calls. A claim left to run out returns to the queue. Finish with `workflow.answer` or `workflow.fail`.
```lua
workflow.next("me")
```
# renew
Keeps a claim on a workflow request through a stretch with no engine calls:
the claim's lease starts over. The lease counts from the last tool call the
claiming session makes on the engine, so a session that is working keeps it
anyway. Whoever took the request with `workflow.next` calls it, and the notice
that a claim is about to run out names the call.
# start
Starts a run of a workflow by name. A workflow is an asset: a program that holds the shape of a job and decides every step of it. Starting returns immediately with `{ run, workflow, state }`; the run is already executing and parks at its first question, which arrives through `workflow.next`. Pass optional `args`, a table handed to the program as its `args`; what a workflow expects is listed in its manifest (`workflow.list`, or `assets.describe`). Raises when the world has no workflow of that name.
```lua
workflow.start("lighthouse")
workflow.start({ name = "lighthouse", args = { instance = "wfrt" } })
```
# status
Reports what workflow runs are doing, read from the run itself. Each run reports its `workflow`, `state`, `phase`, how many requests it has `asked`, `answered` and `cancelled`, the requests it is `waiting` on (with `claimed_by`, `lease_seconds` and `expires_in`), its `result` once done, and its `error` when it failed. `restored` counts the times an engine read the run back from its record. A run that has ended waits on nothing. Pass a run id for one run, or nothing for a list of all of them; an unknown id raises.
```lua
workflow.status()
workflow.status("run-098c453d")
```
# 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.
# 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.