#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.
#Declarative HTTP tools
Set your backend URL as a workspace secret:
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}]}:
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:
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: 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.
{ "result": "ok", "message": "This product is out of stock.", "data": { "stock": 0 } }
{ "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.
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 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:
# 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 fromvatio routines list, in the same environment.- No
request:,access:orparameters:. 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_useis 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 |
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), 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.
remembersaves a note the moment the visitor says something worth keeping.recallsearches 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 and an auth: block.