---
title: "assetType (asset type)"
description: "The assetType is the type of types — the meta-type that every <typename>.assetType/ folder is an instance of, including itself. A <typename>.assetType/ folder declares a new asset type called…"
section: "Types"
slug: "types-assettype"
canonical: "https://origozero.ai/docs/types-assettype"
updated: "2026-08-28T01:17:56.918282822+00:00"
tags: ["asset-type", "reference"]
---

# assetType (asset type)

This type is self-hosting. `assetType.assetType/` conforms to its own
schema — a `type.yaml` plus a `README.md` — so the type system is
defined in terms of itself rather than a hidden engine special-case
(the same way a C compiler is written in C). Every asset in the engine,
including each `.assetType` definition, now resolves to a registered
assetType: a `.material` resolves to `material.assetType`, and
`material.assetType` resolves to `assetType.assetType`, which resolves
to itself.

## Why it exists

Assets used to be mapped to their type *implicitly*, by matching the
folder suffix against a name-keyed registry. That match carried no
stored link: nothing on a `Foo.material` recorded *which*
`material.assetType` it was written against, so two worlds shipping a
same-named type could silently disagree. Making `assetType` a real,
resolvable asset closes that gap — the link from an asset to its type is
now an explicit reference pinned in the asset's `.refs` sidecar
(`via: "asset_type"`), exactly like every other dependency.

## Where it lives

- Source: `/zero/source/.../<typename>.assetType/`
- Identity: `<typename>` (the `.assetType` suffix strips from the
  identity; the folder retains the suffix on disk).
- Folder shape:
  - `type.yaml` — the structural spec the validator reads. **Required.**
  - `README.md` — type-level documentation. **Required.**
  - `behavior.luau` — optional behavior/scaffolding module.
  - `template/` — optional canonical placeholder body the templater
    copies into a new asset of the type.
  - `<name>.module/` — optional shared code the type ships to **every
    instance** of it, conventionally `shared.module/` (see below).

## Shared code its instances reach (`asset.containing` + `.modules`)

A type ships code every instance of it uses by declaring the modules in
its own `behavior.luau`, as a map of tracked `require`s:

```luau
-- inside <typename>.assetType/behavior.luau
M.modules = { shared = require(".shared") }   -- the sibling shared.module/
```

An instance reaches it with one location-independent line:

```luau
-- inside any foo.<typename>/init.luau
local api = asset.containing(__FILE__).modules.shared
```

`__FILE__` is the chunk's own VFS path; `asset.containing` resolves the
calling asset, and `.modules.<name>` follows the instance's pinned
`typeRef` (the type's **guid**, recorded in the instance's `.refs` as
`via = "asset_type"`) to the module its type declares. Because
resolution follows that link rather than a category name, the same line
in an instance of a different type resolves to that type's module, and
identically-named modules in two types never collide. `asset.typeRef(target)`
reads that guid for any target; `asset.resolve(asset.typeRef(target))`
gives the full `AssetRef<assetType>`. See `assetTypes/README.md`
§ "Shared code: type-owned modules" for the full mechanism.

## How to create one

Authoring a new asset type is just creating a `<typename>.assetType/`
folder under `/zero/source/`. The engine registers `<typename>` as a
category **the moment the folder is written** — no engine restart, no
Rust change, and it works for built-in (`@builtin/assetTypes/`) and
user-defined types identically. Once `<typename>` is registered,
`<name>.<typename>/` folders are recognised as assets of that type and
resolve their `asset_type` link back to this definition.

Creating a `<name>.<typename>/` folder *before* its
`<typename>.assetType/` exists fails fast: the suffix has no registered
type, so the folder can't be a valid asset. Author the type first.

## The `behavior.luau` contract

`behavior.luau` is the type's code. It returns ONE table, and the keys below
are the ones the framework reads off it for EVERY type — the surface you get
by declaring them. Anything else on the table is your own: reachable by name
through `require("modules.asset_ref").loadTypeModule("<typename>")` for a
system that knows your type (`computeShader` publishes `dispatchKey` that
way), and never called by the framework.

```luau
local M = {}
M.ref     = { ... }   -- methods on every AssetRef of this type
M.modules = { ... }   -- shared code the instances reach
M.global  = {}        -- reserved key; ship the empty table
function M.onCreate(name, opts) ... end
return M
```

### The tables

