Log inGet started

asset

Updated 5 September 2026

The asset namespace — 141 functions.

asset/create

asset.create(typeName: string, name: string, opts: table?) -> table

[Library] Instance a new asset of an existing registered type: runs that type's onCreate(name, opts) behavior hook (or clones its template/ skeleton when it has none) and writes the produced files to /zero/source/<name>.<type>/ — the one authored location, in every mode. While play runs, the play write lock takes that write onto the play shadow (live in the session, disk source untouched, listed by vfs.playShadowPaths(), promoted or discarded on a guarded play-exit), so the asset keeps the identity, path and require spelling it has in edit. A create made from a component or a scene entrypoint — output the world reproduces on every load — writes to the copy-on-write /zero/runtime/assets/<identity>.<type>/ store instead, so the saved manifest never carries a second copy; name and folder spell the same identity in either store, so a reference written against that identity resolves the asset wherever the call filed it. Four framework opts keys steer placement and are consumed before the hook runs: folder (a relative subfolder under /source, and the dotted prefix of the asset's identity), into (a resolved container ref to author inside), dest (an absolute destination path) and overwrite (re-author in place, keeping the guid). Returns the created asset's AssetRef; raises on bad arguments or a failing onCreate.

Parameters

  • typeName string — Registered asset type to instance (e.g. "material", "texture").
  • name string — Destination asset name (becomes /source/<name>.<typeName>). A bare identity — pass opts.folder to place it in a subfolder rather than spelling a path here. The accepted shape is the type's to declare: ^[A-Za-z][A-Za-z0-9_]*$ unless its behavior.luau exports a namePattern, as guide does to take getting-started and 01-overview. This call names the category FIRST and the asset second. Every other asset.* call taking both names them the other way round — asset.exists(name, category), asset.tryResolve(ref, category) — so a create-then-check pair reads if not asset.exists(n, t) then asset.create(t, n, opts) end. A call whose two arguments are read into each other, at either end of that pair, is refused and told which way round the call reads.
  • opts { [string]: any } (optional) — Optional table forwarded to the type's onCreate hook, minus four framework keys consumed here and never seen by the hook: folder (a relative subfolder under /source to author the asset in, so generated content groups instead of accumulating at the source root, and the asset's identity carries that folder as its dotted prefix), into (author INSIDE a resolved container ref), dest (an absolute destination path), and overwrite (re-author in place, keeping the guid).

Returns AssetRef — The created asset's AssetRef — the SAME interned instance asset.resolve returns (guid/__ref/path + the type's ref methods: :getBytes, :ensureHandle, :serialize, …). Disk-only: nothing is uploaded to CPU/GPU.

local m = asset.create("material", "brick", { shader = "pbr" })
local mesh = asset.create("mesh", "tree", { positions = {...}, indices = {...} })
local mesh = asset.create("mesh", "rock", { positions = {...}, indices = {...}, folder = "terrain/props" })
local mesh = asset.create("mesh", name, { positions = p, indices = i, overwrite = true }) -- idempotent rebuild

globals/asset/add_tag

asset.add_tag(ref: RefArg, tag: string)

Add a tag to the asset's .metadata.tags. Idempotent. Creates the sidecar and the tags array if missing.

Parameters

  • ref RefArg — Any name the asset has.
  • tag string — Tag to add.
asset.add_tag("brick", "wip")

globals/asset/alias

asset.alias(ref: RefArg, alias: string) -> boolean

Add a name the asset answers to. asset.resolve, leaf shorthand, and the typed-argument coercion that content refs travel through all reach the asset by the alias from here on, exactly as they do by its identity — so a material naming a shader by its alias resolves, and content written against an older name keeps working after a rename. The name survives writes to the asset's own files. Raises when the name already resolves to a DIFFERENT asset — an alias extends the identity namespace and never takes a name out of another asset's hands — and when it is shaped like a guid or a VFS path, forms that resolve before identity lookup, so an alias in that shape could never answer.

Parameters

  • ref RefArg — The asset gaining the name.
  • alias string — The additional name. Any identity form: a bare leaf (standard) or a scope-qualified path (@builtin::shaders.legacy).

Returns boolean — True when newly added, false when the asset already answered to it.

asset.alias("@builtin::shaders.pbr", "standard")

globals/asset/aliases

asset.aliases(ref: RefArg) -> { string }

The additional names this asset answers to, beyond its own identity — what asset.alias registered, plus the package-relative ~pkg.tail form when the asset lives inside a package.

Parameters

  • ref RefArg — Any name the asset has.

Returns { string } — Array of alias names in canonical identity form.

for _, n in asset.aliases("pbr") do print(n) end

globals/asset/canCreate

asset.canCreate(typeName: string) -> boolean

Whether asset.create can instance typeName: the type declares creation logic (a behavior.luau onCreate hook) or ships a template/ skeleton the hookless fallback clones. A type with neither — one whose instances only arrive by import — answers false. The query a creation UI derives its offering from, so what it offers is what asset.create accepts.

Parameters

  • typeName string — Registered asset type (e.g. "material", "scene").

Returns boolean — true when asset.create(typeName, …) can produce one.

if asset.canCreate(kind) then asset.create(kind, name) end

globals/asset/categories

asset.categories() -> { string }

List every asset category the engine currently recognises. Use to discover valid type argument values for the rest of asset.*.

Returns { string } — Array of category names.

for _, c in asset.categories() do print(c) end

globals/asset/containing

asset.containing(path: string) -> AssetRef?

Walk path's ancestors and return an AssetRef handle for the OUTERMOST category-folder containing it (e.g. main.scene for "/source/scenes/main.scene/scene.json"). Returns nil for paths outside any registered asset type.

Parameters

  • path string — VFS path to inspect.

Returns AssetRef? — AssetRef handle, or nil.

local a = asset.containing("/source/scenes/main.scene/scene.json")

globals/asset/cpuResident

asset.cpuResident(ref: RefArg, typeName: string?) -> boolean

True when the asset is CPU-resident — a live script-component context holds it (a component's assetRef field, or an imperative asset.resolve/ref made while a component is the caller), which is what warms its bytes into memory. The CPU pool is a different pool from the device's: asset.observe().cpu lists it, asset.observe().textures / .meshes list what the device holds, and an asset can be in one and not the other.

Parameters

  • ref RefArg — Any name the asset has — handle, identity, guid, name or path.
  • typeName string (optional) — Category to restrict the match to. Omit to search every category.

Returns boolean — true when a live context holds it.

if asset.cpuResident(ref) then print("bytes are warm") end

globals/asset/create

asset.create(typeName: string, name: string, opts: { [string]: any }?) -> AssetRef

Instance a new asset of an existing type. Runs the type's behavior.luau onCreate(name, opts) hook to produce the asset's files, then writes them under /source/<name>.<type>/. This is the single generic asset-creation API. Refuses to clobber an existing edit-mode asset unless opts.overwrite = true, which re-authors it in place and keeps the existing guid (only the checksum changes). Pairs with asset.exists for content generators that re-run over the same names.

A create made from a script component's callback or a scene entrypoint is output the world reproduces on every load, so it is filed in the ephemeral /runtime/assets/ store instead, where the saved manifest never carries a second copy of it. name and folder spell the same IDENTITY in either store, so a reference written against that identity resolves the asset wherever the call filed it, and one generator run from an execute and from a component names one asset.

Parameters

  • typeName string — Registered asset type to instance (e.g. "material", "texture").
  • name string — Destination asset name (becomes /source/<name>.<typeName>). A bare identity — pass opts.folder to place it in a subfolder rather than spelling a path here. The accepted shape is the type's to declare: ^[A-Za-z][A-Za-z0-9_]*$ unless its behavior.luau exports a namePattern, as guide does to take getting-started and 01-overview. This call names the category FIRST and the asset second. Every other asset.* call taking both names them the other way round — asset.exists(name, category), asset.tryResolve(ref, category) — so a create-then-check pair reads if not asset.exists(n, t) then asset.create(t, n, opts) end. A call whose two arguments are read into each other, at either end of that pair, is refused and told which way round the call reads.
  • opts { [string]: any } (optional) — Optional table forwarded to the type's onCreate hook, minus four framework keys consumed here and never seen by the hook: folder (a relative subfolder under /source to author the asset in, so generated content groups instead of accumulating at the source root, and the asset's identity carries that folder as its dotted prefix), into (author INSIDE a resolved container ref), dest (an absolute destination path), and overwrite (re-author in place, keeping the guid).

Returns AssetRef — The created asset's AssetRef — the SAME interned instance asset.resolve returns (guid/__ref/path + the type's ref methods: :getBytes, :ensureHandle, :serialize, …). Disk-only: nothing is uploaded to CPU/GPU.

local m = asset.create("material", "brick", { shader = "pbr" })
local mesh = asset.create("mesh", "tree", { positions = {...}, indices = {...} })
local mesh = asset.create("mesh", "rock", { positions = {...}, indices = {...}, folder = "terrain/props" })
local mesh = asset.create("mesh", name, { positions = p, indices = i, overwrite = true }) -- idempotent rebuild

globals/asset/declareReferenceArg

asset.declareReferenceArg(call: string, position: number, assetType: string)

Declare that call's argument at 1-based position names an asset of type, so a string literal written there is recorded as a reference. The positional counterpart of asset.declareReferenceField, for a call that takes its asset as a plain argument — including a world's own spawn helper, which is where a name most often stops being visible to the reference graph. A lookup whose asset is its FIRST argument (asset.resolve and its siblings) is already read and needs no declaration. Only a literal — or a name the file holds in a top-level string constant — is recorded; anything computed is reported as a dynamic resolve.

Parameters

  • call string — The callee as it is written at a call site.
  • position number — Which argument holds the name, counting from 1.
  • assetType string
asset.declareReferenceArg("spawnModel", 3, "mesh")

globals/asset/declareReferenceField

asset.declareReferenceField(call: string, field: string, assetType: string)

Declare that call's options table names an asset of type in its field, so a string literal written there is recorded as a reference by whatever writes the file. This is what puts an API that takes an asset BY NAME into the reference graph: the named asset becomes a dependency, travels with the content that names it into a pack or a pull, and a name nothing answers to becomes an unresolved dependency worldValidation reports and the push gate refuses. A field holding a TABLE of names — a material's textures — records every name in it. Declare once, beside the API; a call taking its asset as the FIRST positional argument is already read and needs no declaration. Only a literal is recorded; a computed name resolves at runtime and is reported as a dynamic resolve.

Parameters

  • call string — The callee as it is written at a call site.
  • field string — The options-table field holding the name, read at the table's own level.
  • assetType string
asset.declareReferenceField("fx.beam", "material", "material")

globals/asset/declareReferenceKey

asset.declareReferenceKey(assetType: string, key: string, refType: string)

Declare that, in a data file belonging to an assetType asset, the top-level key names an asset of type — a .material's mat.yaml naming the shader it draws with and the textures it binds. The names a format holds are references as surely as ones written in code: recording them carries a material's shader along with the material into a pack or a pull, and turns a name nothing answers to into an unresolved dependency instead of a surface that renders as the magenta error material. A key holding a table of names records one per entry.

