#Long-running tools
A long-running tool is one whose work takes longer than a reply can wait: recommendations that write a text for each match, a report or an export, a quote that waits on a supplier, or a job that drives a browser for minutes. A Vatio tool has 8 seconds to answer. An async tool answers within those 8 seconds that it accepted the job, and your backend posts the result when it has it. Meanwhile the agent tells the person it is working on it, keeps talking with them, and gives them the result in the same conversation: in the web widget, in your iOS, Android or Flutter app, on WhatsApp or on Instagram.
#When a tool should be async
- It is often slower than a few seconds. A reply that waits on a slow tool is a person staring at a typing indicator, and at 8 seconds the call fails. Make it async rather than racing the limit.
- The work is a job, not a lookup. Generating text per result, crawling, running a model, filling in a form on someone else's site: anything you would put on a queue in your own app.
- Not when it can be made fast. Reading a row, checking stock or booking a slot should answer in the call. An async tool costs the person a second message to read.
Every other part of a tool stays the same: its parameters, access, headers and respond: mapping (Tools).
#1. Declare the tool
Add async: to the tool's file. async: true is enough; the full form sets how long the job may run and what Vatio sends to call it off.
description: Finds therapists who fit the patient's reason for consulting and area, with a short text on why each one fits.when_to_use: The patient told you what is going on and where they live.parameters: type: object properties: reason: { type: string, description: "In the patient's words" } area: { type: string } required: [reason, area]async: timeout: 10m cancel: method: DELETE path: /vatio/jobs/$job.idrequest: method: POST base_url: "$env.CLINIC_API" path: /vatio/match headers: Authorization: "Bearer $env.CLINIC_API_KEY" body: reason: area:respond: data: therapists: "$.data.therapists"vatio diff checks it and vatio push deploys it to preview. The agent's prompt says the tool runs in the background, so the agent can tell the person it will take a moment. The reference for every key is A tool that takes longer: async.
#2. Accept the job
Your endpoint gets the call as any tool does, plus the job's id and the URL to post its result to, in the headers X-Vatio-Job-Id and X-Vatio-Callback-Url. Put the work on your queue and answer 202 right away. The message you return reaches the agent, so it can say how long the job usually takes.
// Expressapp.post("/vatio/match", requireToolKey, async (req, res) => { const job = { id: req.get("X-Vatio-Job-Id"), callbackUrl: req.get("X-Vatio-Callback-Url"), }; await queue.add("match-therapists", { job, reason: req.body.reason, area: req.body.area }, { jobId: job.id }); res.status(202).json({ message: "Finding matches; this usually takes about two minutes." });});requireToolKey is the check your tools already have: it verifies the Authorization header the tool sends. The callback URL needs no key, yours or Vatio's: it is signed and names this one job. Treat it as a secret and keep it out of your logs.
If the answer is sometimes ready at once (a cache hit, say), answer 200 with the result instead, as a normal tool would. Then there is no job.
In Rails, the Ruby gem reads the job and answers 202 for you, and its run posts your worker's result, or reports the job failed if the worker raises:
class Vatio::MatchesController < ApplicationController include Vatio::Server::AsyncTool
def create MatchTherapistsJob.perform_later(vatio_job, reason: params[:reason], area: params[:area]) accept_vatio_job(message: "Finding matches; this usually takes about two minutes.") endend#3. Post progress and the result
Your worker posts to the callback URL. The final body is the one the endpoint would have answered if it had been fast: respond: maps it the same way. "result": "error" with a message ends the job as failed, and the agent explains it to the person.
async function matchTherapists({ job, reason, area }) { await callback(job.callbackUrl, { progress: "Reviewing 12 therapists in your area", notify: true }); const therapists = await findMatches(reason, area); // the slow part await callback(job.callbackUrl, { result: "ok", message: `${therapists.length} therapists found`, data: { therapists }, });}
// At least once: a repeated result is answered 200 again. 409 and 404 mean// the job is over (timed out or called off), so stop.async function callback(url, body) { for (let attempt = 1; attempt <= 5; attempt++) { const res = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }).catch(() => null); if (res && res.status < 500 && res.status !== 429) return res; await new Promise((resolve) => setTimeout(resolve, attempt * 2000)); }}Progress is optional. Without notify, it is kept for when the person asks how it is going; with "notify": true, the agent tells them right away. Use it for jobs that take minutes, not for every step.
#4. Let the person change their mind
While a job runs, the agent can call it off when the person no longer wants it. Vatio then sends the tool's cancel request. It also sends it when the job runs past its timeout. Stop the work if you can; either way, the result will not be delivered.
app.delete("/vatio/jobs/:id", requireToolKey, async (req, res) => { await queue.remove(req.params.id); res.json({ result: "ok", message: "Cancelled" });});#What the person sees
In the web widget, with a person who writes while the job runs:
Patient: I've been having a lot of anxiety lately, I live in Ñuñoa
Agent: I'm looking for therapists who fit what you're going through, in Ñuñoa. It takes a couple of minutes, and I'll write here as soon as I have them.
Patient: how's it going?
Agent: Still on it: I'm reviewing 12 therapists in your area.
Agent: I have two for you. Ana Rojas treats anxiety with cognitive behavioural therapy and sees patients in Ñuñoa on Tuesdays and Thursdays. Pedro Soto works with mindfulness and sees patients online or in Providencia.
Patient: does the second one do online sessions?
Agent: Yes, Pedro sees patients online as well as in Providencia.
The last answer comes from the result itself: once delivered, the result is in the conversation as the tool's answer, so later questions about it need no new call. Asked how it was going, the agent did not call the tool again, because every turn knows which jobs are running.
The result reaches the person wherever the conversation is:
| Where | How the result arrives |
|---|---|
| Web widget, iOS, Android and Flutter apps | As the agent's next message: live if the chat is open, in the history when it opens again |
| WhatsApp and Instagram | As a message, while the 24-hour window is open |
| WhatsApp, window closed | The whatsapp_template you name brings them back, and the agent gives the result when they answer |
| A person on your team took over | It waits, and the agent gives it on its next turn |
#Test it
Push to preview and talk to the agent from your terminal:
vatio pushvatio chat "I've been having a lot of anxiety lately, I live in Ñuñoa"vatio chat showvatio chat show prints the call, the job it started and, once your backend posts, the result and the reply that gave it. To try your callback by hand, copy X-Vatio-Callback-Url from your backend's request and post to it with curl. A test replay never waits for a callback: the agent is told the job started, as it was in the conversation being replayed.
#Frequently asked questions
#How long can a background job take?
Up to 8 hours, set with async.timeout (10 minutes if you leave it out). That is as long as a WhatsApp or Instagram conversation lasts without a message, so the result always lands in the conversation the person is still in. When it passes without a result, the job ends, your cancel request is sent and the agent tells the person it could not be done this time.
#What if the person writes while the job runs?
The agent answers them as usual. It knows the job is running, so it does not start it again, and it can tell them how far along it is from your last progress. If the result arrives while their message is waiting for an answer, that reply includes the result.
#Can a conversation have several jobs at once?
Yes, up to 5. The same tool with the same arguments is one job: asking again while it runs does not start a second one.
#What happens if my backend posts the result twice?
Nothing new: the repeat gets 200 with "duplicate": true, and the person hears the result once. A post after the job ended (it timed out or was called off) gets 409, so your retries can stop.
#Does the callback need an API key?
No. The URL carries a signed token that names one job and stops working a day after the job's timeout. Anyone who has it can post that job's result, so keep it out of logs.
#Can the agent use the result later in the conversation?
Yes. The result is written into the conversation as the tool's answer, so the agent reads it on every later turn, and safeguards treat amounts and links in it like those from any tool.
#Related guides
- Tools: Every key of a tool's file, async included, and what your backend should answer.
- The AI agent runtime: What happens between a message and the reply, including the turns nobody wrote.
- Best practices: Instructions, tools, knowledge, handoff and tests: what works for an agent real users talk to.