Log inGet started

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 optional idFrom / statusFrom / done / fail / doneWhen / failWhen / progressFrom overrides (sensible defaults cover the common providers).
  • bind / bindFrom — name the value later steps reference; bindFrom extracts 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.
  • .module — reusable library code you require and call.
  • .tool — an agent-callable function that composes multiple engine steps.
  • .component — per-entity behaviour.
  • asset-type
  • reference