# Tools

Tools let the agent call your APIs. A tool is one HTTP request, described in
YAML. The filename is the tool key: `tools/check_stock.yml` becomes
`check_stock`.

Anything that needs branching, a second call, or a derived response is code,
and it belongs in the backend the tool already calls — give the agent one
endpoint that does the whole job.

Add each tool to `agent.tools`; creating its file alone does not enable it.

A tool runs inside the agent's turn and answers in seconds. Work that takes
minutes and changes your code goes to a Claude Code routine, which the agent
also gets as a tool: see [Routine tools](#routine-tools).

## Declarative HTTP tools

Set your backend URL as a workspace secret:

```bash
vatio secrets set ACME_API https://api.acme.com
```

Create `tools/check_stock.yml`. This example expects the API to return
`{"data": [{"name": "Picker", "stock": 3}]}`:

```yaml
description: Check product stock.
when_to_use: The visitor asks whether a product is available.
parameters:
  type: object
  properties:
    product: { type: string }
  required: [product]
access: public
request:
  method: GET
  base_url: "$env.ACME_API"
  path: /products
  requires: [params.product]
  query:
    name: "$params.product"
respond:
  data:
    products: "$.data"
  message:
    when_empty: No products matched that name.
    default: "Found {{count}} matching products."
```

Merge this into your existing `agent` block:

```yaml
agent:
  instructions: Use check_stock to answer questions about product availability.
  tools: [check_stock]
```

The request supports `GET`, `POST`, `PATCH`, and `DELETE`. Supply `method`,
`base_url`, and `path`; the base URL must include its scheme and host, and can
include a path of its own. `path` is appended to it: a `base_url` of
`https://<ref>.supabase.co/functions/v1` and a `path` of `/my-tools/check_stock`
call `https://<ref>.supabase.co/functions/v1/my-tools/check_stock`. A `path`
that already starts with the base URL's path is not prefixed twice.
`headers` and `query` are maps; `body` is a JSON map used for `POST` and `PATCH`.

### Placeholders

Values in `base_url`, `path`, `headers`, `query`, and `body` support:

| Placeholder | Source |
|---|---|
| `$params.<name>` | Tool arguments |
| `$env.<KEY>` | Workspace secrets |
| `$auth.subject`, `$auth.token`, `$auth.claims.<key>` | The tool's authenticated principal |
| `$contact.name`, `$contact.email`, `$contact.phone_number` | Contact profile |

A single placeholder keeps its value's type. A placeholder within a string
is interpolated. `||` selects the first non-empty alternative, such as
`"$params.name || $contact.name"`.

### Required values

Missing values are omitted from the request. List any value the endpoint needs
under `request.requires`, without the `$` prefix, to fail before sending an
incomplete request. A missing contact phone, email, or name produces
`missing_contact_phone`, `missing_contact_email`, or `missing_contact_name`;
other paths produce `missing_requirement`.

On WhatsApp, a tool can count on `$contact.phone_number`. It comes with the
message; a visitor who hides their number behind a WhatsApp username is sent a
message with a one-tap button to share it the first time a tool that requires
`contact.phone_number` runs, and that message is the agent's reply for the
turn. The number arrives with their next message, and the tool runs then. You
do not need `request_contact_info` for this.

Anywhere else, `$contact.phone_number` comes only from the visitor's
[sign-in](https://vatio.ai/docs/authentication/): the `phone_number` claim of your identity token,
or a Supabase user's phone. A number the visitor types in the chat is never
used, so nobody can pass for someone else by typing theirs. On
`missing_contact_phone` there, the agent is told not to ask for the number and
to say this cannot be done from that chat.

### Response mapping

`respond.data` maps output keys to JSON paths: `$` is the entire body, `$.data`
is its `data` field. `respond.message` can be a literal string or the
`when_empty` / `default` mapping shown above. `{{count}}` is
the length of the first array in the mapped data. Without an array, `when_empty`
applies when all mapped values are blank.

Omit `respond.message` to use the API's own `message` field. HTTP 2xx produces
`result: "ok"`; non-2xx produces `result: "error"`, so return the status that
says what happened rather than encoding failures inside a 200.

On a non-2xx, the agent gets the body's `message` and `error_key`, and every
other top-level field as it is, such as a `hint`. A body with no `message` but
a string `error` (`{ "error": "...", "hint": "..." }`) uses `error` as the
message. With neither, the message only gives the HTTP status. A body with no
`error_key` gets `unexpected_http_status`.

## Tool results

Every tool returns an object with `result` (`ok` or `error`) and a non-empty
`message`. Additional fields, such as `data` and `error_key`, are allowed.

```json
{ "result": "ok", "message": "This product is out of stock.", "data": { "stock": 0 } }
```

```json
{ "result": "error", "message": "Stock could not be checked.", "error_key": "backend_unavailable" }
```

Use `ok` when the operation completed, including an empty result or negative
answer. Use `error` when it could not complete. Invalid results become a
platform error. Tool results are visible to the model; never return credentials.

One reply can make up to 12 tool calls. After the twelfth, or after the same
tool fails three times with the same `message`, the agent is told to stop and
answer with what it has, saying what it could not do, and any further call in
that reply is refused. So make an error's `message` say what to change:
"`account` cannot be changed; delete the transaction and create it again" lets
the agent recover on its next call, where "Nothing to change" only gets it
retrying.

Validate with `vatio diff`, deploy with `vatio push`, and exercise the
tool through `vatio chat`. There is no direct tool-invoke command.

## How your backend is answering

Every call a declarative tool makes is measured where it leaves Vatio: the
HTTP status, how long your backend took, and how it ended — `ok`, an HTTP
error, a timeout (8 seconds), a connection that failed, or a 2xx that was not
JSON. A failing tool does not always show in the conversation, because the
agent apologizes and moves on, so check the inbox's **Tools** page:

- each tool's calls, error rate, timeouts, and p50/p95 latency over the last
  hour, day, 7 days or 30 days, per environment, including tools never called;
- the calls that failed, with what your backend answered and a link to the
  conversation.

Your coding agent reads the same page with `vatio tools`: markdown that
starts with how to work it, or `--json`. See
[Tool metrics](https://vatio.ai/docs/cli/improve#tool-metrics).

In the inbox, each of the agent's replies shows the calls it made, with their
arguments and what came back. What your backend answered is kept encrypted, cut to 4 KB, and deleted
after 30 days; the status, duration and outcome stay.

## Routine tools

A tool file can name a [Claude Code routine](https://vatio.ai/docs/integrations/claude-routines)
instead of a request. The agent calls it when someone it may serve asks for
work on your code, and Claude takes the conversation from there:

```yaml
# tools/encargos.yml
description: Pass a request to Claude, which reviews or changes the code of our apps in a separate session.
when_to_use: Someone asks to review or change something in our apps and you know what it is. If what to change or where is missing, ask first.
routine: encargos
```

- **`routine:`** names a routine from `vatio routines list`, in the same
  environment.
- **No `request:`, `access:` or `parameters:`.** The routine reads the
  conversation itself, and its numbers say who may use it.
- **Offered only where it may run:** on WhatsApp, in conversations with the
  routine's numbers, while the routine is on. Everyone else's conversations do
  not have it.
- **`when_to_use` is what the agent decides by.** Say when the request is
  complete enough to hand over, so the agent asks first when it is not.

## Built-in tools

| Tool | Purpose |
|---|---|
| `knowledge_lookup` | Search the knowledge bases listed in `knowledge` |
| `identify_contact` | Save a name supplied by the visitor |
| `request_contact_info` | Ask a WhatsApp visitor to share their phone number |
| `remember` | Save a lasting fact about the contact for later conversations |
| `recall` | Search summaries of earlier conversations with the contact |
| `handoff` | Hand the conversation to a person — see [Human in the loop](https://vatio.ai/docs/handoff) |

Enable `knowledge_lookup`, `identify_contact` and `request_contact_info`
through `agent.tools`, just like your own tools.
`handoff` is attached by declaring `agent.handoff`, so an agent cannot escalate
without a configured destination.
`request_contact_info` sends a message with a sharing button, and that message
ends the agent's turn; the number arrives only if the visitor shares it in a
later message, and the dependent operation can be retried then. A tool that requires `contact.phone_number` sends
the same request on its own (see [Required values](#required-values)), and the
two never send it twice in a row.

### Memory

Once a conversation with a known contact has been quiet for 30 minutes, Vatio
summarizes it in two or three lines and saves any lasting facts it revealed as
notes, for every agent. Supervisors read both in the inbox, on the contact.

Enable `remember`, `recall` or both, the same way, to give the agent that
memory too. With either one on:

- Up to 20 notes about the contact are in the agent's prompt on every
  conversation, so it knows them without calling anything.
- `remember` saves a note the moment the visitor says something worth keeping.
- `recall` searches the summaries and notes, and returns the few that match --
  never a whole transcript.

Memory belongs to the contact, so every agent in the workspace that has it on
shares it, and preview and live contacts never see each other's.
If your workspace already defines a tool named `remember` or `recall`, yours
keeps the name and the platform tool is not attached.

A tool that reads a signed-in visitor's own records needs
[`access: private`](https://vatio.ai/docs/authentication/tools) and an `auth:` block.
