Log inGet started

Importer (asset type)

Updated 22 August 2026

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 resolve it via asset.resolve. 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 .importer suffix strips).
  • Folder shape:
    • init.luau (or init.lua) — exports canImport and import. Required.
    • README.md — instance documentation. Required.
    • .metadata — agent-editable tags + free-form fields. Required.

How to create one

asset.create("importer", "<name>")
-- Creates: /zero/source/<name>.importer/
--   init.luau   (canImport / import stubs)
--   README.md   (instance README template)

-- `folder` places it in a subfolder of /source instead of the root:
asset.create("importer", "<name>", { folder = "importers" })

How it operates

  1. Registration. Writing init.luau into a .importer/ folder indexes the importer with the importer registry.
  2. 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 its import function.
  3. 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.
  4. Idempotency. import MUST 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.
  5. 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 imported again — writing the source path re-dispatches the importers, and tools.use("importers", "run", <path>) re-imports a target, many targets, or a whole folder on demand.

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.
  • tools.use("importers", "list") — every registered importer, with its name and identity.
  • tools.use("importers", "explain", <path>) — which importers claim a given source and which gate would block the import.

Authoring conventions

  • Keep canImport cheap — it's called for every VFS write. Match on extension or a quick header probe; defer real parsing to import.
  • Name derived assets predictably (e.g. imported/<source-stem>/<asset-name>). Predictable paths let callers reference derived assets directly.
  • Make import pure 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.
  • .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.
  • asset-type
  • reference