| Key | Shape | What the engine does with it |
|-----|-------|------------------------------|
| `M.ref` | `{ [name] = function(self, ...) }` | Every function becomes a method on every `AssetRef` of this type: `ref:name(...)`, with `self` the ref. This is how a type gives its instances an API. Two names in here are contracted — see below. |
| `M.modules` | `{ [name] = require(".sibling") }` | Shared code the type's instances reach through `asset.containing(__FILE__).modules.<name>`, resolved by the instance's pinned `typeRef` guid. See the section above. |
| `M.refShapes` | `{ [method] = "TypeName" }` | Declares the result type of a `M.ref` method so the LSP can check what a call site does with it. See the `M.refShapes` section below. |
| `M.events` | an events schema | The events an ASSET of this type fires, declared the way a component declares its own. Readers subscribe through `ref.events.<name>:connect(...)`; firing authority stays with the type. Keyed by the asset's guid, so every resolver of the same asset shares the signals and a re-resolved ref re-attaches to subscriptions already there. |
| `M.namePattern` | a Lua pattern string | The name shape `asset.create` enforces for instances of this type, replacing the default `^[A-Za-z][A-Za-z0-9_]*$`. |
| `M.global` | `{}` | Reserved. Ship the empty table. |

### The lifecycle hooks

Each is optional; a type that omits one costs nothing. The engine calls them:

| Hook | Signature | When it fires |
|------|-----------|---------------|
| `M.onCreate` | `(name, opts) -> { [filename] = contents }` | On `asset.create("<type>", name, opts)`. Returns the files that scaffold the new instance, as a map of relative path to contents; the framework writes them. The `opts` type annotation is the schema `asset.create` validates the caller's arguments against, so annotate it. |
| `M.onRegister` | `(self)` | Exactly once per instance, the first time it registers — on `engine.onWorldLoaded` for instances already in the world, and immediately on a live `asset.create`. Guarded by the ref's shared `runtime` table, so a double trigger never double-registers. This is what makes "write an asset into the world and it takes effect live" work for content that registers into a runtime registry. The `@builtin` library is excluded from the sweep. |
| `M.onChange` | `(ref, change)` | On every VFS write to a file INSIDE one of this type's instances. `change` is `{ path, asset, type, kind, origin }` — `kind` is `"edited"` or `"seeded"`, `origin` is `"local"` or `"remote"` (an importer runs on the originator only). **Filter on `change.path`**: the hook fires for any file under the folder, so act on the one you care about. It runs SYNCHRONOUSLY and a re-entrancy guard suppresses writes back into the same asset inline — but the guard does not span asynchronous work, so the hook must be convergent on its own: diff the meaningful state and short-circuit while your own regeneration is in flight. |
| `M.onDelete` | `(ref)` | When an instance of this type is deleted. |
| `M.validate` | `(assetRef) -> { { code, message, severity? } }` | On `asset.validate`, after the structural `type.yaml` check, for the type's own SEMANTIC validation. `severity` defaults to `"error"`; error-severity problems flip `ok` to false, warnings do not. A hook that raises or returns a non-table is itself reported as a `validate.hook_failed` error. `world.push` runs this per user asset, so declaring it enforces your type's rules at publish time with no further wiring. |

### The two contracted `M.ref` names

Most `M.ref` methods are your type's own surface: name them what you like,
return what you like. Two are read by the engine and mean the same thing for
every type, so their shape is fixed:

- **`instantiate(self, target?, opts?) -> (root, idMap)`** — makes an instance
  part of the scene. Defining it is the whole opt-in: `ref:canInstantiate()`
  is true exactly when it exists. Its **return is a contract** — see the next
  section.
- **`inspect(self) -> detail`** — the type-specific half of `asset.inspect`.
  See its section below.

### Names a type cannot shadow

Some keys resolve on every `AssetRef` before per-type dispatch, so an `M.ref`
entry of the same name is never reached: `canInstantiate`, `getSource` /
`getBytes` / `getText`, `exists`, `deps`, `meta`, `runtime`, `events`,
`modules`, `typeRef`, and the residency flags `has_backing_asset`,
`has_runtime_changes`, `cpu_resident`, `gpu_resident`. These are the
behaviours every asset must expose identically, which is why they win.

### Reloading

A `behavior.luau` edit ripples to existing refs with no restart: dispatch
reads `mod.ref` fresh each time and `require`'s cache is re-run in place. A
type whose folder appears at runtime is picked up on the next dispatch —
negative results are not cached either.

## What search does with your type (`indexing:`)

A type declares what search embeds for its instances. **This is not optional
and there is no useful default**: ZeroMind runs your declaration and adds
nothing of its own, so anything you do not point at is absent from the index —
silently, and for every instance of the type forever. Twenty types once shipped
with no `indexing:` block and every instance of them was unfindable.

Two words carry the whole thing, and they are the same two the search tool
exposes:

- **`identity`** — what an instance IS. For anything you can look at, that is
  the picture (`content:` + `modality: image`, matched directly by a text
  query). For everything else it is the authored text that says what it is.
- **`capability`** — what it DOES and how it is made. Code, settings, the
  model-written summary of them.

