---
title: "The tool system"
description: "Tools are how the engine's capabilities become named, discoverable workflow operations. A tool composes a multi-step job (resolve an asset, spawn an entity, add a Model, add a collider, position it)…"
section: "Core"
slug: "core-tools"
canonical: "https://origozero.ai/docs/core-tools"
updated: "2026-09-04T05:37:45.686272753+00:00"
tags: ["documentation", "guide"]
---

# The tool system

## What a tool is for

A tool earns its place by doing one of three things:

- **Makes a one-off easy.** One call that would otherwise be several: resolve an asset, spawn an entity, add a Model, add a collider, position it → one `spawnModel(...)`.
- **Packs a whole workflow into one call.** "Convert every material in this scene to the ps1 shader", "bake every probe in the scene", "run this input sequence". Brute force is fine: tools run on the authoring path, not the gameplay hot path.
- **Hands the agent context an API wouldn't.** Spawn a model and get back its position, bounds, and neighbouring entities in one shot.

Its inputs must be settable by an agent **blind to how the engine works underneath**: words, intents, `nil` meaning "all", proxies already in hand. If using a tool requires understanding the system beneath it, it's the wrong tool.

A tool composes over a **capability**, the underlying engine API the tool body itself calls: the `entity` / `component` / `asset` globals, the `modules.api.engine.*` modules, and the rest of the engine surface. Gameplay logic lives in those capabilities (or a module built on them) and is called directly; a tool calls the same capability and wraps it in a schema an agent can discover and invoke. That split is what lets a tool stay freely renamable: gameplay code reaches for the capability, the agent reaches for the tool.

## Three surfaces, one registry

A tool's identity is `<toolbox>.<name>`, a `tool` inside a `toolbox`. The same live registry answers on three surfaces, so reach for whichever one you already hold. None is a subset of the others: each can browse, read a signature, and run.

**`zero`, the shell command, whenever you have `bash`.** Browsing, reading a signature and running are all the same command, and **`--help` answers at every level**, so you can walk from "what exists" to a correct call without leaving the shell:

```bash
zero                                    # every toolbox and what it covers
zero entityOps                          # the tools in one toolbox (same as `zero entityOps --help`)
zero entityOps fromAsset --help         # one tool: every argument, its type, whether it's required
zero entityOps fromAsset cube           # run it
zero --search spawn                     # keyword search across every toolbox (short: -k)
```

Arguments are **named** (`--name value`) or **positional** in signature order, and `--help` prints both runnable forms for the tool in front of you. A value that reads as JSON is passed as JSON (`12`, `true`, `[0,5,0]`, `{"internal":true}`), a bare run of numbers like `0,5,0` becomes an array, and anything else is a string. The dotted identity works too: `zero entityOps.fromAsset cube`.

**`use_tool`, the MCP tool call, when you're driving the engine from outside.** The executing sibling of `search_tools`: `search_tools` finds the tool, `use_tool` runs it, with no `execute()` snippet to hand-write. Pass the `toolbox`, the `tool`, and `args` — an **array** for positional arguments in signature order, or an **object** naming the parameters, the same names `--name value` uses in the shell. It returns the tool's **ZmToolResult envelope** (`{ ok, value | error, durationMs, tool }`):

```
search_tools { query = "spawn asset" }         -- find it: returns toolbox "entityOps", tool "fromAsset", its signature
use_tool { toolbox = "entityOps", tool = "fromAsset",
           args = ["cube", { name = "ragdoll", position = [0, 0, 0] }] }
-- → { ok = true, value = { id = "<entity id>", ... }, durationMs = 1.8, tool = "entityOps.fromAsset" }

use_tool { toolbox = "MaterialAuthor", tool = "fromColor",
           args = { color = [1, 0, 0], opts = { name = "Red" } } }   -- by name
```

An `args` object whose keys are **not** parameter names is the tool's single table parameter, so `capture.oneshot(opts)` takes `args = { pass = "depth" }` as that table. Mixing the two — one parameter name beside fields belonging inside a table — is refused with the parameter list, because either reading would hand the tool something it never asked for.

`describe_tool { toolbox, tool }` is the MCP spelling of `--help`: one tool's full schema.

**`tools.use(toolbox, tool, ...)`, from Luau, when you're authoring in code.** Inside an `execute()` call or an editor/authoring script, this runs a tool and **unwraps** the envelope: the raw value on success, a raised error on failure, so you write against the return value directly with no `.ok` / `.value` threading:

```lua
local r = tools.use("entityOps", "fromAsset", "cube", { name = "ragdoll" })
-- r.id is the entity id; a failing tool raises
```

Because `toolbox` and `tool` are asset identities, a `tools.use(...)` call written with **literal** names is statically resolved: the LSP checks the toolbox and tool exist, and the call is captured as a dependency edge so content that ships with a `tools.use` call carries its tool deps with it. Library toolboxes resolve by root: `tools.use("@somelib::toolbox", "name", ...)`.

## Where a tool may run

Tools write the VFS, create persistent assets, make HTTP calls, and queue heavy jobs. That is **authoring** work, not per-frame work, and it is the line: a tool belongs in an authoring context (an `execute()` call, the shell, an editor tool or panel), never in runtime code. **Runtime code (a component, a scene entrypoint, anything that ticks each frame or runs on load) reaches for the underlying capability, not a tool.** The engine enforces this statically:

- **A component reaches for a capability, not a tool.** Calling a tool from a component is an error. A tool can opt in with `declare { componentSafe = true }` at the top of its `init.luau`, but that switch is an explicit *"I acknowledge I'm using this against its intent"*: it's heavily frowned upon, there only for the rare case where you genuinely can't avoid duplicating the logic. The right move is almost always to extract the operation into a module the component calls directly.
- **A tool reaches for a module, not another tool.** Using `tools.use` inside a `.tool`/`.toolbox` source is an error; shared logic goes in a `require`'d module.

So when a guide shows a component spawning an entity, driving a light, or reading input, it uses the capability (`entity.spawn`, `modules.api.engine.lighting`, `zinput`); the tool form is for the interactive authoring you do from the shell and from `execute()`.

## Toolboxes, the namespace

Tools live inside a **toolbox**, which gives each tool its `<toolbox>.<name>` identity. A toolbox can hold a `shared.module` for helpers more than one of its tools reuse, reached with `require(".shared")` from inside the toolbox. Browse the built-in toolboxes with `zero` in the shell, `search_tools` with no arguments over MCP, or `asset.list("toolbox")` from Luau: each gives a grouped overview of every toolbox and what it is for.

The right tool often lives in a toolbox you would not guess (re-skinning a hierarchy is under `appearance`, screenshots under `capture`, scene structure under `scene`), which is why browsing is worth a call before searching for a name you have imagined.

## The result contract is system-enforced

A tool body returns a **raw value** (or calls `error(msg)` to fail). The asset type wraps every call into the `{ ok, value | error, durationMs, tool }` envelope and times it. The contract lives in the type, not in each tool, so every tool behaves the same way: returning a value is success, `error(...)` is failure, and the envelope is added for you. `use_tool` hands you that envelope; `tools.use` and `zero` unwrap it.

## Anatomy, and creating one

A tool is an ordinary asset folder: `<toolbox>.toolbox/<name>.tool/` with an `init.luau`. The **schema lives entirely in that Luau**: the typed function signature *is* the tool's signature, and the `--!desc` / `--!arg` / `--!return` / `--!example` annotations above it are its per-function docs; tags live in the sibling `.metadata`. The function whose name matches the `.tool` folder stem is the canonical entry. Edit the file like any source and it hot-reloads, so a toolbox added to the world after boot is listed and callable on every surface with no rebuild.

```luau
local M = {}

export type PlaceOpts = { position: { number }?, parent: EntityRef?, count: number? }

--!desc Spawn a primitive and return the entities it created.
--!arg kind Primitive shape to spawn.
--!arg opts Placement and repetition options.
--!return The spawned entity refs.
--!example "cube", { count = 4 }
typed function M.spawn(kind: ("cube" | "sphere" | "plane"), opts: PlaceOpts?): { EntityRef }
    -- ...
end

return M
```

**The `typed` keyword is what makes the signature real, and every exported tool function carries it.** It compiles to a dispatch wrapper that checks each argument at call time and raises a message naming the argument, the expected type, and what arrived. A plain `function` is checked by nothing — including one written `function M.spawn(kind: string)`, where the annotation informs the LSP and enforces nothing. Such a tool takes any input, runs its body on it, and returns a success envelope, which is why a wrong call to it looks like a working one.

**`--!closed` above the declaration says its records spell every key it reads.** A record type is the minimum shape an argument must have, so by default a table carrying keys beyond it is taken and the extra keys reach the body, where a tool that reads its options by name looks at none of them — a misspelled `rooots` or a key borrowed from a sibling tool is dropped and the call reports success. Written above the `typed function`, `--!closed` turns that into a refusal naming the key and the fields the record does spell, the answer a wrong field type already gets. It reaches every table argument of that declaration, through `T?`, `(T)`, the branches of a union and a named alias — and stops at a field's own type, which is a different declaration and answers for itself. Write it on a tool whose declared record is the whole set of keys it reads; leave it off one that hands an argument on to something that reads more — `pp.add` reads its options against the property schema of the effect being added, which a static record cannot spell, so it names that schema's own set in its refusal.

```luau
--!desc Assign a material to every renderable under the named roots.
--!closed
typed function M.swapMaterials(material: string | AssetRef<material>, opts: SwapMaterialsOpts?): AssignSummary
```

```
appearance.swapMaterials: argument #2 'opts': no field 'targets' — the fields
it reads are: first, match, roots
```

Type each argument as narrowly as the operation accepts: a fixed set of accepted words is a literal union (`("cube" | "sphere")`), never `string`; a structured argument is a named `export type`, never `{ [string]: any }`; an entity or asset is `EntityRef` / `AssetRef<kind>`, never `any`. An `op`-keyed dispatch table is several tools wearing one signature — split it. Reserve `any` for a value the tool genuinely does not constrain.

Those annotations are what `zero <toolbox> <tool> --help` and `describe_tool` print, so writing them well is what makes the tool usable by the next reader.

To make tools programmatically, `tools.createToolbox` and `tools.create` write the folder for you (`lsp.describe` gives their exact signatures). Whatever the route, the live surface is the reference:

```lua
tools.list()                 -- toolboxes and their tools
tools.get("entityOps.spawn") -- a tool's full schema
```

Reading a built-in tool's `init.luau` is the best way to see how one is built, and `asset.inspect("<toolbox>.<name>")` shows any tool's schema and docs. The asset system behind tools (identities, hot-reload, sharing) is in the assets guide.