Parameters

  • assetType string — The category owning the file, e.g. "material".
  • key string — The top-level key holding the name(s).
  • refType string
asset.declareReferenceKey("material", "shader", "shader")

globals/asset/deps

asset.deps(ref: RefArg, type: string?) -> DepsResult

Return the asset's outbound dependency graph — every other asset recorded as a content dependency of it.

Parameters

  • ref RefArg — Any name the asset has.
  • type string (optional) — Category hint (optional).

Returns DepsResult{ deps = { { asset_guid, origin, literal, via, ... } } }.

for _, d in asset.deps("main.scene").deps do print(d.asset_guid) end

globals/asset/describe

asset.describe(typeName: string) -> DescribeResult

The creation contract for an asset type: the parameters its onCreate(name, opts) hook accepts, as data. kind is "schema" (typed contract), "legacy" (untyped opts — anything passes), "none" (template scaffold — takes no opts), or "error" (the type's schema failed to parse; error says why). contract is the human-readable rendering validation errors print.

Parameters

  • typeName string — Registered asset type to describe (e.g. "texture").

Returns DescribeResult — the creation contract.

local contract = asset.describe("texture").contract

globals/asset/diagnose

asset.diagnose(ref: RefArg) -> any

Why one asset can or cannot be used, read from the engine rather than from what the caller asked for. Always carries usable; when false, reason is one of asset.unusableReasons() and detail is the engine's own message. primary names the file the type's declared primary list resolved to, so an asset that loaded a preview image instead of its payload shows the wrong filename rather than a successful load. The payload's bytes are read by the engine's own decoder wherever it has one for that container, so usable is the verdict a load would reach and the call costs that decode.

Parameters

  • ref RefArg — Any name the asset has — handle, identity, guid, name or path.

Returns any — DiagnoseRecord

local d = asset.diagnose("myTex") if not d.usable then print(d.reason, d.detail) end

globals/asset/exists

asset.exists(name: string, typeName: string) -> boolean

Parameters

  • name string
  • typeName string

Returns boolean

globals/asset/get_field

asset.get_field(ref: RefArg, key: string) -> any

Read one top-level field from the asset's .metadata.

Parameters

  • ref RefArg — Any name the asset has.
  • key string — Field name.

Returns any — Field value or nil.

local author = asset.get_field("brick", "author")
local settings = asset.get_field("tree", "settings") -- → a Luau table

globals/asset/gpuResident

asset.gpuResident(ref: RefArg) -> boolean

True when the device holds a texture or mesh under this asset's guid, read off the inventory the renderer publishes.

Parameters

  • ref RefArg — Any name the asset has — handle, identity, guid, name or path.

Returns boolean — true when the device holds it.

print(asset.gpuResident("@builtin::models.Sample.DamagedHelmet"))

globals/asset/guid

asset.guid(ref: RefArg, type: string?) -> string

Return the guid for an asset.

Parameters

  • ref RefArg — Any name the asset has.
  • type string (optional) — Category hint (optional).

Returns string — Guid.

local g = asset.guid("@builtin::components.Camera")

globals/asset/has_field

asset.has_field(ref: RefArg, key: string) -> boolean

True when the asset's .metadata carries the named field.

Parameters

  • ref RefArg — Any name the asset has.
  • key string — Field name.

Returns boolean — True when present.

if asset.has_field("brick", "author") then end

globals/asset/has_tag

asset.has_tag(ref: RefArg, tag: string) -> boolean

True when the asset's .metadata.tags contains tag.

Parameters

  • ref RefArg — Any name the asset has.
  • tag string — Tag to check for.

Returns boolean — True when present.

if asset.has_tag("brick", "wip") then end

globals/asset/identity

asset.identity(ref: RefArg, type: string?) -> string

Return the canonical identity for an asset.

Parameters

  • ref RefArg — Any name the asset has.
  • type string (optional) — Category hint (optional).

Returns string — Canonical identity.

local id = asset.identity("brick")

globals/asset/import

asset.import(path: string) -> string?

Import a raw source file NOW and return the produced asset path (a .bundle for a model, .texture for an image, .audio for a sound, …), or nil if no importer claims it. This is the deterministic, on-demand counterpart to the engine's automatic import-on-write: it runs in the calling task and returns only when the import is complete. Pair it with a quiet write — vfs.write(path, bytes, { quiet = true }) lands the raw bytes without firing the automatic importer, then asset.import(path) imports them under your control, so you can act on the result instead of polling for the import to appear.

Parameters

  • path string — The raw source VFS path to import (e.g. a just-written .glb).

Returns string? — The produced asset path, or nil when nothing claimed it.

local bundle = asset.import("/zero/source/generated/chest.glb")

globals/asset/inspect

asset.inspect(ref: RefArg, type: string?) -> InspectRecord

Everything known about one asset in a single record: identity, guid, source, type, scope and origin, its description and tags, the ref methods its type exposes, and the type's own inspect detail when it declares one. The read-everything counterpart to asset.resolve, which hands back a ref.

Parameters

  • ref RefArg — An AssetRef, an identity string, or a path.
  • type string (optional) — Narrow the resolve to one asset type when assets of several categories answer to the same bare name.

Returns InspectRecord — The inspect record.

local rec = asset.inspect("@builtin::materials.default")
local rec = asset.inspect(name, "mesh"); print(rec.guid, #rec.tags)

globals/asset/list

asset.list(type_or_opts: (AssetCategory | ListOpts)?, scope: string?, opts: ListOpts?) -> ListResult

Query registered assets, returning each match as a resolved AssetRef handle. Every filter narrows the same enumeration and they compose: path selects a VFS subtree (the folder and everything under it), type keeps only those asset types within it, scope keeps only that scope, and fields keeps only assets whose .metadata matches. type and path each take one value or a list matching any of its entries, and all / any / none group whole filters — none excludes what it matches. order, limit, and offset shape the result: matches come back ordered by identity unless order names another field (name / path / type / guid). Each entry is the same envelope asset.resolve returns (__ref / type / name / guid / identity / path), so it can be passed anywhere an AssetRef is accepted, and the result carries :first() / :random() / :filter() / :sort() and friends. type takes the same values asset.categories() lists. The first positional argument is a path when it is absolute, a type otherwise. An unknown key raises, as does a table setting both type and its older spelling category. A static (literal) type or path makes the enumeration part of the calling file's content dependencies when it is saved — the set travels with published content, so consumers get at-least the authoring world's assets.

Parameters

  • type_or_opts (AssetCategory | ListOpts) (optional) — Type or VFS path filter (a static literal so the enumeration can be captured for publish), or the full query table.
  • scope string (optional) — Scope filter (when first arg is a type).
  • opts ListOpts (optional) — The query table — see ListOpts.

Returns ListResult — The matched AssetRef handles, as a result carrying query methods.

local hero = asset.list({ path = "/zero/source/props", type = "mesh", fields = { tags = "hero" } }):first()

globals/asset/list_field_values

asset.list_field_values(key: string) -> { any }

Distinct values seen for the named field across every asset's .metadata.

Parameters

  • key string — Field name.

Returns { any } — Array of distinct values.

local authors = asset.list_field_values("author")

globals/asset/list_fields

asset.list_fields() -> { string }

Distinct top-level field keys observed across every asset's .metadata. Useful for tooling discovering custom keys in use.

Returns { string } — Array of field names.

for _, k in asset.list_fields() do print(k) end

globals/asset/meta

asset.meta(ref: RefArg, type: string?) -> AssetMeta

Read the asset's engine-owned identity record (guid / checksum). Distinct from .metadata (agent-editable); for that use asset.metadata.

Parameters

  • ref RefArg — Any name the asset has.
  • type string (optional) — Category hint (optional).

Returns AssetMeta — Metadata table.

local m = asset.meta("brick") -- { guid = ..., checksum = ... }

globals/asset/metadata

asset.metadata(ref: RefArg, type: string?) -> AssetMeta

Read the asset's agent-editable .metadata sidecar as a Lua table. Missing sidecar returns {}. Distinct from asset.meta (engine-owned).

Parameters

  • ref RefArg — Any name the asset has.
  • type string (optional) — Category hint (optional).

Returns AssetMeta — JSON-shaped table; empty when no sidecar exists.

local md = asset.metadata("brick")

globals/asset/observe

asset.observe() -> any

What the engine is holding for content right now, in one reading: textures and meshes (one row per resource the device holds, each with the bytes it costs, its dimensions or buffer split, and where it came from), cpu (one row per asset a live script-component context holds), and totals — the aggregates those rows sum to, so the listing reconciles against renderer.textureMemory() and renderer.gpuMemory().

Each pool is named because they are different pools: an asset can be on the device and not CPU-resident, or the reverse. devicePublished is false when no renderer has published an inventory and cpuPublished when the scripting VM has not published its pool — an engine that cannot answer reads differently from one answering with nothing resident.

Returns any — ResidencyReading

local r = asset.observe() print(#r.textures, r.totals.textureBytes)

globals/asset/preview

asset.preview(ref: RefArg, opts: { [string]: any }?, type: string?) -> { [string]: any }

Render a preview of an asset. Resolves the ref and dispatches to its type's preview ref-method when present; otherwise returns the { available = false } sentinel ("no preview available for this type").

Parameters

  • ref RefArg — Any name the asset has.
  • opts { [string]: any } (optional) — Optional { size = { width, height }, angle = { yaw, pitch } }.
  • type string (optional) — Category hint (optional).

Returns { [string]: any }{ available, imageBase64?, width?, height?, bounds?, stats?, reason? }.

local p = asset.preview("@builtin::materials.gold", { size = { width = 512, height = 512 } })

globals/asset/primaryFile

asset.primaryFile(ref: RefArg) -> any

The file the asset type's declared primary list resolves to inside this asset, as the loader itself resolves it. resolved is false when no declaration matched and path is then absent; declared is the type's own primary list in match order.

Parameters

  • ref RefArg — Any name the asset has — handle, identity, guid, name or path.

Returns any{ path: string?, resolved: boolean, isFolder: boolean, declared: { string } }

print(asset.primaryFile("myTex").path)

globals/asset/ref

asset.ref(ref: RefArg, type: string?) -> AssetRef

Build a reference handle for an asset — the canonical ref envelope constructor. Identical shape to asset.resolve; preferred name for the author-side use case (embedding refs in YAML / JSON / Luau output).

Naming the asset here reads exactly as naming it in asset.resolve, down to raising on a miss: a literal is recorded as this source's dependency, a computed name is a dynamic resolve — free in a tool, refused on the gameplay path. asset.exists(name, type) is the probe a computed name can ask.

Parameters

  • ref RefArg — The asset to reference — an identity, a guid, a VFS path, or a handle.
  • type string (optional) — Category hint (optional).

Returns AssetRef — Ref handle. Raises on a miss or ambiguity, as asset.resolve does.

local r = asset.ref("animations.idle", "animation")

globals/asset/reloadPending

asset.reloadPending(ref: RefArg, typeName: string?) -> boolean

True while a write to this asset still owes it a reload — the write is inside the settle window that collects one authoring step's writes, or its reload is queued and the engine has not run it yet. False means every content change written so far has reached its subscribers, so a consumer bound to the asset now cannot be interrupted by a reload the earlier writes already earned. The recording is synchronous with the write, so a call made right after one already reads true.

Parameters

  • ref RefArg — Any name the asset has — handle, identity, guid, name or path.
  • typeName string (optional) — Category to restrict the match to. Omit to search every category.

Returns boolean — true while a content-change reload is still owed.

repeat task.wait() until not asset.reloadPending(ref)

globals/asset/reloadSeq

asset.reloadSeq(ref: RefArg, typeName: string?) -> number

How many content-change reloads this asset has been through — the count of onAssetReload dispatches the engine has RUN for it. A write to a file inside an asset does not reload it on the spot: the writes of one authoring step are collected for a settle window and the reload runs on a later frame. Read this, write, then poll for a larger number to learn the write's reload has actually reached subscribers. Monotonic per asset and session-scoped; 0 for an asset whose content has not changed since boot.

Parameters

  • ref RefArg — Any name the asset has — handle, identity, guid, name or path.
  • typeName string (optional) — Category to restrict the match to. Omit to search every category.

Returns number — content-change reloads dispatched for this asset.

local at = asset.reloadSeq(ref)
vfs.write(asset.source(ref) .. "/graph.json", encoded)
repeat task.wait() until asset.reloadSeq(ref) > at

globals/asset/remove_field

asset.remove_field(ref: RefArg, key: string)

Remove one top-level field from the asset's .metadata. No-op when the field isn't present.

Parameters

  • ref RefArg — Any name the asset has.
  • key string — Field name.
asset.remove_field("brick", "author")

globals/asset/remove_tag

asset.remove_tag(ref: RefArg, tag: string)

Remove a tag from the asset's .metadata.tags. No-op when the tag isn't present.

Parameters

  • ref RefArg — Any name the asset has.
  • tag string — Tag to remove.
asset.remove_tag("brick", "wip")

globals/asset/resolve

asset.resolve(ref: RefArg, type: (C & string)?) -> AssetRef<C>

Find an asset. The returned handle carries every name form the asset has (guid, identity, path, type) so downstream code can read any one of them without calling resolve again. Raises when ref resolves to no asset — or, with a type, to no asset of that type — and when ref reaches more than one asset, where it names the candidates for you to pick from instead of picking one of them. A <scope>::-qualified identity reaches exactly one: @root::name for the asset this world holds at its source root, the library identity (@builtin::…) for a library's. For the same lookup answering a miss with nil, use asset.tryResolve(ref, type).

A name written as a string LITERAL is recorded as this source's dependency on that asset, so the asset travels with the content and still resolves once someone installs it in another world. A COMPUTED name cannot be written down, so nothing pins what it reaches: that is a dynamic resolve — free in a tool, refused on the gameplay path (a component or scene entrypoint). asset.tryResolve, asset.ref and asset.source read the name they are given exactly this way too, so which of the four you reach for changes neither answer. To ask whether a computed name has files without reaching a handle, use asset.exists(name, type).

Parameters

  • ref RefArg — The asset to find — an identity, a guid, a VFS path, or a handle.
  • type (C & string) (optional) — Category to restrict the match to (optional). Separates a bare name that assets of different categories share (asset.resolve("cube", "mesh")); where several assets of the SAME category answer to it, the scope-qualified identity is what separates them. A reference naming a file an importer has since promoted (wall.png after the texture importer turned it into wall.texture) resolves to the promoted asset, and says so in the log once per reference.

Returns AssetRef<C> — Asset handle, carrying the category when one was named — so the methods that category defines are checked on the result. Raises (rather than returning nil) on a miss, and on a name that reaches more than one asset.

local a = asset.resolve("@builtin::components.Camera")

globals/asset/set_field

asset.set_field(ref: RefArg, key: string, value: any?)

Set one field in the asset's .metadata, creating the sidecar if missing. Sibling fields are preserved. When the new value AND the existing value are both maps (objects), the new value DEEP-MERGES into the existing one, so writing one sub-key never drops the others — set_field(ref, "settings", { keepCpu = true }) keeps every other setting. Arrays and scalars replace. Clear a whole field with asset.remove_field; replace the entire sidecar with asset.set_metadata.

Parameters

  • ref RefArg — Any name the asset has.
  • key string — Field name.
  • value any (optional) — Field value (any JSON-serialisable Lua value).
asset.set_field("brick", "author", "me")
asset.set_field("tree", "settings", { keepCpu = true }) -- merges; sibling settings kept

globals/asset/set_metadata

asset.set_metadata(ref: RefArg, data: AssetMeta)

Replace the asset's .metadata sidecar with the given table. Pass an empty table to clear all fields.

Parameters

  • ref RefArg — Any name the asset has.
  • data AssetMeta — Full JSON-shaped contents for the sidecar.
asset.set_metadata("brick", { author = "me", tags = { "wip" } })

globals/asset/source

asset.source(ref: RefArg, type: string?) -> string

Return the VFS source path for an asset.

It reaches the asset, so the name carries the same reference contract asset.resolve's does: a literal is recorded as this source's dependency, a computed name is a dynamic resolve — free in a tool, refused on the gameplay path. asset.exists(name, type) is the probe a computed name can ask.

Parameters

  • ref RefArg — Any name the asset has.
  • type string (optional) — Category hint (optional).

Returns string — VFS source path.

local p = asset.source("brick") -- "/source/brick.material"

globals/asset/tags

asset.tags(ref: RefArg) -> { string }

Convenience read of the .metadata.tags array.

Parameters

  • ref RefArg — Any name the asset has.

Returns { string } — Array of tag strings.

for _, t in asset.tags("brick") do print(t) end

globals/asset/tryResolve

asset.tryResolve(ref: RefArg, type: string?, base: string?) -> AssetRef?

The same lookup asset.resolve performs, answering a miss with nil instead of raising. Every name form, the same type narrowing, and the same handle on success — so "use it if it is there" needs no pcall around a call whose failure would otherwise be indistinguishable from a real error.

This consults the asset REGISTRY, so it sees registered assets wherever their files live, @builtin:: ones included. asset.exists(name, type) answers the narrower question of whether an asset's files are present in the current mode's store.

A name that reaches more than one asset still raises — that is a question about the reference, not about presence, and a nil there would report absence for content that is present twice.

It reaches the asset, so the name carries the same reference contract asset.resolve's does: a literal is recorded as this source's dependency, a computed name is a dynamic resolve — free in a tool, refused on the gameplay path. asset.exists(name, type) is the probe a computed name can ask.

Parameters

  • ref RefArg — The asset to look up — an identity, a guid, a VFS path, or a handle.
  • type string (optional) — Category to restrict the match to (optional). A reference naming a file an importer has since promoted resolves to the promoted asset, the same as asset.resolve.
  • base string (optional) — Referring VFS path a ~ / ~.tail ref expands against, the same as asset.resolve's — so the two answer the same question and differ only in what a miss is.

Returns AssetRef? — Asset handle, or nil when the reference resolves to no asset.

local mat = asset.tryResolve(name, "material")

globals/asset/typeRef

asset.typeRef(target: RefArg) -> string?

Return the pinned asset_type reference (the type's guid) that the asset is an instance of. Resolve the full type with asset.resolve(asset.typeRef(target)). Returns nil for loose files / assets with no pinned type.

Parameters

  • target RefArg — Asset handle / identity / guid / VFS path.

Returns string? — Pinned type's guid, or nil.

local t = asset.resolve(asset.typeRef("brick"))

globals/asset/unusableReasons

asset.unusableReasons() -> { string }

Every reason asset.diagnose can report an asset unusable for, sorted.

Returns { string } — Array of reason names.

for _, r in ipairs(asset.unusableReasons()) do print(r) end

globals/asset/validate

asset.validate(ref: RefArg, type: string?) -> ValidateResult

Validate an asset folder against its type's type.yaml, plus the type's own semantic validation. Structural problems come from type.yaml — missing required files, unsatisfied one_of_group alternatives, and (when allow_unlisted: false) unexpected children. validated = false when no type.yaml is registered — nothing structural to check. On top of that, when the asset's type ships a behavior.luau exporting a top-level validate(assetRef) -> { { code, message, severity? } }, its reported problems (severity defaults to "error") are appended to problems; error-severity problems flip ok to false, warnings leave it untouched. A hook that raises or returns a non-table is itself reported as a validate.hook_failed error problem — a broken hook blocks. A type with no validate export behaves exactly as the structural check alone. world.push calls this per user asset, so a type's semantic validation is enforced at publish time with no further wiring.

Parameters

  • ref RefArg — Any name the asset has.
  • type string (optional) — Category hint (optional).

Returns ValidateResult{ ok, typeName, validated, problems }.

local v = asset.validate("@builtin::components.Camera")

globals/asset/warmup

asset.warmup(ref: RefArg, opts: WarmupOpts?) -> WarmupResult

Warm an asset's bytes into CPU memory and follow its declared content dependencies to each referenced asset, deduped by guid. Type-agnostic (reads the generic ref graph) and CPU-only — never touches the GPU. Side-effect-free name resolution (uses asset.guid/asset.deps, not asset.resolve).

Parameters

  • ref RefArg — Any name the root asset has — handle, identity, guid, or path.
  • opts WarmupOpts (optional) — Optional { vias, max } — restrict ref-edge kinds / cap closure size.

Returns WarmupResult{ closure, count } — the deduped guid closure warmed and its size.

local w = asset.warmup("@builtin::scenes.test_arena")

modules/asset/README

require("@builtin/modules/api/engine/asset") -- asset (also available as global 'asset')

Asset resolver, ref envelope builder, sidecar metadata. Public Luau surface over the __asset Internal FFI namespace.

Usage: local asset = require("@builtin/modules/api/engine/asset") Also available as global: asset

modules/asset/add_tag

add_tag(ref: RefArg, tag: string)

Add a tag to the asset's .metadata.tags. Idempotent. Creates the sidecar and the tags array if missing.

Parameters

  • ref RefArg — Any name the asset has.
  • tag string — Tag to add.
asset.add_tag("brick", "wip")

modules/asset/alias

alias(ref: RefArg, alias: string): boolean

Add a name the asset answers to. asset.resolve, leaf shorthand, and the typed-argument coercion that content refs travel through all reach the asset by the alias from here on, exactly as they do by its identity — so a material naming a shader by its alias resolves, and content written against an older name keeps working after a rename. The name survives writes to the asset's own files. Raises when the name already resolves to a DIFFERENT asset — an alias extends the identity namespace and never takes a name out of another asset's hands — and when it is shaped like a guid or a VFS path, forms that resolve before identity lookup, so an alias in that shape could never answer.

Parameters

  • ref RefArg — The asset gaining the name.
  • alias string — The additional name. Any identity form: a bare leaf (standard) or a scope-qualified path (@builtin::shaders.legacy).
asset.alias("@builtin::shaders.pbr", "standard")

modules/asset/aliases

aliases(ref: RefArg): { string }

The additional names this asset answers to, beyond its own identity — what asset.alias registered, plus the package-relative ~pkg.tail form when the asset lives inside a package.

Parameters

  • ref RefArg — Any name the asset has.
for _, n in asset.aliases("pbr") do print(n) end

modules/asset/canCreate

canCreate(typeName: string): boolean

Whether asset.create can instance typeName: the type declares creation logic (a behavior.luau onCreate hook) or ships a template/ skeleton the hookless fallback clones. A type with neither — one whose instances only arrive by import — answers false. The query a creation UI derives its offering from, so what it offers is what asset.create accepts.

Parameters

  • typeName string — Registered asset type (e.g. "material", "scene").
if asset.canCreate(kind) then asset.create(kind, name) end

modules/asset/categories

categories(): { string }

List every asset category the engine currently recognises. Use to discover valid type argument values for the rest of asset.*.

for _, c in asset.categories() do print(c) end

modules/asset/containing

containing(path: string): AssetRef?

Walk path's ancestors and return an AssetRef handle for the OUTERMOST category-folder containing it (e.g. main.scene for "/source/scenes/main.scene/scene.json"). Returns nil for paths outside any registered asset type.

Parameters

  • path string — VFS path to inspect.
local a = asset.containing("/source/scenes/main.scene/scene.json")

modules/asset/cpuResident

cpuResident(ref: RefArg, typeName: string?): boolean

True when the asset is CPU-resident — a live script-component context holds it (a component's assetRef field, or an imperative asset.resolve/ref made while a component is the caller), which is what warms its bytes into memory. The CPU pool is a different pool from the device's: asset.observe().cpu lists it, asset.observe().textures / .meshes list what the device holds, and an asset can be in one and not the other.

Parameters

  • ref RefArg — Any name the asset has — handle, identity, guid, name or path.
  • typeName string? (optional) — Category to restrict the match to. Omit to search every category.
if asset.cpuResident(ref) then print("bytes are warm") end

modules/asset/create

create(typeName: string, name: string, opts: { [string]: any }?): AssetRef

Instance a new asset of an existing type. Runs the type's behavior.luau onCreate(name, opts) hook to produce the asset's files, then writes them under /source/<name>.<type>/. This is the single generic asset-creation API. Refuses to clobber an existing edit-mode asset unless opts.overwrite = true, which re-authors it in place and keeps the existing guid (only the checksum changes). Pairs with asset.exists for content generators that re-run over the same names.

A create made from a script component's callback or a scene entrypoint is output the world reproduces on every load, so it is filed in the ephemeral /runtime/assets/ store instead, where the saved manifest never carries a second copy of it. name and folder spell the same IDENTITY in either store, so a reference written against that identity resolves the asset wherever the call filed it, and one generator run from an execute and from a component names one asset.

Parameters

  • typeName string — The asset category to instance — one of asset.categories().
  • name string — The new asset's name.
  • opts { [string]: any }? (optional) — Optional table forwarded to the type's onCreate hook, minus four framework keys consumed here and never seen by the hook: folder (a relative subfolder under /source to author the asset in, so generated content groups instead of accumulating at the source root, and the asset's identity carries that folder as its dotted prefix), into (author INSIDE a resolved container ref), dest (an absolute destination path), and overwrite (re-author in place, keeping the guid).
local m = asset.create("material", "brick", { shader = "pbr" })
local mesh = asset.create("mesh", "tree", { positions = {...}, indices = {...} })
local mesh = asset.create("mesh", "rock", { positions = {...}, indices = {...}, folder = "terrain/props" })
local mesh = asset.create("mesh", name, { positions = p, indices = i, overwrite = true }) -- idempotent rebuild

modules/asset/declareReferenceArg

declareReferenceArg(call: string, position: number, assetType: string)

Declare that call's argument at 1-based position names an asset of type, so a string literal written there is recorded as a reference. The positional counterpart of asset.declareReferenceField, for a call that takes its asset as a plain argument — including a world's own spawn helper, which is where a name most often stops being visible to the reference graph. A lookup whose asset is its FIRST argument (asset.resolve and its siblings) is already read and needs no declaration. Only a literal — or a name the file holds in a top-level string constant — is recorded; anything computed is reported as a dynamic resolve.

Parameters

  • call string — The callee as it is written at a call site.
  • position number — Which argument holds the name, counting from 1.
  • assetType string
asset.declareReferenceArg("spawnModel", 3, "mesh")

modules/asset/declareReferenceField

declareReferenceField(call: string, field: string, assetType: string)

Declare that call's options table names an asset of type in its field, so a string literal written there is recorded as a reference by whatever writes the file. This is what puts an API that takes an asset BY NAME into the reference graph: the named asset becomes a dependency, travels with the content that names it into a pack or a pull, and a name nothing answers to becomes an unresolved dependency worldValidation reports and the push gate refuses. A field holding a TABLE of names — a material's textures — records every name in it. Declare once, beside the API; a call taking its asset as the FIRST positional argument is already read and needs no declaration. Only a literal is recorded; a computed name resolves at runtime and is reported as a dynamic resolve.

Parameters

  • call string — The callee as it is written at a call site.
  • field string — The options-table field holding the name, read at the table's own level.
  • assetType string
asset.declareReferenceField("fx.beam", "material", "material")

modules/asset/declareReferenceKey

declareReferenceKey(assetType: string, key: string, refType: string)

Declare that, in a data file belonging to an assetType asset, the top-level key names an asset of type — a .material's mat.yaml naming the shader it draws with and the textures it binds. The names a format holds are references as surely as ones written in code: recording them carries a material's shader along with the material into a pack or a pull, and turns a name nothing answers to into an unresolved dependency instead of a surface that renders as the magenta error material. A key holding a table of names records one per entry.

Parameters

  • assetType string — The category owning the file, e.g. "material".
  • key string — The top-level key holding the name(s).
  • refType string
asset.declareReferenceKey("material", "shader", "shader")

modules/asset/deps

deps(ref: RefArg, type: string?): DepsResult

Return the asset's outbound dependency graph — every other asset recorded as a content dependency of it.

Parameters

  • ref RefArg — Any name the asset has.
  • type string? (optional) — Category hint (optional).
for _, d in asset.deps("main.scene").deps do print(d.asset_guid) end

modules/asset/describe

describe(typeName: string): DescribeResult

The creation contract for an asset type: the parameters its onCreate(name, opts) hook accepts, as data. kind is "schema" (typed contract), "legacy" (untyped opts — anything passes), "none" (template scaffold — takes no opts), or "error" (the type's schema failed to parse; error says why). contract is the human-readable rendering validation errors print.

Parameters

  • typeName string — Registered asset type to describe (e.g. "texture").
local contract = asset.describe("texture").contract

modules/asset/diagnose

diagnose(ref: RefArg): any

Why one asset can or cannot be used, read from the engine rather than from what the caller asked for. Always carries usable; when false, reason is one of asset.unusableReasons() and detail is the engine's own message. primary names the file the type's declared primary list resolved to, so an asset that loaded a preview image instead of its payload shows the wrong filename rather than a successful load. The payload's bytes are read by the engine's own decoder wherever it has one for that container, so usable is the verdict a load would reach and the call costs that decode.

Parameters

  • ref RefArg — Any name the asset has — handle, identity, guid, name or path.
local d = asset.diagnose("myTex") if not d.usable then print(d.reason, d.detail) end

modules/asset/exists

exists(name: string, typeName: string): boolean

Parameters

  • name string
  • typeName string

modules/asset/get_field

get_field(ref: RefArg, key: string): any

Read one top-level field from the asset's .metadata.

Parameters

  • ref RefArg — Any name the asset has.
  • key string — Field name.
local author = asset.get_field("brick", "author")
local settings = asset.get_field("tree", "settings") -- → a Luau table

modules/asset/gpuResident

gpuResident(ref: RefArg): boolean

True when the device holds a texture or mesh under this asset's guid, read off the inventory the renderer publishes.

Parameters

  • ref RefArg — Any name the asset has — handle, identity, guid, name or path.
print(asset.gpuResident("@builtin::models.Sample.DamagedHelmet"))

modules/asset/guid

guid(ref: RefArg, type: string?): string

Return the guid for an asset.

Parameters

  • ref RefArg — Any name the asset has.
  • type string? (optional) — Category hint (optional).
local g = asset.guid("@builtin::components.Camera")

modules/asset/has_field

has_field(ref: RefArg, key: string): boolean

True when the asset's .metadata carries the named field.

Parameters

  • ref RefArg — Any name the asset has.
  • key string — Field name.
if asset.has_field("brick", "author") then end

modules/asset/has_tag

has_tag(ref: RefArg, tag: string): boolean

True when the asset's .metadata.tags contains tag.

Parameters

  • ref RefArg — Any name the asset has.
  • tag string — Tag to check for.
if asset.has_tag("brick", "wip") then end

modules/asset/identity

identity(ref: RefArg, type: string?): string

Return the canonical identity for an asset.

Parameters

  • ref RefArg — Any name the asset has.
  • type string? (optional) — Category hint (optional).
local id = asset.identity("brick")

modules/asset/import

import(path: string): string?

Import a raw source file NOW and return the produced asset path (a .bundle for a model, .texture for an image, .audio for a sound, …), or nil if no importer claims it. This is the deterministic, on-demand counterpart to the engine's automatic import-on-write: it runs in the calling task and returns only when the import is complete. Pair it with a quiet write — vfs.write(path, bytes, { quiet = true }) lands the raw bytes without firing the automatic importer, then asset.import(path) imports them under your control, so you can act on the result instead of polling for the import to appear.

Parameters

  • path string — The raw source VFS path to import (e.g. a just-written .glb).
local bundle = asset.import("/zero/source/generated/chest.glb")

modules/asset/inspect

inspect(ref: RefArg, type: string?): InspectRecord

Everything known about one asset in a single record: identity, guid, source, type, scope and origin, its description and tags, the ref methods its type exposes, and the type's own inspect detail when it declares one. The read-everything counterpart to asset.resolve, which hands back a ref.

Parameters

  • ref RefArg — An AssetRef, an identity string, or a path.
  • type string? (optional) — Narrow the resolve to one asset type when assets of several categories answer to the same bare name.
local rec = asset.inspect("@builtin::materials.default")
local rec = asset.inspect(name, "mesh"); print(rec.guid, #rec.tags)

modules/asset/list

list(type_or_opts: (AssetCategory | ListOpts)?, scope: string?, opts: ListOpts?): ListResult

Query registered assets, returning each match as a resolved AssetRef handle. Every filter narrows the same enumeration and they compose: path selects a VFS subtree (the folder and everything under it), type keeps only those asset types within it, scope keeps only that scope, and fields keeps only assets whose .metadata matches. type and path each take one value or a list matching any of its entries, and all / any / none group whole filters — none excludes what it matches. order, limit, and offset shape the result: matches come back ordered by identity unless order names another field (name / path / type / guid). Each entry is the same envelope asset.resolve returns (__ref / type / name / guid / identity / path), so it can be passed anywhere an AssetRef is accepted, and the result carries :first() / :random() / :filter() / :sort() and friends. type takes the same values asset.categories() lists. The first positional argument is a path when it is absolute, a type otherwise. An unknown key raises, as does a table setting both type and its older spelling category. A static (literal) type or path makes the enumeration part of the calling file's content dependencies when it is saved — the set travels with published content, so consumers get at-least the authoring world's assets.

Parameters

  • type_or_opts (AssetCategory | ListOpts)? (optional) — Type or VFS path filter (a static literal so the enumeration can be captured for publish), or the full query table.
  • scope string? (optional) — Scope filter (when first arg is a type).
  • opts ListOpts? (optional) — The query table — see ListOpts.
local hero = asset.list({ path = "/zero/source/props", type = "mesh", fields = { tags = "hero" } }):first()

modules/asset/list_field_values

list_field_values(key: string): { any }

Distinct values seen for the named field across every asset's .metadata.

Parameters

  • key string — Field name.
local authors = asset.list_field_values("author")

modules/asset/list_fields

list_fields(): { string }

Distinct top-level field keys observed across every asset's .metadata. Useful for tooling discovering custom keys in use.

for _, k in asset.list_fields() do print(k) end

modules/asset/meta

meta(ref: RefArg, type: string?): AssetMeta

Read the asset's engine-owned identity record (guid / checksum). Distinct from .metadata (agent-editable); for that use asset.metadata.

Parameters

  • ref RefArg — Any name the asset has.
  • type string? (optional) — Category hint (optional).
local m = asset.meta("brick") -- { guid = ..., checksum = ... }

modules/asset/metadata

metadata(ref: RefArg, type: string?): AssetMeta

Read the asset's agent-editable .metadata sidecar as a Lua table. Missing sidecar returns {}. Distinct from asset.meta (engine-owned).

Parameters

  • ref RefArg — Any name the asset has.
  • type string? (optional) — Category hint (optional).
local md = asset.metadata("brick")

modules/asset/observe

observe(): any

What the engine is holding for content right now, in one reading: textures and meshes (one row per resource the device holds, each with the bytes it costs, its dimensions or buffer split, and where it came from), cpu (one row per asset a live script-component context holds), and totals — the aggregates those rows sum to, so the listing reconciles against renderer.textureMemory() and renderer.gpuMemory().

Each pool is named because they are different pools: an asset can be on the device and not CPU-resident, or the reverse. devicePublished is false when no renderer has published an inventory and cpuPublished when the scripting VM has not published its pool — an engine that cannot answer reads differently from one answering with nothing resident.

local r = asset.observe() print(#r.textures, r.totals.textureBytes)

modules/asset/preview

preview(ref: RefArg, opts: { [string]: any }?, type: string?): { [string]: any }

Render a preview of an asset. Resolves the ref and dispatches to its type's preview ref-method when present; otherwise returns the { available = false } sentinel ("no preview available for this type").

Parameters

  • ref RefArg — Any name the asset has.
  • opts { [string]: any }? (optional) — Optional { size = { width, height }, angle = { yaw, pitch } }.
  • type string? (optional) — Category hint (optional).
local p = asset.preview("@builtin::materials.gold", { size = { width = 512, height = 512 } })

modules/asset/primaryFile

primaryFile(ref: RefArg): any

The file the asset type's declared primary list resolves to inside this asset, as the loader itself resolves it. resolved is false when no declaration matched and path is then absent; declared is the type's own primary list in match order.

Parameters

  • ref RefArg — Any name the asset has — handle, identity, guid, name or path.
print(asset.primaryFile("myTex").path)

modules/asset/ref

ref(ref: RefArg, type: string?): AssetRef

Build a reference handle for an asset — the canonical ref envelope constructor. Identical shape to asset.resolve; preferred name for the author-side use case (embedding refs in YAML / JSON / Luau output).

Naming the asset here reads exactly as naming it in asset.resolve, down to raising on a miss: a literal is recorded as this source's dependency, a computed name is a dynamic resolve — free in a tool, refused on the gameplay path. asset.exists(name, type) is the probe a computed name can ask.

Parameters

  • ref RefArg — The asset to reference — an identity, a guid, a VFS path, or a handle.
  • type string? (optional) — Category hint (optional).
local r = asset.ref("animations.idle", "animation")

modules/asset/reloadPending

reloadPending(ref: RefArg, typeName: string?): boolean

True while a write to this asset still owes it a reload — the write is inside the settle window that collects one authoring step's writes, or its reload is queued and the engine has not run it yet. False means every content change written so far has reached its subscribers, so a consumer bound to the asset now cannot be interrupted by a reload the earlier writes already earned. The recording is synchronous with the write, so a call made right after one already reads true.

Parameters

  • ref RefArg — Any name the asset has — handle, identity, guid, name or path.
  • typeName string? (optional) — Category to restrict the match to. Omit to search every category.
repeat task.wait() until not asset.reloadPending(ref)

modules/asset/reloadSeq

reloadSeq(ref: RefArg, typeName: string?): number

How many content-change reloads this asset has been through — the count of onAssetReload dispatches the engine has RUN for it. A write to a file inside an asset does not reload it on the spot: the writes of one authoring step are collected for a settle window and the reload runs on a later frame. Read this, write, then poll for a larger number to learn the write's reload has actually reached subscribers. Monotonic per asset and session-scoped; 0 for an asset whose content has not changed since boot.

Parameters

  • ref RefArg — Any name the asset has — handle, identity, guid, name or path.
  • typeName string? (optional) — Category to restrict the match to. Omit to search every category.
local at = asset.reloadSeq(ref)
vfs.write(asset.source(ref) .. "/graph.json", encoded)
repeat task.wait() until asset.reloadSeq(ref) > at

modules/asset/remove_field

remove_field(ref: RefArg, key: string)

Remove one top-level field from the asset's .metadata. No-op when the field isn't present.

Parameters

  • ref RefArg — Any name the asset has.
  • key string — Field name.
asset.remove_field("brick", "author")

modules/asset/remove_tag

remove_tag(ref: RefArg, tag: string)

Remove a tag from the asset's .metadata.tags. No-op when the tag isn't present.

Parameters

  • ref RefArg — Any name the asset has.
  • tag string — Tag to remove.
asset.remove_tag("brick", "wip")

modules/asset/resolve

resolve<C>(ref: RefArg, type: (C & string)?): AssetRef<C>

Find an asset. The returned handle carries every name form the asset has (guid, identity, path, type) so downstream code can read any one of them without calling resolve again. Raises when ref resolves to no asset — or, with a type, to no asset of that type — and when ref reaches more than one asset, where it names the candidates for you to pick from instead of picking one of them. A <scope>::-qualified identity reaches exactly one: @root::name for the asset this world holds at its source root, the library identity (@builtin::…) for a library's. For the same lookup answering a miss with nil, use asset.tryResolve(ref, type).

A name written as a string LITERAL is recorded as this source's dependency on that asset, so the asset travels with the content and still resolves once someone installs it in another world. A COMPUTED name cannot be written down, so nothing pins what it reaches: that is a dynamic resolve — free in a tool, refused on the gameplay path (a component or scene entrypoint). asset.tryResolve, asset.ref and asset.source read the name they are given exactly this way too, so which of the four you reach for changes neither answer. To ask whether a computed name has files without reaching a handle, use asset.exists(name, type).

Parameters

  • ref RefArg — The asset to find — an identity, a guid, a VFS path, or a handle.
  • type (C & string)? (optional) — Category to restrict the match to (optional). Separates a bare name that assets of different categories share (asset.resolve("cube", "mesh")); where several assets of the SAME category answer to it, the scope-qualified identity is what separates them. A reference naming a file an importer has since promoted (wall.png after the texture importer turned it into wall.texture) resolves to the promoted asset, and says so in the log once per reference.
local a = asset.resolve("@builtin::components.Camera")

modules/asset/set_field

set_field(ref: RefArg, key: string, value: any)

Set one field in the asset's .metadata, creating the sidecar if missing. Sibling fields are preserved. When the new value AND the existing value are both maps (objects), the new value DEEP-MERGES into the existing one, so writing one sub-key never drops the others — set_field(ref, "settings", { keepCpu = true }) keeps every other setting. Arrays and scalars replace. Clear a whole field with asset.remove_field; replace the entire sidecar with asset.set_metadata.

Parameters

  • ref RefArg — Any name the asset has.
  • key string — Field name.
  • value any (optional) — Field value (any JSON-serialisable Lua value).
asset.set_field("brick", "author", "me")
asset.set_field("tree", "settings", { keepCpu = true }) -- merges; sibling settings kept

modules/asset/set_metadata

set_metadata(ref: RefArg, data: AssetMeta)

Replace the asset's .metadata sidecar with the given table. Pass an empty table to clear all fields.

Parameters

  • ref RefArg — Any name the asset has.
  • data AssetMeta — Full JSON-shaped contents for the sidecar.
asset.set_metadata("brick", { author = "me", tags = { "wip" } })

modules/asset/source

source(ref: RefArg, type: string?): string

Return the VFS source path for an asset.

It reaches the asset, so the name carries the same reference contract asset.resolve's does: a literal is recorded as this source's dependency, a computed name is a dynamic resolve — free in a tool, refused on the gameplay path. asset.exists(name, type) is the probe a computed name can ask.

Parameters

  • ref RefArg — Any name the asset has.
  • type string? (optional) — Category hint (optional).
local p = asset.source("brick") -- "/source/brick.material"

modules/asset/storeHoldsIdentity

storeHoldsIdentity(identity: string, typeName: string, reproduced: boolean): boolean

Whether an asset of type typeName that name reaches has files in the store asset.create writes from this context — the authored /source tree, plus the ephemeral /runtime/assets/ store when the caller is a script component or a scene entrypoint, which is where a create there files it. name is any name the registry answers to for that asset: its canonical identity ("a.b.thing"), a registered alias, or the bare leaf ("thing") — the spelling asset.list publishes as an entry's name and the spelling asset.create was given alongside opts.folder. So the if not asset.exists(n, t) then asset.create(t, n, opts) end pairing is satisfied by its own create, spelled the way the create was, in either store and at any folder depth. A plain existence probe — it does NOT resolve a handle or pin a content dependency, so it is safe to call with a COMPUTED name (unlike asset.resolve, whose handle would become a static-pinned dependency).

The name is read the way asset.resolve reads one; the ANSWER comes from the store, so a registered asset whose files live outside the store this context writes — every @builtin:: asset among them — is false. asset.tryResolve(name, typeName) asks the registry the wider question and hands back the handle — and because it reaches the asset, the name it is given follows the reference rule every lookup follows: a literal is pinned, a computed one is a dynamic resolve. So a name this source computed is what asset.exists is for. This call names the asset FIRST and its category second, as every asset.* call taking both does except asset.create(category, name, opts). A call carrying a category where the asset goes and a non-category where the category goes is refused, naming which way round the call reads, rather than reporting the asset absent.

Parameters

  • identity string
  • typeName string — The asset type (e.g. "mesh", "texture", "material") — one of asset.categories().
  • reproduced boolean
if not asset.exists(meshName, "mesh") then asset.create("mesh", meshName, geo) end
if not asset.exists(n, "mesh") then asset.create("mesh", n, { folder = "props", positions = p, indices = i }) end

modules/asset/tags

tags(ref: RefArg): { string }

Convenience read of the .metadata.tags array.

Parameters

  • ref RefArg — Any name the asset has.
for _, t in asset.tags("brick") do print(t) end

modules/asset/tryResolve

tryResolve(ref: RefArg, type: string?, base: string?): AssetRef?

The same lookup asset.resolve performs, answering a miss with nil instead of raising. Every name form, the same type narrowing, and the same handle on success — so "use it if it is there" needs no pcall around a call whose failure would otherwise be indistinguishable from a real error.

This consults the asset REGISTRY, so it sees registered assets wherever their files live, @builtin:: ones included. asset.exists(name, type) answers the narrower question of whether an asset's files are present in the current mode's store.

A name that reaches more than one asset still raises — that is a question about the reference, not about presence, and a nil there would report absence for content that is present twice.

It reaches the asset, so the name carries the same reference contract asset.resolve's does: a literal is recorded as this source's dependency, a computed name is a dynamic resolve — free in a tool, refused on the gameplay path. asset.exists(name, type) is the probe a computed name can ask.

Parameters

  • ref RefArg — The asset to look up — an identity, a guid, a VFS path, or a handle.
  • type string? (optional) — Category to restrict the match to (optional). A reference naming a file an importer has since promoted resolves to the promoted asset, the same as asset.resolve.
  • base string? (optional) — Referring VFS path a ~ / ~.tail ref expands against, the same as asset.resolve's — so the two answer the same question and differ only in what a miss is.
local mat = asset.tryResolve(name, "material")

modules/asset/typeRef

typeRef(target: RefArg): string?

Return the pinned asset_type reference (the type's guid) that the asset is an instance of. Resolve the full type with asset.resolve(asset.typeRef(target)). Returns nil for loose files / assets with no pinned type.

Parameters

  • target RefArg — Asset handle / identity / guid / VFS path.
local t = asset.resolve(asset.typeRef("brick"))

modules/asset/unusableReasons

unusableReasons(): { string }

Every reason asset.diagnose can report an asset unusable for, sorted.

for _, r in ipairs(asset.unusableReasons()) do print(r) end

modules/asset/validate

validate(ref: RefArg, type: string?): ValidateResult

Validate an asset folder against its type's type.yaml, plus the type's own semantic validation. Structural problems come from type.yaml — missing required files, unsatisfied one_of_group alternatives, and (when allow_unlisted: false) unexpected children. validated = false when no type.yaml is registered — nothing structural to check. On top of that, when the asset's type ships a behavior.luau exporting a top-level validate(assetRef) -> { { code, message, severity? } }, its reported problems (severity defaults to "error") are appended to problems; error-severity problems flip ok to false, warnings leave it untouched. A hook that raises or returns a non-table is itself reported as a validate.hook_failed error problem — a broken hook blocks. A type with no validate export behaves exactly as the structural check alone. world.push calls this per user asset, so a type's semantic validation is enforced at publish time with no further wiring.

Parameters

  • ref RefArg — Any name the asset has.
  • type string? (optional) — Category hint (optional).
local v = asset.validate("@builtin::components.Camera")

modules/asset/warmup

warmup(ref: RefArg, opts: WarmupOpts?): WarmupResult

Warm an asset's bytes into CPU memory and follow its declared content dependencies to each referenced asset, deduped by guid. Type-agnostic (reads the generic ref graph) and CPU-only — never touches the GPU. Side-effect-free name resolution (uses asset.guid/asset.deps, not asset.resolve).

Parameters

  • ref RefArg — Any name the root asset has — handle, identity, guid, or path.
  • opts WarmupOpts? (optional) — Optional { vias, max } — restrict ref-edge kinds / cap closure size.
local w = asset.warmup("@builtin::scenes.test_arena")

typed/builtin//modules/api/engine/asset/asset/add_tag

asset.add_tag(ref: RefArg, tag: string)

Add a tag to the asset's .metadata.tags. Idempotent. Creates the sidecar and the tags array if missing.

Parameters

  • ref RefArg — Any name the asset has.
  • tag string — Tag to add.
asset.add_tag("brick", "wip")

typed/builtin//modules/api/engine/asset/asset/alias

asset.alias(ref: RefArg, alias: string) -> boolean

Add a name the asset answers to. asset.resolve, leaf shorthand, and the typed-argument coercion that content refs travel through all reach the asset by the alias from here on, exactly as they do by its identity — so a material naming a shader by its alias resolves, and content written against an older name keeps working after a rename. The name survives writes to the asset's own files. Raises when the name already resolves to a DIFFERENT asset — an alias extends the identity namespace and never takes a name out of another asset's hands — and when it is shaped like a guid or a VFS path, forms that resolve before identity lookup, so an alias in that shape could never answer.

Parameters

  • ref RefArg — The asset gaining the name.
  • alias string — The additional name. Any identity form: a bare leaf (standard) or a scope-qualified path (@builtin::shaders.legacy).

Returns boolean — True when newly added, false when the asset already answered to it.

asset.alias("@builtin::shaders.pbr", "standard")

typed/builtin//modules/api/engine/asset/asset/aliases

asset.aliases(ref: RefArg) -> { string }

The additional names this asset answers to, beyond its own identity — what asset.alias registered, plus the package-relative ~pkg.tail form when the asset lives inside a package.

Parameters

  • ref RefArg — Any name the asset has.

Returns { string } — Array of alias names in canonical identity form.

for _, n in asset.aliases("pbr") do print(n) end

typed/builtin//modules/api/engine/asset/asset/canCreate

asset.canCreate(typeName: string) -> boolean

Whether asset.create can instance typeName: the type declares creation logic (a behavior.luau onCreate hook) or ships a template/ skeleton the hookless fallback clones. A type with neither — one whose instances only arrive by import — answers false. The query a creation UI derives its offering from, so what it offers is what asset.create accepts.

Parameters

  • typeName string — Registered asset type (e.g. "material", "scene").

Returns boolean — true when asset.create(typeName, …) can produce one.

if asset.canCreate(kind) then asset.create(kind, name) end

typed/builtin//modules/api/engine/asset/asset/categories

asset.categories() -> { string }

List every asset category the engine currently recognises. Use to discover valid type argument values for the rest of asset.*.

Returns { string } — Array of category names.

for _, c in asset.categories() do print(c) end

typed/builtin//modules/api/engine/asset/asset/containing

asset.containing(path: string) -> AssetRef?

Walk path's ancestors and return an AssetRef handle for the OUTERMOST category-folder containing it (e.g. main.scene for "/source/scenes/main.scene/scene.json"). Returns nil for paths outside any registered asset type.

Parameters

  • path string — VFS path to inspect.

Returns AssetRef? — AssetRef handle, or nil.

local a = asset.containing("/source/scenes/main.scene/scene.json")

typed/builtin//modules/api/engine/asset/asset/cpuResident

asset.cpuResident(ref: RefArg, typeName: string?) -> boolean

True when the asset is CPU-resident — a live script-component context holds it (a component's assetRef field, or an imperative asset.resolve/ref made while a component is the caller), which is what warms its bytes into memory. The CPU pool is a different pool from the device's: asset.observe().cpu lists it, asset.observe().textures / .meshes list what the device holds, and an asset can be in one and not the other.

Parameters

  • ref RefArg — Any name the asset has — handle, identity, guid, name or path.
  • typeName string (optional) — Category to restrict the match to. Omit to search every category.

Returns boolean — true when a live context holds it.

if asset.cpuResident(ref) then print("bytes are warm") end

typed/builtin//modules/api/engine/asset/asset/create

asset.create(typeName: string, name: string, opts: { [string]: any }?) -> AssetRef

Instance a new asset of an existing type. Runs the type's behavior.luau onCreate(name, opts) hook to produce the asset's files, then writes them under /source/<name>.<type>/. This is the single generic asset-creation API. Refuses to clobber an existing edit-mode asset unless opts.overwrite = true, which re-authors it in place and keeps the existing guid (only the checksum changes). Pairs with asset.exists for content generators that re-run over the same names.

A create made from a script component's callback or a scene entrypoint is output the world reproduces on every load, so it is filed in the ephemeral /runtime/assets/ store instead, where the saved manifest never carries a second copy of it. name and folder spell the same IDENTITY in either store, so a reference written against that identity resolves the asset wherever the call filed it, and one generator run from an execute and from a component names one asset.

Parameters

  • typeName string — Registered asset type to instance (e.g. "material", "texture").
  • name string — Destination asset name (becomes /source/<name>.<typeName>). A bare identity — pass opts.folder to place it in a subfolder rather than spelling a path here. The accepted shape is the type's to declare: ^[A-Za-z][A-Za-z0-9_]*$ unless its behavior.luau exports a namePattern, as guide does to take getting-started and 01-overview. This call names the category FIRST and the asset second. Every other asset.* call taking both names them the other way round — asset.exists(name, category), asset.tryResolve(ref, category) — so a create-then-check pair reads if not asset.exists(n, t) then asset.create(t, n, opts) end. A call whose two arguments are read into each other, at either end of that pair, is refused and told which way round the call reads.
  • opts { [string]: any } (optional) — Optional table forwarded to the type's onCreate hook, minus four framework keys consumed here and never seen by the hook: folder (a relative subfolder under /source to author the asset in, so generated content groups instead of accumulating at the source root, and the asset's identity carries that folder as its dotted prefix), into (author INSIDE a resolved container ref), dest (an absolute destination path), and overwrite (re-author in place, keeping the guid).

Returns AssetRef — The created asset's AssetRef — the SAME interned instance asset.resolve returns (guid/__ref/path + the type's ref methods: :getBytes, :ensureHandle, :serialize, …). Disk-only: nothing is uploaded to CPU/GPU.

local m = asset.create("material", "brick", { shader = "pbr" })
local mesh = asset.create("mesh", "tree", { positions = {...}, indices = {...} })
local mesh = asset.create("mesh", "rock", { positions = {...}, indices = {...}, folder = "terrain/props" })
local mesh = asset.create("mesh", name, { positions = p, indices = i, overwrite = true }) -- idempotent rebuild

typed/builtin//modules/api/engine/asset/asset/declareReferenceArg

asset.declareReferenceArg(call: string, position: number, assetType: string)

Declare that call's argument at 1-based position names an asset of type, so a string literal written there is recorded as a reference. The positional counterpart of asset.declareReferenceField, for a call that takes its asset as a plain argument — including a world's own spawn helper, which is where a name most often stops being visible to the reference graph. A lookup whose asset is its FIRST argument (asset.resolve and its siblings) is already read and needs no declaration. Only a literal — or a name the file holds in a top-level string constant — is recorded; anything computed is reported as a dynamic resolve.

Parameters

  • call string — The callee as it is written at a call site.
  • position number — Which argument holds the name, counting from 1.
  • assetType string
asset.declareReferenceArg("spawnModel", 3, "mesh")

typed/builtin//modules/api/engine/asset/asset/declareReferenceField

asset.declareReferenceField(call: string, field: string, assetType: string)

Declare that call's options table names an asset of type in its field, so a string literal written there is recorded as a reference by whatever writes the file. This is what puts an API that takes an asset BY NAME into the reference graph: the named asset becomes a dependency, travels with the content that names it into a pack or a pull, and a name nothing answers to becomes an unresolved dependency worldValidation reports and the push gate refuses. A field holding a TABLE of names — a material's textures — records every name in it. Declare once, beside the API; a call taking its asset as the FIRST positional argument is already read and needs no declaration. Only a literal is recorded; a computed name resolves at runtime and is reported as a dynamic resolve.

Parameters

  • call string — The callee as it is written at a call site.
  • field string — The options-table field holding the name, read at the table's own level.
  • assetType string
asset.declareReferenceField("fx.beam", "material", "material")

typed/builtin//modules/api/engine/asset/asset/declareReferenceKey

asset.declareReferenceKey(assetType: string, key: string, refType: string)

Declare that, in a data file belonging to an assetType asset, the top-level key names an asset of type — a .material's mat.yaml naming the shader it draws with and the textures it binds. The names a format holds are references as surely as ones written in code: recording them carries a material's shader along with the material into a pack or a pull, and turns a name nothing answers to into an unresolved dependency instead of a surface that renders as the magenta error material. A key holding a table of names records one per entry.

Parameters

  • assetType string — The category owning the file, e.g. "material".
  • key string — The top-level key holding the name(s).
  • refType string
asset.declareReferenceKey("material", "shader", "shader")

typed/builtin//modules/api/engine/asset/asset/deps

asset.deps(ref: RefArg, type: string?) -> DepsResult

Return the asset's outbound dependency graph — every other asset recorded as a content dependency of it.

Parameters

  • ref RefArg — Any name the asset has.
  • type string (optional) — Category hint (optional).

Returns DepsResult{ deps = { { asset_guid, origin, literal, via, ... } } }.

for _, d in asset.deps("main.scene").deps do print(d.asset_guid) end

typed/builtin//modules/api/engine/asset/asset/describe

asset.describe(typeName: string) -> DescribeResult

The creation contract for an asset type: the parameters its onCreate(name, opts) hook accepts, as data. kind is "schema" (typed contract), "legacy" (untyped opts — anything passes), "none" (template scaffold — takes no opts), or "error" (the type's schema failed to parse; error says why). contract is the human-readable rendering validation errors print.

Parameters

  • typeName string — Registered asset type to describe (e.g. "texture").

Returns DescribeResult — the creation contract.

local contract = asset.describe("texture").contract

typed/builtin//modules/api/engine/asset/asset/diagnose

asset.diagnose(ref: RefArg) -> any

Why one asset can or cannot be used, read from the engine rather than from what the caller asked for. Always carries usable; when false, reason is one of asset.unusableReasons() and detail is the engine's own message. primary names the file the type's declared primary list resolved to, so an asset that loaded a preview image instead of its payload shows the wrong filename rather than a successful load. The payload's bytes are read by the engine's own decoder wherever it has one for that container, so usable is the verdict a load would reach and the call costs that decode.

Parameters

  • ref RefArg — Any name the asset has — handle, identity, guid, name or path.

Returns any — DiagnoseRecord

local d = asset.diagnose("myTex") if not d.usable then print(d.reason, d.detail) end

typed/builtin//modules/api/engine/asset/asset/exists

asset.exists(name: string, typeName: string) -> boolean

Parameters

  • name string
  • typeName string

Returns boolean

typed/builtin//modules/api/engine/asset/asset/get_field

asset.get_field(ref: RefArg, key: string) -> any

Read one top-level field from the asset's .metadata.

Parameters

  • ref RefArg — Any name the asset has.
  • key string — Field name.

Returns any — Field value or nil.

local author = asset.get_field("brick", "author")
local settings = asset.get_field("tree", "settings") -- → a Luau table

typed/builtin//modules/api/engine/asset/asset/gpuResident

asset.gpuResident(ref: RefArg) -> boolean

True when the device holds a texture or mesh under this asset's guid, read off the inventory the renderer publishes.

Parameters

  • ref RefArg — Any name the asset has — handle, identity, guid, name or path.

Returns boolean — true when the device holds it.

print(asset.gpuResident("@builtin::models.Sample.DamagedHelmet"))

typed/builtin//modules/api/engine/asset/asset/guid

asset.guid(ref: RefArg, type: string?) -> string

Return the guid for an asset.

Parameters

  • ref RefArg — Any name the asset has.
  • type string (optional) — Category hint (optional).

Returns string — Guid.

local g = asset.guid("@builtin::components.Camera")

typed/builtin//modules/api/engine/asset/asset/has_field

asset.has_field(ref: RefArg, key: string) -> boolean

True when the asset's .metadata carries the named field.

Parameters

  • ref RefArg — Any name the asset has.
  • key string — Field name.

Returns boolean — True when present.

if asset.has_field("brick", "author") then end

typed/builtin//modules/api/engine/asset/asset/has_tag

asset.has_tag(ref: RefArg, tag: string) -> boolean

True when the asset's .metadata.tags contains tag.

Parameters

  • ref RefArg — Any name the asset has.
  • tag string — Tag to check for.

Returns boolean — True when present.

if asset.has_tag("brick", "wip") then end

typed/builtin//modules/api/engine/asset/asset/identity

asset.identity(ref: RefArg, type: string?) -> string

Return the canonical identity for an asset.

Parameters

  • ref RefArg — Any name the asset has.
  • type string (optional) — Category hint (optional).

Returns string — Canonical identity.

local id = asset.identity("brick")

typed/builtin//modules/api/engine/asset/asset/import

asset.import(path: string) -> string?

Import a raw source file NOW and return the produced asset path (a .bundle for a model, .texture for an image, .audio for a sound, …), or nil if no importer claims it. This is the deterministic, on-demand counterpart to the engine's automatic import-on-write: it runs in the calling task and returns only when the import is complete. Pair it with a quiet write — vfs.write(path, bytes, { quiet = true }) lands the raw bytes without firing the automatic importer, then asset.import(path) imports them under your control, so you can act on the result instead of polling for the import to appear.

Parameters

  • path string — The raw source VFS path to import (e.g. a just-written .glb).

Returns string? — The produced asset path, or nil when nothing claimed it.

local bundle = asset.import("/zero/source/generated/chest.glb")

typed/builtin//modules/api/engine/asset/asset/inspect

asset.inspect(ref: RefArg, type: string?) -> InspectRecord

Everything known about one asset in a single record: identity, guid, source, type, scope and origin, its description and tags, the ref methods its type exposes, and the type's own inspect detail when it declares one. The read-everything counterpart to asset.resolve, which hands back a ref.

Parameters

  • ref RefArg — An AssetRef, an identity string, or a path.
  • type string (optional) — Narrow the resolve to one asset type when assets of several categories answer to the same bare name.

Returns InspectRecord — The inspect record.

local rec = asset.inspect("@builtin::materials.default")
local rec = asset.inspect(name, "mesh"); print(rec.guid, #rec.tags)

typed/builtin//modules/api/engine/asset/asset/list

asset.list(type_or_opts: (AssetCategory | ListOpts)?, scope: string?, opts: ListOpts?) -> ListResult

Query registered assets, returning each match as a resolved AssetRef handle. Every filter narrows the same enumeration and they compose: path selects a VFS subtree (the folder and everything under it), type keeps only those asset types within it, scope keeps only that scope, and fields keeps only assets whose .metadata matches. type and path each take one value or a list matching any of its entries, and all / any / none group whole filters — none excludes what it matches. order, limit, and offset shape the result: matches come back ordered by identity unless order names another field (name / path / type / guid). Each entry is the same envelope asset.resolve returns (__ref / type / name / guid / identity / path), so it can be passed anywhere an AssetRef is accepted, and the result carries :first() / :random() / :filter() / :sort() and friends. type takes the same values asset.categories() lists. The first positional argument is a path when it is absolute, a type otherwise. An unknown key raises, as does a table setting both type and its older spelling category. A static (literal) type or path makes the enumeration part of the calling file's content dependencies when it is saved — the set travels with published content, so consumers get at-least the authoring world's assets.

Parameters

  • type_or_opts (AssetCategory | ListOpts) (optional) — Type or VFS path filter (a static literal so the enumeration can be captured for publish), or the full query table.
  • scope string (optional) — Scope filter (when first arg is a type).
  • opts ListOpts (optional) — The query table — see ListOpts.

Returns ListResult — The matched AssetRef handles, as a result carrying query methods.

local hero = asset.list({ path = "/zero/source/props", type = "mesh", fields = { tags = "hero" } }):first()

typed/builtin//modules/api/engine/asset/asset/list_field_values

asset.list_field_values(key: string) -> { any }

Distinct values seen for the named field across every asset's .metadata.

Parameters

  • key string — Field name.

Returns { any } — Array of distinct values.

local authors = asset.list_field_values("author")

typed/builtin//modules/api/engine/asset/asset/list_fields

asset.list_fields() -> { string }

Distinct top-level field keys observed across every asset's .metadata. Useful for tooling discovering custom keys in use.

Returns { string } — Array of field names.

for _, k in asset.list_fields() do print(k) end

typed/builtin//modules/api/engine/asset/asset/meta

asset.meta(ref: RefArg, type: string?) -> AssetMeta

Read the asset's engine-owned identity record (guid / checksum). Distinct from .metadata (agent-editable); for that use asset.metadata.

Parameters

  • ref RefArg — Any name the asset has.
  • type string (optional) — Category hint (optional).

Returns AssetMeta — Metadata table.

local m = asset.meta("brick") -- { guid = ..., checksum = ... }

typed/builtin//modules/api/engine/asset/asset/metadata

asset.metadata(ref: RefArg, type: string?) -> AssetMeta

Read the asset's agent-editable .metadata sidecar as a Lua table. Missing sidecar returns {}. Distinct from asset.meta (engine-owned).

Parameters

  • ref RefArg — Any name the asset has.
  • type string (optional) — Category hint (optional).

Returns AssetMeta — JSON-shaped table; empty when no sidecar exists.

local md = asset.metadata("brick")

typed/builtin//modules/api/engine/asset/asset/observe

asset.observe() -> any

What the engine is holding for content right now, in one reading: textures and meshes (one row per resource the device holds, each with the bytes it costs, its dimensions or buffer split, and where it came from), cpu (one row per asset a live script-component context holds), and totals — the aggregates those rows sum to, so the listing reconciles against renderer.textureMemory() and renderer.gpuMemory().

Each pool is named because they are different pools: an asset can be on the device and not CPU-resident, or the reverse. devicePublished is false when no renderer has published an inventory and cpuPublished when the scripting VM has not published its pool — an engine that cannot answer reads differently from one answering with nothing resident.

Returns any — ResidencyReading

local r = asset.observe() print(#r.textures, r.totals.textureBytes)

typed/builtin//modules/api/engine/asset/asset/preview

asset.preview(ref: RefArg, opts: { [string]: any }?, type: string?) -> { [string]: any }

Render a preview of an asset. Resolves the ref and dispatches to its type's preview ref-method when present; otherwise returns the { available = false } sentinel ("no preview available for this type").

Parameters

  • ref RefArg — Any name the asset has.
  • opts { [string]: any } (optional) — Optional { size = { width, height }, angle = { yaw, pitch } }.
  • type string (optional) — Category hint (optional).

Returns { [string]: any }{ available, imageBase64?, width?, height?, bounds?, stats?, reason? }.

local p = asset.preview("@builtin::materials.gold", { size = { width = 512, height = 512 } })

typed/builtin//modules/api/engine/asset/asset/primaryFile

asset.primaryFile(ref: RefArg) -> any

The file the asset type's declared primary list resolves to inside this asset, as the loader itself resolves it. resolved is false when no declaration matched and path is then absent; declared is the type's own primary list in match order.

Parameters

  • ref RefArg — Any name the asset has — handle, identity, guid, name or path.

Returns any{ path: string?, resolved: boolean, isFolder: boolean, declared: { string } }

print(asset.primaryFile("myTex").path)

typed/builtin//modules/api/engine/asset/asset/ref

asset.ref(ref: RefArg, type: string?) -> AssetRef

Build a reference handle for an asset — the canonical ref envelope constructor. Identical shape to asset.resolve; preferred name for the author-side use case (embedding refs in YAML / JSON / Luau output).

Naming the asset here reads exactly as naming it in asset.resolve, down to raising on a miss: a literal is recorded as this source's dependency, a computed name is a dynamic resolve — free in a tool, refused on the gameplay path. asset.exists(name, type) is the probe a computed name can ask.

typed/builtin//modules/api/engine/asset/asset/reloadPending

asset.reloadPending(ref: RefArg, typeName: string?) -> boolean

True while a write to this asset still owes it a reload — the write is inside the settle window that collects one authoring step's writes, or its reload is queued and the engine has not run it yet. False means every content change written so far has reached its subscribers, so a consumer bound to the asset now cannot be interrupted by a reload the earlier writes already earned. The recording is synchronous with the write, so a call made right after one already reads true.

Parameters

  • ref RefArg — Any name the asset has — handle, identity, guid, name or path.
  • typeName string (optional) — Category to restrict the match to. Omit to search every category.

Returns boolean — true while a content-change reload is still owed.

repeat task.wait() until not asset.reloadPending(ref)

typed/builtin//modules/api/engine/asset/asset/reloadSeq

asset.reloadSeq(ref: RefArg, typeName: string?) -> number

How many content-change reloads this asset has been through — the count of onAssetReload dispatches the engine has RUN for it. A write to a file inside an asset does not reload it on the spot: the writes of one authoring step are collected for a settle window and the reload runs on a later frame. Read this, write, then poll for a larger number to learn the write's reload has actually reached subscribers. Monotonic per asset and session-scoped; 0 for an asset whose content has not changed since boot.

Parameters

  • ref RefArg — Any name the asset has — handle, identity, guid, name or path.
  • typeName string (optional) — Category to restrict the match to. Omit to search every category.

Returns number — content-change reloads dispatched for this asset.

local at = asset.reloadSeq(ref)
vfs.write(asset.source(ref) .. "/graph.json", encoded)
repeat task.wait() until asset.reloadSeq(ref) > at

typed/builtin//modules/api/engine/asset/asset/remove_field

asset.remove_field(ref: RefArg, key: string)

Remove one top-level field from the asset's .metadata. No-op when the field isn't present.

Parameters

  • ref RefArg — Any name the asset has.
  • key string — Field name.
asset.remove_field("brick", "author")

typed/builtin//modules/api/engine/asset/asset/remove_tag

asset.remove_tag(ref: RefArg, tag: string)

Remove a tag from the asset's .metadata.tags. No-op when the tag isn't present.

Parameters

  • ref RefArg — Any name the asset has.
  • tag string — Tag to remove.
asset.remove_tag("brick", "wip")

typed/builtin//modules/api/engine/asset/asset/resolve

asset.resolve(ref: RefArg, type: (C & string)?) -> AssetRef<C>

Find an asset. The returned handle carries every name form the asset has (guid, identity, path, type) so downstream code can read any one of them without calling resolve again. Raises when ref resolves to no asset — or, with a type, to no asset of that type — and when ref reaches more than one asset, where it names the candidates for you to pick from instead of picking one of them. A <scope>::-qualified identity reaches exactly one: @root::name for the asset this world holds at its source root, the library identity (@builtin::…) for a library's. For the same lookup answering a miss with nil, use asset.tryResolve(ref, type).

A name written as a string LITERAL is recorded as this source's dependency on that asset, so the asset travels with the content and still resolves once someone installs it in another world. A COMPUTED name cannot be written down, so nothing pins what it reaches: that is a dynamic resolve — free in a tool, refused on the gameplay path (a component or scene entrypoint). asset.tryResolve, asset.ref and asset.source read the name they are given exactly this way too, so which of the four you reach for changes neither answer. To ask whether a computed name has files without reaching a handle, use asset.exists(name, type).

Parameters

  • ref RefArg — The asset to find — an identity, a guid, a VFS path, or a handle.
  • type (C & string) (optional) — Category to restrict the match to (optional). Separates a bare name that assets of different categories share (asset.resolve("cube", "mesh")); where several assets of the SAME category answer to it, the scope-qualified identity is what separates them. A reference naming a file an importer has since promoted (wall.png after the texture importer turned it into wall.texture) resolves to the promoted asset, and says so in the log once per reference.

Returns AssetRef<C> — Asset handle, carrying the category when one was named — so the methods that category defines are checked on the result. Raises (rather than returning nil) on a miss, and on a name that reaches more than one asset.

local a = asset.resolve("@builtin::components.Camera")

typed/builtin//modules/api/engine/asset/asset/set_field

asset.set_field(ref: RefArg, key: string, value: any?)

Set one field in the asset's .metadata, creating the sidecar if missing. Sibling fields are preserved. When the new value AND the existing value are both maps (objects), the new value DEEP-MERGES into the existing one, so writing one sub-key never drops the others — set_field(ref, "settings", { keepCpu = true }) keeps every other setting. Arrays and scalars replace. Clear a whole field with asset.remove_field; replace the entire sidecar with asset.set_metadata.

Parameters

  • ref RefArg — Any name the asset has.
  • key string — Field name.
  • value any (optional) — Field value (any JSON-serialisable Lua value).
asset.set_field("brick", "author", "me")
asset.set_field("tree", "settings", { keepCpu = true }) -- merges; sibling settings kept

typed/builtin//modules/api/engine/asset/asset/set_metadata

asset.set_metadata(ref: RefArg, data: AssetMeta)

Replace the asset's .metadata sidecar with the given table. Pass an empty table to clear all fields.

Parameters

  • ref RefArg — Any name the asset has.
  • data AssetMeta — Full JSON-shaped contents for the sidecar.
asset.set_metadata("brick", { author = "me", tags = { "wip" } })

typed/builtin//modules/api/engine/asset/asset/source

asset.source(ref: RefArg, type: string?) -> string

Return the VFS source path for an asset.

It reaches the asset, so the name carries the same reference contract asset.resolve's does: a literal is recorded as this source's dependency, a computed name is a dynamic resolve — free in a tool, refused on the gameplay path. asset.exists(name, type) is the probe a computed name can ask.

Parameters

  • ref RefArg — Any name the asset has.
  • type string (optional) — Category hint (optional).

Returns string — VFS source path.

local p = asset.source("brick") -- "/source/brick.material"

typed/builtin//modules/api/engine/asset/asset/tags

asset.tags(ref: RefArg) -> { string }

Convenience read of the .metadata.tags array.

Parameters

  • ref RefArg — Any name the asset has.

Returns { string } — Array of tag strings.

for _, t in asset.tags("brick") do print(t) end

typed/builtin//modules/api/engine/asset/asset/tryResolve

asset.tryResolve(ref: RefArg, type: string?, base: string?) -> AssetRef?

The same lookup asset.resolve performs, answering a miss with nil instead of raising. Every name form, the same type narrowing, and the same handle on success — so "use it if it is there" needs no pcall around a call whose failure would otherwise be indistinguishable from a real error.

This consults the asset REGISTRY, so it sees registered assets wherever their files live, @builtin:: ones included. asset.exists(name, type) answers the narrower question of whether an asset's files are present in the current mode's store.

A name that reaches more than one asset still raises — that is a question about the reference, not about presence, and a nil there would report absence for content that is present twice.

It reaches the asset, so the name carries the same reference contract asset.resolve's does: a literal is recorded as this source's dependency, a computed name is a dynamic resolve — free in a tool, refused on the gameplay path. asset.exists(name, type) is the probe a computed name can ask.

Parameters

  • ref RefArg — The asset to look up — an identity, a guid, a VFS path, or a handle.
  • type string (optional) — Category to restrict the match to (optional). A reference naming a file an importer has since promoted resolves to the promoted asset, the same as asset.resolve.
  • base string (optional) — Referring VFS path a ~ / ~.tail ref expands against, the same as asset.resolve's — so the two answer the same question and differ only in what a miss is.

Returns AssetRef? — Asset handle, or nil when the reference resolves to no asset.

local mat = asset.tryResolve(name, "material")

typed/builtin//modules/api/engine/asset/asset/typeRef

asset.typeRef(target: RefArg) -> string?

Return the pinned asset_type reference (the type's guid) that the asset is an instance of. Resolve the full type with asset.resolve(asset.typeRef(target)). Returns nil for loose files / assets with no pinned type.

typed/builtin//modules/api/engine/asset/asset/unusableReasons

asset.unusableReasons() -> { string }

Every reason asset.diagnose can report an asset unusable for, sorted.

Returns { string } — Array of reason names.

for _, r in ipairs(asset.unusableReasons()) do print(r) end

typed/builtin//modules/api/engine/asset/asset/validate

asset.validate(ref: RefArg, type: string?) -> ValidateResult

Validate an asset folder against its type's type.yaml, plus the type's own semantic validation. Structural problems come from type.yaml — missing required files, unsatisfied one_of_group alternatives, and (when allow_unlisted: false) unexpected children. validated = false when no type.yaml is registered — nothing structural to check. On top of that, when the asset's type ships a behavior.luau exporting a top-level validate(assetRef) -> { { code, message, severity? } }, its reported problems (severity defaults to "error") are appended to problems; error-severity problems flip ok to false, warnings leave it untouched. A hook that raises or returns a non-table is itself reported as a validate.hook_failed error problem — a broken hook blocks. A type with no validate export behaves exactly as the structural check alone. world.push calls this per user asset, so a type's semantic validation is enforced at publish time with no further wiring.

Parameters

  • ref RefArg — Any name the asset has.
  • type string (optional) — Category hint (optional).

Returns ValidateResult{ ok, typeName, validated, problems }.

local v = asset.validate("@builtin::components.Camera")

typed/builtin//modules/api/engine/asset/asset/warmup

asset.warmup(ref: RefArg, opts: WarmupOpts?) -> WarmupResult

Warm an asset's bytes into CPU memory and follow its declared content dependencies to each referenced asset, deduped by guid. Type-agnostic (reads the generic ref graph) and CPU-only — never touches the GPU. Side-effect-free name resolution (uses asset.guid/asset.deps, not asset.resolve).

Parameters

  • ref RefArg — Any name the root asset has — handle, identity, guid, or path.
  • opts WarmupOpts (optional) — Optional { vias, max } — restrict ref-edge kinds / cap closure size.

Returns WarmupResult{ closure, count } — the deduped guid closure warmed and its size.

local w = asset.warmup("@builtin::scenes.test_arena")
  • api
  • reference