# Tools

A tool is a function the model can decide to call, exactly like the built-in `web_search` or
`workspace_read_file`. You describe it with a name, a description and a JSON Schema for its input; the model
decides when to call it and with what arguments; your handler runs in the sandbox and whatever it returns goes
back to the model.

## Register a tool

> Written for these docs. Official plugins register tools the same way — for example `briefing_weather` in
> `plugins/morning-briefing/src/weather.ts`. The shape below is exactly what the runtime and the publish check
> enforce.

```ts
globalThis.manifest = {
  egress: ["api.open-meteo.com"],
};

globalThis.tools.get_forecast = {
  description: "Get tomorrow's temperature range for a latitude/longitude. Use when the user asks about weather.",
  parameters: {
    type: "object",
    properties: {
      latitude: { type: "number", description: "Decimal degrees, north positive" },
      longitude: { type: "number", description: "Decimal degrees, east positive" },
    },
    required: ["latitude", "longitude"],
  },
  handler: async (input: { latitude: number; longitude: number }) => {
    const response = await host.fetch({
      url:
        "https://api.open-meteo.com/v1/forecast" +
        `?latitude=${input.latitude}&longitude=${input.longitude}` +
        "&daily=temperature_2m_max,temperature_2m_min&forecast_days=2",
    });
    if (response.status !== 200) throw new Error(`forecast service returned ${response.status}`);
    const daily = JSON.parse(response.body).daily;
    return { minC: daily.temperature_2m_min[1], maxC: daily.temperature_2m_max[1] };
  },
};
```

| Field | Rules |
| --- | --- |
| name (the key) | `^[a-z][a-z0-9_]{0,63}$` — lowercase, digits, underscores, starts with a letter |
| `description` | **required**, non-empty. This is what the model reads to decide when to call you: say what it does *and when to use it*. |
| `parameters` | optional JSON Schema for the input. Defaults to `{ "type": "object", "properties": {} }` (no arguments). Must be plain JSON. |
| `handler` | **required**, a function `(input, context) => result` (sync or async). |
| `alwaysListed` | optional boolean. `true` keeps the tool in the agent's tool list when an agent has more tools than its model takes and your other tools move behind `search_tools`. At most 3 per plugin; a 4th fails the publish. |
| `deferred` | optional boolean. `true` keeps the tool behind `search_tools` on every agent, however few tools the agent has. Cannot be combined with `alwaysListed`; a tool that sets both fails the publish. |

A bare function (`globalThis.tools.x = async () => …`) or a registration without a description fails the publish.

### Keep a tool out of the list

A plugin with many tools would add all of them to every request of every agent it is installed on. Mark a tool
`deferred: true` and the agent no longer sees it in its list; it finds the tool with `search_tools` and runs it with
`call_tool`. Write the tool's `description` with the words someone would search for, because that text is what
`search_tools` matches on. The owner can still pin the tool on the agent's **Access** tab, and a pin overrides
`deferred`. You cannot set both `deferred` and `alwaysListed` on one tool.

## What your handler receives and returns

- `input` is the model's arguments object, parsed from JSON. The platform doesn't validate it against your schema
  — check what you depend on.
- `context` is `{ callId: string; initiator: TurnInitiator }`, frozen. `initiator` says what started the turn, set by
  the platform and never by the model: `kind` is `"user"`, `"channelSender"`, `"schedule"`, `"event"`, `"agent"` or
  `"platform"`, and `isVerifiedPerson` is true when every input came from a signed-in user or a verified contact. It
  does not see text the model read through tools or earlier turns, so it is not proof that an action is free of
  planted instructions. Use it to refuse actions that a schedule, an event or another agent must not trigger on its
  own.
- Return any JSON-serializable value. It is serialized and handed to the model as the tool's result. Returning
  `undefined` sends `null`.
- **Throwing is fine.** A thrown error, a timeout or a memory fault becomes a failed tool result the model can see
  and react to; the agent's turn carries on.

Keep results compact: everything you return lands in the model's context.

## How the model sees it

Your tool joins the agent's tool list next to the built-ins, named **`<plugin-slug>__<tool-name>`** — for example
`weather__get_forecast` for a plugin with slug `weather`. Two plugins can therefore both have a `search` tool.
(A tool whose final name equals a built-in tool's name is dropped with a warning.)

## Which agents get it

Installing the plugin makes the tool available to **every agent on the account**. On each agent's **Access** tab
the owner can switch **Tools** from "all" to a short list, deny your tool on one agent, or pin it. Nothing to do on
your side. A crew plugin can instead fix an agent's tool list itself with `tools` on
[`host.agents.create`/`update`](plugin-host-api.md#hostagents).

## What a tool can reach

A tool call runs inside a chat, on behalf of one agent, so it gets the richest context of any capability:

- `host.fetch`, `host.settings`, `host.crypto`, `host.log` — always.
- `host.session.*` — act on the calling chat: read the conversation, post a message, manage todos, schedule a
  timer, spawn a subagent, message a peer (each by permission — see [Permissions](plugin-permissions.md)).
- `host.workspace.*` — read and write the calling agent's files (and the shared workspace).
- `host.agents.*`, `host.cronJobs.*`, `host.models.list()` — your plugin's own crew and schedules.
- `host.plugins.*` — build, try and release the plugins the account owns, asking the user before anything leaves the account ([details](plugin-host-api.md#hostplugins)).
- `host.oauth.accounts()`, `host.license()` — the customer's connected sign-ins and your licence.
- `host.session.llm`, `host.llm`, `host.decide` — call a model.
- `host.media.*` — upload and transcode audio.

Written for these docs — a tool that saves a note into the agent's workspace:

```ts
globalThis.tools.save_note = {
  description: "Save a short note into the agent's workspace under notes/.",
  parameters: {
    type: "object",
    properties: { title: { type: "string" }, text: { type: "string" } },
    required: ["title", "text"],
  },
  handler: async (input: { title: string; text: string }) => {
    const path = `notes/${input.title.replace(/[^a-z0-9-]+/gi, "-").toLowerCase()}.md`;
    await host.workspace!.write({ path, content: input.text });
    return { saved: path };
  },
};
```

Calling `host.workspace.write` derives `host:workspace:write`, so the installer sees "can write the agent's
files" before installing.

## Limits

30 s wall clock and 128 MB per call (at least 60 s and 512 MB with `host:media:transcode`, and at least 120 s with `agentApi:plugins.write`). Each call is metered as
a plugin invocation.

## TypeScript

The downloadable SDK types declare the registry: `declare var tools: Record<string, ToolDefinition>`, with
`ToolDefinition` = `{ description: string; parameters: Record<string, unknown>; handler: (input: any, context:
ToolCallContext) => unknown | Promise<unknown>; alwaysListed?: boolean; deferred?: boolean }`. The types require `parameters`; the
runtime defaults it to no arguments, so a tool with none still writes `parameters: { type: "object", properties: {} }`
when you type-check.
