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(orinit.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
- Registration. Writing
init.luau+tool.yamlinto a.tool/folder registers the tool with the tools registry. It becomes callable viatools.call("<toolbox>.<name>", args). - Schema validation.
tool.yamldeclares the parameter shape;tools.callvalidates inputs against it before dispatching toinit.luau. - Return envelope. Tools MUST return
ZmToolResult:{ exitCode = 0|n, data? = any, stdout? = string, message? = string }exitCode = 0means success; non-zero means failure, withmessagecarrying the human-readable reason. - Discovery. The tool surfaces in
tools.list(),search_tools { query: "..." }, andasset.inspect'sexposes:section (parsed fromtool.yaml). - Hot reload. Editing
init.luauortool.yamlre-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 ininit.luau. The toolbox'sshared.module/is for genuinely cross-tool helpers (ok/failconstructors, shared parsers / validators), not per-tool logic. Do NOT makeinit.luaua one-line forwarder onto a function defined inshared.module/. - Use
shared.module/in the parent toolbox forok/failresult constructors. Don't hand-roll the envelope per tool. Pull it in withlocal shared = require(".shared")from any tool that needs the helpers. - Annotate every public function with
--!desc/--!arg/--!return/--!exampledirectly above the function — the LSP and toolbox surfaces extract these. - Use
typed function M.name(arg: T, arg: T?): Rwhen the signature has clear types; fall back to plainfunction 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::tagssosearch_toolscan find them by domain (e.g.entity,scene,assets,physics).
Common pitfalls
- Missing
tool.yaml. Without the schema the tool isn't registered. Runasset.validateto catch this. - Wrong return envelope. Returning a raw value instead of
{ exitCode = 0, ... }will be flagged by callers expectingZmToolResult. - 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.callfails at runtime with a missing-module error.
Related types
.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.