---
title: "Tool (asset type)"
description: "A tool is a single agent-callable function whose schema is its typed function signature. Tools are the workflow surface — composed Luau operations that bundle multiple engine APIs into one named…"
section: "Types"
slug: "types-tool"
canonical: "https://origozero.ai/docs/types-tool"
updated: "2026-09-05T16:41:47.505847867+00:00"
tags: ["asset-type", "reference"]
---

# Tool (asset type)

**Every exported tool function is declared `typed function`.** The
`typed` keyword is what makes the signature real: it compiles to a
runtime dispatch wrapper that checks each argument against its
declared type and raises on a mismatch. A plain `function` — even one
carrying Luau type annotations — is checked by nothing at call time,
so the tool accepts any input and reports success on garbage. See
"The typed signature is the schema" below.

## 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 typed signature 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, and carries its
    schema in `--!desc` / `--!arg` / `--!return` / `--!example`
    annotations above it. The typed signature IS the tool's signature.
    **Required.**
  - `README.md` — instance documentation. **Required.**
  - `.metadata` — tool-level tier and tags. **Required.**

## How to create one

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

```luau
-- The `tools` global authors a tool inside an existing toolbox:
tools.create({
    name        = "<name>",
    toolbox     = "<Toolbox>",
    description = "What this tool does, agent-facing.",
    code        = "return shared.ok(true)",   -- the function body
})

-- Or scaffold the folder 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 `tools.createToolbox({ name = "<Name>", description = "..." })`
first.

## How it operates

1. **Registration.** Writing `init.luau` into a `.tool/` folder
   registers the tool with the tools registry. It becomes callable
   as `tools.use("<toolbox>", "<name>", ...)` from Luau,
   `use_tool { toolbox, tool, args }` over MCP, and
   `zero <toolbox> <name> [args]` from the engine shell.
2. **Schema validation.** The `typed function` signature declares the
   parameter shape. Its runtime dispatch wrapper checks each argument
   before the body runs and raises a message naming the argument, the
   expected type, and what arrived.
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 the typed signature and the
   `--!` annotations).
5. **Hot reload.** Editing `init.luau` 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.
- **Declare every exported function `typed function M.name(arg: T,
  opt: U?): R`.** This is not conditional on the signature being
  "clear" — a loose argument is the case that needs the checked
  contract most. Give the argument the type it actually accepts
  (a union, a named table type, a literal enum) instead of dropping
  to an unchecked `function`.
- Keep tools focused. One tool, one named workflow. Split rather
  than grow.
- Tag tools in the sibling `.metadata` (`{ "tags": [...], "tier":
  "core|content|specialized" }`) so `search_tools` can find them by
  domain (e.g. `entity`, `scene`, `assets`, `physics`).

## The typed signature is the schema

The `typed` keyword compiles the function into a dispatch wrapper
that validates arguments at call time. Without it there is no check
at all, so the tool runs its body on whatever arrived and returns a
success envelope built from garbage. Type each argument as narrowly
as the operation actually accepts:

```luau
export type Mode = "edit" | "play"

export type SpawnOpts = {
    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: SpawnOpts?): { EntityRef }
```

- **A fixed set of accepted words is a literal union**, never
  `string`. `("cube" | "sphere")` rejects `"Cube"` at the call and
  lists the accepted values in the error; `kind: string` accepts
  `"banana"` and fails somewhere deeper, or not at all.
- **A structured argument is a named `export type`**, never
  `{ [string]: any }`. Naming it puts every field and its type on
  the tool's page and checks them on entry.
- **A named type is only as strong as its fields.** `opts: DrawOpts`
  where `DrawOpts = { value: any }` passes for a table and checks
  nothing inside it — the same hole, one level down. The argument
  checker walks nested fields, so type each one; a type name is not
  a place to put the work off.
- **A dispatch table keyed by an `op` string is several tools.**
  `{ op: string, [string]: any }` is one unchecked entry point
  wearing N signatures — split it so each operation carries its own
  checked arguments.
- **An entity or asset argument is `EntityRef` / `AssetRef<kind>`**,
  never `any` and never a bare name string.
- `any` is correct only for a value the tool genuinely does not
  constrain — a passthrough payload, arbitrary user data. It is not
  a placeholder for a type not yet written down.

## Common pitfalls

- **A plain `function` export.** The tool registers and dispatches,
  so nothing looks wrong — it just never rejects bad input, and
  every call reports success. Declare it `typed function`.
- **Annotations without `typed`.** `function M.f(x: string)` reads
  as typed and checks nothing at runtime; the annotation only
  informs the LSP. The keyword is what enforces it.
- **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 the call fails 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.
