On this page

#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:

PlaceholderSource
$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_numberContact 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 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

ToolPurpose
knowledge_lookupSearch the knowledge bases listed in knowledge
identify_contactSave a name supplied by the visitor
request_contact_infoAsk a WhatsApp visitor to share their phone number
rememberSave a lasting fact about the contact for later conversations
recallSearch summaries of earlier conversations with the contact
handoffHand 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.
  • 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 and an auth: block.

Last updated:

Latest CLI version: 0.79.0 · Changelog · The whole contract as Markdown: /docs/docs.md · or vatio docs