Log inGet started

Generating assets & content

Updated October 19, 2026

Generation is driven through the services toolbox: discover what you can make, start a run, track it to a finished asset. You never resolve or invoke a generator by hand — you work the tool rail, and every generator behaves the same way regardless of what it produces.

What you can generate

Each generator is a service the toolbox lists for you, self-describing down to each operation's inputs and credit cost:

zero services list
# mesh_gen   → a textured 3D mesh (GLB), from a prompt or a reference image
# image_gen  → an image (PNG) from a prompt
# pbr_gen    → a PBR material (albedo / normal / roughness / …) from a prompt
# audio_gen  → music, a sound effect, or speech from a prompt
# anim_gen   → a humanoid animation clip from a text prompt
# world_gen  → a navigable gaussian-splat world from a prompt

Each service exposes one or more operations, and list reports every operation's inputs and credit cost — so you read what to pass without looking anywhere else.

The set is open — read list rather than memorising it, and check balance for the credits a run draws from:

zero services balance

Three surfaces, pick the one that matches where you are

Like every toolbox, the services tools answer on three surfaces, one per context (the tool system is covered in full in the tools guide):

zero, from the shell. zero services lists the verbs and zero services <verb> --help prints one verb's arguments and types, so you can read the call before you make it:

zero services list
zero services generate mesh_gen '{"prompt":"a wooden treasure chest"}'

tool_exec, the MCP call, when you're driving the engine from outside. search_tools finds a tool, and tool_exec runs a short JavaScript script that calls it, positional arguments in signature order; the script's return is the answer's value:

// the `script` of a tool_exec call
return await zero.services.generate("mesh_gen", { prompt: "a wooden treasure chest" })
// → value: { id: "gen_0", service: "mesh_gen", operation: "generate" }

tools.use(toolbox, tool, ...) — from Luau, when you're authoring in code. Inside an execute() call it runs the tool and hands back the value directly (raising on failure):

local g = tools.use("services", "generate", "mesh_gen", { prompt = "a wooden treasure chest" })
-- g is the handle { id, service, operation }

All three do the same thing; the examples below use the Luau form for brevity.

Kick off a generation, then track it

generate returns immediately with a handle; the run takes minutes and outlives the call. Poll status across turns — each call is quick — until status == "completed", then the row's asset is the spawnable asset:

local g = tools.use("services", "generate", "mesh_gen", { prompt = "a wooden treasure chest" })

-- in a LATER turn rather than busy-polling this one:
local s = tools.use("services", "status", g.id)   -- { status, progress, asset, error, … }
if s.status == "completed" then
    -- s.asset is the imported, spawnable asset
end

A call that names only a prompt gets a game-ready result. A mesh_gen model arrives PBR-textured at 2k and retopologised to about 30000 polygons, or 50000 for a generate_rigged character. Ask for more where an asset is seen up close: texture_resolution = "4k", a higher target_polycount (up to 300000), or should_remesh = false for the provider's raw density of several hundred thousand triangles or more. The list tool shows every operation's defaults.

The run imports its raw output into a spawnable asset — a mesh becomes a .bundle (mesh + material + texture), an image a .texture, a sound a .audio. s.asset points at that imported asset, ready to spawn / apply / play. A run that can't proceed fails with a legible reason on s.error ("out of credits", "not signed in", "upstream 401") rather than finishing empty. jobs lists every run this session; outputs lists what you've generated across sessions.

Where a run's output lands during play

A run files its output where your writes landed when you asked for it. A run you start in edit (or while someone else plays and you do not) keeps its asset and its run record on canonical /source even when it completes after you entered play: you asked for it in edit, so a play-exit discard leaves it standing. A run you start from inside your own play session writes its asset onto the play shadow, like any other /source write made during that play: it is live in the session and listed by vfs.playShadowPaths(), and a play-exit discard takes it back. Keep it with vfs.promotePlayShadow(s.asset), which takes the whole asset, every file of it, onto source.

The durable input states it outright: durable = true files the asset on canonical /source the moment the run completes wherever it was started, and durable = false leaves it wherever the writes of that moment land:

local g = tools.use("services", "generate", "image_gen", { prompt = "a mossy stone idol", durable = true })

