Log inGet started

Tool (asset type)

A tool is a single agent-callable function with a YAML-declared schema. Tools are the workflow surface — composed Luau operations that bundle multiple engine APIs into one named call, suitable for…

When to use one

  • You're composing several engine APIs into a workflow that's worth naming and discovering (tools.list, search_tools).
  • You want an agent to be able to call this function by name without reading source code — the YAML schema is the API contract.
  • The operation has clear success / failure modes that fit { exitCode, data?, stdout?, message? }.

If the function is just a Luau utility called from other code, use a .module. If it's stateful background work, use a .service. If it's pure GPU compute, use a compute .shader.

Where it lives

Tools nest under a .toolbox/ parent — toolboxes are the namespace.

  • Source: /zero/source/.../<Toolbox>.toolbox/<name>.tool/
  • Identity: <Toolbox>.<name> (dotted form — the toolbox name is part of the tool identity).
  • Folder shape:
    • init.luau (or init.lua) — exports the function. Required.
    • tool.yaml — schema (name, description, parameters, returns, tags). Required.
    • README.md — instance documentation. Required.

How to create one

Use the workflow tool that scaffolds the right .tool/ shape under an existing toolbox:

-- From the tools workflow module:
local tools = require("@builtin::modules.tools")
tools.create("<Toolbox>", "<name>", function() return ok() end)

-- Or scaffold manually:
local box = asset.resolve("<Toolbox>.toolbox")
asset.create("tool", "<name>", { into = box })

Always nest under an existing toolbox — if you don't have one yet, call asset.create("toolbox", "<Name>") first.

How it operates

  1. Registration. Writing init.luau + tool.yaml into a .tool/ folder registers the tool with the tools registry. It becomes callable via tools.call("<toolbox>.<name>", args).
  2. Schema validation. tool.yaml declares the parameter shape; tools.call validates inputs against it before dispatching to init.luau.
  3. Return envelope. Tools MUST return ZmToolResult:
    { exitCode = 0|n, data? = any, stdout? = string, message? = string }
    
    exitCode = 0 means success; non-zero means failure, with message carrying the human-readable reason.
  4. Discovery. The tool surfaces in tools.list(), search_tools { query: "..." }, and asset.inspect's exposes: section (parsed from tool.yaml).
  5. Hot reload. Editing init.luau or tool.yaml re-registers the tool. Subsequent calls see the new behaviour / schema.

Discovery

  • tools.list() — every registered tool (any toolbox).
  • search_tools { query: "..." } — MCP-side tool search.
  • asset.list("tool") — same set, asset-style listing.
  • asset.inspect("<toolbox>.<name>") — signature, parameters, return shape, this type README.
  • cat /zero/source/<Toolbox>.toolbox/<name>.tool — same summary.

Authoring conventions

  • Implementation lives in init.luau. The tool's full function body — argument validation, the call into engine APIs, the return value — goes in init.luau. The toolbox's shared.module/ is for genuinely cross-tool helpers (ok / fail constructors, shared parsers / validators), not per-tool logic. Do NOT make init.luau a one-line forwarder onto a function defined in shared.module/.
  • Use shared.module/ in the parent toolbox for ok / fail result constructors. Don't hand-roll the envelope per tool. Pull it in with local shared = require(".shared") from any tool that needs the helpers.
  • Annotate every public function with --!desc / --!arg / --!return / --!example directly above the function — the LSP and toolbox surfaces extract these.
  • Use typed function M.name(arg: T, arg: T?): R when the signature has clear types; fall back to plain function M.name(...) when args are variadic / loosely-typed.
  • Keep tools focused. One tool, one named workflow. Split rather than grow.
  • Tag tools in tool.yaml::tags so search_tools can find them by domain (e.g. entity, scene, assets, physics).

Common pitfalls

  • Missing tool.yaml. Without the schema the tool isn't registered. Run asset.validate to catch this.
  • Wrong return envelope. Returning a raw value instead of { exitCode = 0, ... } will be flagged by callers expecting ZmToolResult.
  • Tool identity. The fully-qualified identity is <toolbox>.<name>, NOT just <name>. Toolbox-less tools don't exist by design.
  • Missing dependency. Tools requiring another package / service must declare it; otherwise tools.call fails at runtime with a missing-module error.
  • .toolbox — the required parent namespace for tools.
  • .module — for Luau libraries with no agent-facing schema.
  • .service — for stateful background work, not callable request/response operations.
  • asset-type
  • reference