Importer (asset type)
An importer is a Luau function that converts a source file (GLB, PNG, CSV, custom binary) into one or more derived assets registered in the engine. Importers extend the engine's file-format knowledge…
When to use one
- You're bringing a new file format into the engine and want to derive engine-native assets from it (meshes, textures, animations, data tables).
- You want one VFS write to fan out into multiple derived assets (e.g. a GLB write derives mesh, texture, and animation assets).
- You want format ingestion to be idempotent + hot-reloadable — importers re-run on source-file changes.
If you only need to consume an existing engine asset type, you don't
need an importer; just load via asset.load. If you're building an
authoring tool that emits assets, that's a .tool, not an
importer.
Where it lives
- Source:
/zero/source/.../<name>.importer/ - Identity:
<name>(the.importersuffix strips). - Folder shape:
init.luau(orinit.lua) — exportscanImportandimport. Required.README.md— instance documentation. Required.
How to create one
asset.create("importer", "<name>")
-- Creates: /zero/source/importers/<name>.importer/
-- init.luau (canImport / import stubs)
-- README.md (instance README template)
How it operates
- Registration. Writing
init.luauinto a.importer/folder indexes the importer with the importer registry. - Dispatch. When a file is written to VFS, the engine asks every
registered importer
canImport(path, bytes)(cheap predicate). The first importer that accepts dispatches to itsimportfunction. - Derivation.
import(path, bytes)returns a list of{ path, kind }pairs naming the assets to derive. The engine instantiates each derived asset against its registered category. - Idempotency.
importMUST be deterministic — importing the same source bytes twice produces identical derived assets. The importer pipeline uses content-hashing to short-circuit re-imports when bytes haven't changed. - Hot reload. Editing the importer reloads it; subsequent writes use the new logic. Already-imported assets stay imported with the old derivation until their source is re-written — any write of the source path re-dispatches the importers (that is the only trigger; there is no separate "run" call). Writing the source is therefore both the first import and the way to force a re-import.
Discovery
asset.list("importer")— every registered importer.asset.inspect("<name>")— claimed extensions, derivation conventions, source, this type README.cat /zero/source/<name>.importer— same summary./zero/importers/index(VFS) — the importer registry.
Authoring conventions
- Keep
canImportcheap — it's called for every VFS write. Match on extension or a quick header probe; defer real parsing toimport. - Name derived assets predictably (e.g.
imported/<source-stem>/<asset-name>). Predictable paths let callers reference derived assets directly. - Make
importpure relative to its inputs. Don't read mutable globals or side-channel state. - Log derivations via
print/ the script log so authors can see what got produced from each source.
Common pitfalls
- Expensive
canImport. It runs on every VFS write; an expensive predicate slows down the entire authoring loop. - Non-idempotent
import. Re-importing produces different output → content hashing falsely caches stale results. Use only deterministic transforms. - Conflicting importers. Two importers claiming the same extension causes ambiguous dispatch. First-registered wins, but this is fragile — coordinate ownership of file extensions.
- Derived-asset suffixes. Make sure derived paths end in the
right
.<category>suffix; otherwise they won't register as recognized assets.
Related types
.module— for plain ingestion helpers that don't need the importer dispatch path..tool— for one-off conversions an agent invokes manually..service— if your importer needs persistent state (e.g. a cache); the service owns it, the importer reads from it.