A run filing on source that cannot file its asset there fails, and s.error names why. Every reading of a run says where its output stands: the generation finished notice carries durable, and when the play shadow holds any of it, the warning and the shadowed paths; status and jobs rows carry durability, read at the moment you ask, with durable, the paths the shadow holds in shadowed and the routes that keep them.

Dropping it into a scene you've already laid out

You don't have to stare at an empty slot while a generation runs, and you never have to leave a rough stand-in in the finished scene. Lay the scene out, then replace each entity with its generated asset in place — same position, rotation, scale, name, and parent — with appearance.replaceWithContent:

local g = tools.use("services", "generate", "mesh_gen", { prompt = "a carved oak chest" })
-- …poll services status to completion…
local asset = tools.use("services", "status", g.id).asset
tools.use("appearance", "replaceWithContent", "chest_slot", asset)

The entity named chest_slot (and its subtree) is removed and the generated model takes its exact place. This is how a scene blocked out with stand-ins becomes the finished thing — you swap each placeholder for the real, generated (or library) asset rather than shipping the stand-in.

Inputs beyond text

Most operations take a prompt, but inputs are whatever the operation declares — numbers (width, height), booleans (seamless), a public asset URL, or a local asset (e.g. feed an image to mesh_gen's from_image operation). The list tool shows each operation's exact inputs, so read it rather than guessing.

The inputs every call takes

Alongside an operation's own inputs, the input table carries three fields the call itself takes. list publishes them on every operation, marked framework = true:

  • operation — which of the service's operations to run, by name. Each entry's default names the operation a call runs when you leave this out; where it is absent the service refuses the call and names its operations, because picking one for you would spend credits on a guess, and list marks operation required there. Read the entry rather than assuming: a service can offer several operations and still run one of them unasked.
  • asset_path: where the run writes what it generates, a full path carrying the operation's extension. The written file is imported exactly as one under the default name is, so .../open1.mp3 lands as the .soundClip .../open1.soundClip, and the completed row's asset names that asset. A path ending in the asset's own extension (.../open1.soundClip) names the asset directly: the file is written beside it under the operation's extension and imported into it. Leave asset_path out and the asset lands at /zero/source/generated/<kind>/<the prompt, slugified>, which is fine for a probe and unhelpful for content you are filing into a project. An operation that produces a set of files (pbr_gen's map set) authors its own folder, so it does not take asset_path and refuses one.
  • durable: true files the asset the run produces on canonical /source when the run completes, with play still running; false leaves it wherever the writes of that moment land. Left out, a run files its output where your writes landed when you started it (see "Where a run's output lands during play" above).
-- a non-default operation, and a name of your choosing for what comes back
tools.use("services", "generate", "audio_gen", {
    operation = "speech",
    text = "Welcome aboard.",
    asset_path = "/zero/source/game/audio/lines/open1.mp3",
})

generate also takes the operation as a third argument, which is the same thing said in the other place — generate("audio_gen", { text = "…" }, "speech"). Name it once; naming it twice differently is refused.

Credits

Generating consumes credits from your ZeroMind account — when a bot generates, from the human who owns the bot. Metering is server-side: a service names a provider offering and a logical endpoint; ZeroMind resolves the real upstream, injects the provider's secret, debits the price, and relays the result. The offering identity and the secret never appear in the service or your world. Read a run's cost up front in the list output, and check balance before a batch.

Descriptions are text, not content

The services toolbox also has describe, which writes text: the description an asset's type asks it to carry. tools.use("services", "describe", { mode = "find" }) lists the assets in your world that lack one, and how many calls filling them takes; a call costs 1 credit and covers many assets. { mode = "all" } fills them, and { assets = { ... } } fills the ones you name. What it writes is the file the type declares for it (a README, or a type's about.yaml), an ordinary file for you to edit. The describing-assets guide covers what a good description says.

The throughline

Reach for the ZeroMind library and asset.list to see what already exists before building anything by hand (search the library for each part you need, and read a candidate's files with zm show @<guid>:<path> before installing it); when the asset you need isn't there, generate it from a description through the services toolbox, then swap it into place. Authoring your own generator (becoming a provider others spend credits on) is the .service asset type; its README (types/service) is the worked reference.

  • documentation
  • guide