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

*The life of an async tool's job. The visitor asks; Vatio calls your backend with a job id and a callback URL, and your backend answers 202 Accepted. The agent tells the visitor it is on it. While the job runs, the agent sees it in every turn: your backend can post progress, and when the visitor asks how it is going the agent answers from it. Your backend posts the result to the callback URL; Vatio writes it into the conversation as the tool's answer and runs a turn that gives it to the visitor on their channel. If the visitor calls it off or the timeout passes, Vatio sends your cancel request.*

## 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](https://vatio.ai/docs/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.

```yaml
# tools/match_therapists.yml
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.id
request:
  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](https://vatio.ai/docs/tools#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.

```js
// Express
app.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](https://vatio.ai/docs/sdk/ruby#async-tools) 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:

```ruby
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.")
  end
end
```

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

```js
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.

```js
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:

```bash
vatio push
vatio chat "I've been having a lot of anxiety lately, I live in Ñuñoa"
vatio chat show
```

`vatio 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](https://vatio.ai/docs/improve) 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](https://vatio.ai/docs/tools): Every key of a tool's file, async included, and what your backend should answer.
- [The AI agent runtime](https://vatio.ai/docs/guides/ai-agent-runtime): What happens between a message and the reply, including the turns nobody wrote.
- [Best practices](https://vatio.ai/docs/guides/ai-agent-best-practices): Instructions, tools, knowledge, handoff and tests: what works for an agent real users talk to.