A type with no behaviour has no `capability` entry. A type with nothing to look
at has no image. Leaving a slot empty is a statement; filling it with whatever
happens to be lying around is not.

The one you have to decide when authoring a type is: *for an instance of this,
what is the thing a person would recognise it by, and what is the thing it
does?* A texture answers "the image" and "its compression settings". A module
answers "its README" and "its code". Write those two answers into `indexing:`
and the rest follows.

Three specifics worth knowing before you write one:

- **`derive` is instructed by this type's own `README.md`.** A model reads an
  instance's source and writes a description; what it embeds is the
  description, not the source. It follows your README to know what it is
  looking at, so a type whose README says what its instances are gets good
  derivations for free. There is no prompt to name.
- **`derive` loses detail.** It keeps the main technique and drops secondary
  ones. If the source is code you want searchable by its own vocabulary,
  declare the same files a second time as `capability` with
  `extractor: verbatim`. Two entries, same role, different failure modes.
- **Never `source: { field: name }`.** A filename is one or two words with no
  usable embedding, and a corpus indexed that way answers "language runtime"
  with `say_runtime_2.soundClip`.

`facets:` is the second stage: a `file:` embedded whole (`.metadata`, so keys
nobody declared still get indexed) plus computed keys that state a value for
every instance including `false` — the raw file can imply "not rigged" and can
never say it.

The full schema, every extractor and the facet sources are in
[`assetTypes/README.md`](../README.md); the scaffolded block with inline
guidance is in [`template/type.yaml`](template/type.yaml).

## Typing a result from the instance (`M.refShapes`)

Every method on `M.ref` is checked against the type its `--!return` names,
and every asset of the type gets the same one. That is right for most
methods and wrong for the ones whose result is shaped by the ASSET: a
method answering one entry per child folder, per row of a config, per
binding an input map declares. The widest true annotation for those is
`{ [string]: Thing }`, and an indexer accepts every key — so a caller's
typo reads as valid and nothing reports.

`M.refShapes` lets the type state the result per instance. Each entry is
`function(self) -> (typeExpression, source?)`: a Luau type expression for
THIS asset, and the module whose type vocabulary the expression uses.
Read the asset; never run it — stating a type must not take effect.

```lua
-- inputMap.assetType/behavior.luau — the shipped example.
-- `activate()` hands back one handle per control the map declares.
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)` resolves and `map.noexisting` is
reported with the map's real controls. Without it, both pass silently.

### What the call site has to say

A per-instance type is matched by the asset's IDENTITY, so it applies only
where the reference the method is called on says WHICH asset. Two spellings
do:

```lua
-- a component field, whose declared default names the asset
public = { map = Field.assetRef("inputMap", "@builtin::inputMaps.default", Sync) }
local controls = public.map:activate()   -- typed for THAT map

-- an annotation, naming category and identity
local m: AssetRef<"inputMap", "@builtin::inputMaps.default">
```

`asset.resolve("<identity>", "<category>")` carries the CATEGORY, so the
methods the category defines are checked on the result — but not which
asset it found, so a per-instance result keeps the category's declared
return. A reference that names no asset (`asset.resolve(someVariable)`, a
value passed in as a parameter) carries neither, and calls on it are
unchecked. That is the same rule everywhere: the checker states what the
code states, and a name computed at runtime states nothing.

### The result is a value, not a binding

The type belongs to the expression, so it travels the way any inferred type
travels. Indexing the call's result is checked; stashing it in a module
local and indexing it from another function is not, because the local's
declared type is what carries across — and `any` (or no annotation) carries
nothing:

```lua
-- checked: `d.greting` is reported
function awake()
    local d = public.dialogue:lines()
    d.greeting:say()
end

-- NOT checked: `lines` is a module local typed `any`
local lines: any = nil
function awake() lines = public.dialogue:lines() end
function start() lines.greting:say() end
```

Bind what you need at the call site and the names stay checked in both
places — the asset-derived type at the boundary, ordinary scoping after it:

```lua
local lineGreeting = nil
function awake()
    local d = public.dialogue:lines()
    lineGreeting = d.greeting        -- `d.greting` is reported here
end
function start() lineGreeting:say() end
```

### When nothing is reported and you expected something

From a call site, a type that is correct and a type that was never published
look the same: no diagnostic either way.
`require("@builtin::modules.asset_ref_shapes").published()` answers which it
is. It returns two maps — the category surfaces, and the per-asset results
keyed by identity and then by method:

```lua
local shapes = require("@builtin::modules.asset_ref_shapes").published()

shapes.categories.dialogue
--> "{ lines: () -> any, lineNames: () -> { string } }"

shapes.returns.Shopkeeper.lines
--> "{ browsing: Line, farewell: Line, greeting: Line, wares: Line }"
```

