> ## Documentation Index
> Fetch the complete documentation index at: https://docs.burn0.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Tools

> The only surface your agent can touch

Tools are the whole of the agent's access to the world. It cannot read state, enumerate collections, or reach around them — if it should be able to do something, a tool exists for it, and if no tool exposes it, the agent cannot know it.

That makes the tool surface a design decision, not a formality. It is the API of the world you are simulating.

## Anatomy

```typescript tools/opportunities.ts theme={null}
export const opportunityTools = [
  defineTool({
    name: "reassign_opportunity",
    description:
      "Move an open opportunity to a different owner. The new owner must be an active user. Use this when a rep leaves and their pipeline needs a home.",
    inputSchema: schema(
      {
        opportunityId: S.string("Opportunity ID."),
        newOwnerId: S.string("User who will take the opportunity over."),
        actorUserId: S.string("User performing the action."),
      },
      ["opportunityId", "newOwnerId", "actorUserId"],
    ),
    run(state, input) {
      const user = actor(state, readString(input, "actorUserId"));
      const opportunity = requireOpportunity(state, readString(input, "opportunityId"));
      assertOpen(opportunity);

      const newOwnerId = readString(input, "newOwnerId");
      assertAssignable(state, newOwnerId);

      opportunity.ownerId = newOwnerId;
      audit(state, user.id, "reassign_opportunity", "opportunity", opportunity.id, "...");

      return { updated: opportunitySummary(state, opportunity) };
    },
  }),
];
```

| Field               | Notes                                                                                        |
| ------------------- | -------------------------------------------------------------------------------------------- |
| `name`              | What the agent calls. Snake case by convention.                                              |
| `description`       | Read by the model to decide *whether* to call it. Say when to use it, not just what it does. |
| `inputSchema`       | JSON Schema. Silo validates every call against it before `run` executes.                     |
| `run(state, input)` | Receives the live world. Mutate `state` directly; Silo clones whatever you return.           |

## Descriptions are prompt engineering

The model chooses tools by reading descriptions. A description that only restates the name wastes the one signal you have:

```typescript theme={null}
// Weak
description: "Archives an account."

// Useful
description:
  "Create a new account. Use this when a lead's company does not exist yet; check with search_accounts first so you do not create a duplicate."
```

Say what it is for, when to reach for it, and what must be true first. CRM's `get_current_date` description explicitly warns that every staleness judgement is relative to the simulated date, not the real calendar — because a model that assumes otherwise gets the task wrong.

## Validation happens before your code runs

Arguments are checked against `inputSchema` in `bindTools`. A call with a hallucinated property never reaches `run`:

```json theme={null}
{
  "output": {
    "code": "invalid_input",
    "message": "Invalid arguments for \"get_opportunity\": \"opportunityId\" is required; \"opportunityID\" is not a parameter; expected one of: opportunityId."
  },
  "isError": true
}
```

The message names both the mistake and the real parameter, which is what gives the model a chance to self-correct. Before this existed, unknown arguments were silently dropped and tools answered questions nobody had asked.

## Failing usefully

Throw `toolError` with a code the agent can act on. It is returned as a value, not raised — a rejected call is an observation, not the end of the rollout.

```typescript theme={null}
throw toolError("not_found", `Line "${lineId}" is not on ${invoice.id}.`);
```

| Code             | Use for                                        |
| ---------------- | ---------------------------------------------- |
| `invalid_input`  | Arguments that are structurally fine but wrong |
| `not_found`      | A referenced entity does not exist             |
| `invalid_state`  | The operation is not legal right now           |
| `not_allowed`    | The actor may not do this                      |
| `conflict`       | The change contradicts the current world       |
| `limit_exceeded` | A quota or bound was passed                    |

Any string is accepted, so a domain can add its own vocabulary — but prefer these when they fit.

Business rules belong in the tool, not in the verifier. CRM refuses to assign work to a deactivated user inside `assertAssignable`, which is why "reassign the departed rep's pipeline" is a real task rather than a formality.

## Registration is explicit

```bash theme={null}
npx @burn0/silo tool add archive_account --env demo \
  --description "Archive an account that is no longer active."
```

```
Created tools/archive-account.ts
Registered archiveAccount in tools/index.ts
Next: implement run() in tools/archive-account.ts (look for TODO).
```

The scaffold writes a stub and adds it to the barrel. The stub **throws on purpose**:

```typescript theme={null}
run(state, input) {
  // TODO: implement. Read or mutate `state` directly; Silo clones whatever
  // you return.
  throw toolError("invalid_state", "Tool \"archive_account\" is not implemented yet.");
}
```

An unimplemented tool that silently returned nothing would look like a working tool that found nothing.

A file appearing in `tools/` does **not** become callable until its export is in `tools/index.ts`. That asymmetry with `data/` is deliberate: a dataset landing on disk is inert, a tool landing on disk would widen what the agent can do.

```typescript tools/index.ts theme={null}
export const tools: CrmTool[] = [
  ...clockTools,
  ...userTools,
  ...accountTools,
  // ...
];
```

## How many tools?

More than feels necessary. The count follows from the rule that the agent sees nothing but tools: every collection it must enumerate needs a way to list, search and fetch, before a single mutation exists.

CRM needs 42 across nine collections. ERP has 185. A surface small enough to feel tidy is usually one that hides state the agent needs.

```bash theme={null}
npx @burn0/silo tool list --env demo
```

```
get_current_date   Today's date in the simulation. Every age, staleness and past-due...
list_users         List sales users. Use this to find who owns what, and to check...
get_user           Get one user with their quota, open pipeline and closed-won total...
list_accounts      List customer accounts, optionally filtered by owner, tier or industry.
search_accounts    Search accounts by name, industry or website.
get_account        Get one account with its contacts and every opportunity against it.
```

## Sharing a contract

Templates pin `defineTool` to their own state type once, so every tool file imports one thing:

```typescript tools/contract.ts theme={null}
export type CrmTool = SiloTool<State>;

export function defineTool(tool: CrmTool): CrmTool {
  return tool;
}
```

Alongside it live the schema builders (`S.string`, `S.enumeration`), input readers (`readString`, `readOptionalNumber`) and paging helpers every tool shares. Lookup and mutation helpers go in `tools/helpers.ts`, so a rule like "only an active user may act" is written once rather than re-implemented per tool.
