Service (asset type)
A service generates content you don't have yet — a 3D mesh, an image, a PBR material, a sound effect, a humanoid animation, a navigable splat world — from the inputs each operation declares, landing…
A <name>.service/ is a declaration, not code. The asset type ships the whole
runtime — the metered invoke, the submit → poll → download → write pipeline, the
async generation handle, error handling — in its shared.module/. An instance only
declares its surface: which provider offering it meters, and the operations
it offers. There is no HTTP, polling, credit, or secret code in a service.
Using a service
Every service is used the same way, whatever it generates:
local mesh = asset.resolve("mesh_gen", "service")
mesh:operations() -- callable operations, each with its inputs + credit cost
mesh:cost() -- credits the next run costs (estimate)
mesh:balance() -- credits you have
local g = mesh:invoke({ prompt = "a wooden treasure chest" })
--> { id, task, asset_path } -- async: watch g.id, then use g.asset_path
:invoke returns immediately with a handle; the run finishes in the background and
writes g.asset_path. The services toolbox (status, jobs) is the agent-facing
way to track a run to completion — see the generating-assets-and-content guide.
A service with several operations selects via input.operation
(mesh:invoke({ operation = "from_image", image = "/zero/source/.../ref.png" }));
a single-operation service just takes its input.
Authoring one
-- <name>.service/init.luau
local Service = asset.containing(__FILE__).modules.shared
return Service.define({
name = "<name>",
description = "Generate <thing> from a text prompt.",
offering = "<provider>/<name>", -- the offering ZeroMind meters
output = { kind = "mesh", ext = "glb", dir = "/zero/source/generated/meshes" },
operations = {
generate = {
description = "What this makes, agent-facing.",
input = { prompt = { type = "string", required = true } },
cost = 5,
steps = { … }, -- the pipeline (see below)
result = { url = "$url" }, -- the framework downloads + writes it
},
},
})
Declaring the pipeline
steps is an ordered list of metered calls. Each step:
call— the offering's logical endpoint name.body/params/headers— templated."$name"injects an input or a prior step's bound value;"${name}"interpolates inside a string (a missing value drops the key);"$name?"is optional.{ fromFile = "$path", dataUri = "image/png" }sends a local asset;{ firstOf = { a, b } }uses the first present value.poll— when the call submits a job:{ endpoint, param }plus optionalidFrom/statusFrom/done/fail/doneWhen/failWhen/progressFromoverrides (sensible defaults cover the common providers).bind/bindFrom— name the value later steps reference;bindFromextracts it (a dotted path, a fallback list{ "a", "b" }, or an array-find{ find = "assets", where = { type = "x" }, get = "url" }). For a polled step the default bind is the submitted job id.
result is either { url = "$bound" } (download the URL and write it) or
{ bytes = "$bound" } (write bytes a step returned directly).
output sets where the asset lands (<dir>/<safe(prompt)>.<ext>); a caller's
asset_path overrides it. Read mesh_gen as the worked template:
asset.resolve("mesh_gen","service"):getDefinition().
Discovery
asset.list("service")— every registered service.asset.resolve("<name>","service"):info()— offering, output, operations.asset.resolve("<name>","service"):operations()— each operation's inputs + cost.
Related types
.module— reusable library code yourequireand call..tool— an agent-callable function that composes multiple engine steps..component— per-entity behaviour.