An identity absent from `returns` was never published for, which is a
different thing from published-and-correct — and the reason to look here
rather than at the call site.

The answers are recomputed when an instance is written, so a control added
to a map reaches the checker with no restart. Only types that declare
`refShapes` are read per instance, and at most 64 assets of one type are —
past that the type keeps its declared return and the engine log names what
was dropped.

## The instantiate hook (`M.ref.instantiate`) — a contracted return

A type opts into becoming part of the scene by defining `instantiate` on its
`ref` table. That is the whole opt-in: `ref:canInstantiate()` is true exactly
when the hook exists, so consumers offer a scene path — an `Asset.source`
field, a viewport drop, a tool argument — by CAPABILITY rather than by a list
of type names.

Unlike `inspect`, this hook's **return is a contract**. A caller writes one
piece of code against every instantiable type, so what comes back cannot vary
by type:

```lua
-- behavior.luau
local Instantiable = require("modules.scene_instantiable")
local M = {}

M.ref = {
  instantiate = function(self, target, opts)
    local root = Instantiable.root(self, target, opts)   -- IN:  the base opts
    -- ... compose whatever this type is, under `root`, NOW ...
    return Instantiable.result(self, root, idMap)        -- OUT: the contract
  end,
}

return M
```

**IN.** `target` is an owning entity ref: the instance lands under (or, for a
hierarchy type, onto) that owner. With no target the type spawns a fresh root.
`position`, `rotation`, `scale`, `name` and `temporary` mean the same for every
type — `Instantiable.root` applies them to a root you mint, `Instantiable.place`
to one you adopt, so you never re-read the spec. Honour more opts of your own if
your type needs them, and document them in the hook's `--!arg` docs.

**OUT.** Return `(root, idMap)` through `Instantiable.result`:

- **`root`** — the composed root, as an `EntityRef`. Composition is
  **synchronous**: everything your type builds is live when you return, so the
  caller can parent to it and read its components in the same statement. Do not
  defer the work to a component's `awake` and return an empty shell — a root
  that is not live is refused.
- **`idMap`** — the `originalId -> runtimeId` map naming what you spawned, or
  nil for a type with no addressable children (`result` normalises it to `{}`).
  **A caller never receives nil.** A component that re-composes your asset on
  every load keeps this map and passes it back in, which is how a cross-entity
  reference into the composition survives a reload.

`AssetRef` runs every `instantiate` through `result` on the way out whether or
not your type called it, so returning something else fails at your own call
rather than handing a caller a nil root. Calling it yourself is still how you
say what you return, and running it twice changes nothing.

`modules/scene_instantiable`'s README is the full reference for both halves.

## The inspect hook (`M.ref.inspect`)

`asset.inspect(ref)` resolves `ref`, builds a common envelope shared by
every asset (`identity`, `name`, `guid`, `source`, `typeName`,
`typeDefinitionPath`, `scope`, `origin`, `description`, `tags`), then
calls the resolved type's own inspect hook — `M.ref.inspect(self)` on
`behavior.luau` — for the type-specific `detail`. The hook returns
*only* `detail`; the framework fills in everything else:

```lua
-- behavior.luau
local M = {}

M.ref = {
  inspect = function(self)
    -- self.path is the asset's own folder. Parse the asset's OWN
    -- files — source text, a native payload header, its `.metadata`
    -- — never runtime state (a live component instance, a GPU
    -- upload, a compiled schema): inspect must work on an asset that
    -- hasn't been compiled, uploaded, or registered yet.
    return {
      -- type-specific fields, whatever this type wants to surface
    }
  end,
}

return M
```

A type that omits `M.ref.inspect` yields the generic record: the
envelope alone, `detail = nil`. That's a valid, working state — not an
error — but it means agents calling `asset.inspect` /
`tools.use("assets","describe", ...)` on an instance of the type see
only identity and scope, none of what makes an instance of it
meaningful. Declaring the hook is what makes a type's instances
inspectable for what they actually are.

A hook that raises leaves `detail = nil` and sets `record.warning` to
the error text — `asset.inspect` itself never raises for a resolved
ref.

## Discovery

- `asset.list("assetType")` — every registered asset type.
- `asset.inspect("<typename>")` — the type's identity, source path, and
  this README.
- `asset.resolve("<typename>", "assetType")` — the `<typename>.assetType`
  asset itself (used by the validator to find the type's `type.yaml`).

## Related

- `assetTypes/README.md` — the full `type.yaml` schema and validator
  rules.
- `modules/scene_instantiable` — the instantiation contract's shared
  implementation, both halves.
- Every other `*.assetType/` here — the concrete types built on this
  meta-type.
