> ## Documentation Index
> Fetch the complete documentation index at: https://docs.support.onclarity.com/llms.txt
> Use this file to discover all available pages before exploring further.

# In-app tools

> Let your AI agent read from and act inside your website or mobile app, with the customer's consent for every change that matters.

In-app tools let your AI agent use your own website or mobile app during a chat. Your website or app declares what the agent may do, such as listing the customer's devices, opening a page or removing a device, and supplies the code that does it. The agent calls a tool, your code runs in the customer's browser or app, and the result goes back to the agent.

The code runs inside the customer's signed-in session. It calls your own API as the customer, with the same permissions the customer already has.

## How it works

**1. Your app declares its tools**

Each tool has a name, a description the agent reads, a JSON Schema for its arguments, and a kind: **read**, **safe action** or **action**. You register the tools with the web widget or the mobile SDK, together with the function that runs each one.

**2. The agent is offered them**

While the customer chats, the AI agent is offered the tools this customer's app declared, when the channel has **Use for chat** on. On a channel with it off, the agent never sees them.

**3. The agent calls a tool**

A **read** runs at once, whenever the agent needs the data. A **safe action** runs at once too, but only when the customer asks for it. An **action** shows the customer a consent card first, and runs only after they tap **Allow**.

**4. The result goes back to the agent**

Your function's return value reaches the agent, which uses it in its next reply. The conversation keeps a record of every call, visible to the customer and to your team in the inbox.

## Turn it on for a channel

App tools are set per channel, in one place, for the chat and for voice calls. They are off by default: on every channel, a website's or an Android or iOS app's, **Use for chat** and **Use for voice** start off, and the agent gets no in-app tools until an admin turns **Use for chat** on. To turn them on:

1. In the cabinet, open the channel: **Integrations > Web** for a website, or **Integrations > Mobile** for an Android or iOS app. A Flutter app has an Android and an iOS channel: turn the switches on for both.
2. Under **In-app tools**, turn on **Use for chat**: it lets the AI agent that answers the chats of this channel use the tools your website or app registers. Once it is on, the [ready-made tools](#ready-made-tools) work too, all groups by default, and your developers turn groups off in code. To let the voice assistant use them during a call too, turn on **Use for voice** (see [In a voice call](#in-a-voice-call)). **Use for voice** needs **Use for chat**, because the voice assistant works inside the chat: turning chat off turns voice off too.
3. Click **Save changes**.

Clarity checks the switch on its side: a chat on a channel with **Use for chat** off gets no app tools, whatever the page registers.

> [!NOTE]
>
> The tools your developers register work on every channel. The [ready-made tools](#ready-made-tools) need Android SDK 1.9.0 or ClaritySDK 1.6.0 for iOS in an app: the SDK version decides, not a cabinet setting.

## Choose the kind

The kind says what a tool changes for the customer, and so when the agent may call it:

| Kind | Changes for the customer | The agent calls it | Asks the customer | Examples |
| - | - | - | - | - |
| `read` | Nothing: only the agent sees the result | Whenever it needs the data to answer | No | List the devices, read the plan, read a help article without opening it |
| `safe_action` | What is on screen | Only when the customer asks | No | Open a page, scroll to a section, highlight a button |
| `action` | The customer's data, money or settings | When the customer asks, or when it cannot answer their question without it (where they are) | Yes, with a consent card, which is the question: the agent does not ask in words first | Sign out a device, cancel an order, change a setting, share the location |

For example, "Which devices are on my account?" makes the agent call `list_devices` and answer in the chat. "Show me my devices" opens the devices page with a `safe_action`. "Sign out the TV" puts a consent card in front of the customer before `remove_device` runs.

## Declare your tools

| Field | Required | Description |
| - | - | - |
| `name` | Yes | Lowercase letters, digits and `_`, starting with a letter. At most 64 characters. Unique within your app. |
| `description` | Yes | What the tool does and when to use it, written for the agent. At most 1000 characters. |
| `kind` | No | `read`, `safe_action` or `action`. See [Choose the kind](#choose-the-kind). Default `action`. |
| `rememberResult` | No | For an `action` with no arguments whose answer does not change during a visit (the customer's location, their device). Once the customer allowed it and it succeeded, the answer stays in that browser tab and the tool is offered as a `read` that returns it, so the agent gets it again without a second card. It never leaves the customer's browser. The answer belongs to that customer, that conversation and that tool: it is forgotten when `anecdoteLiveChatIdentify` signs a customer in or out, when the page calls `anecdoteLiveChatRemove`, when a new conversation starts, and when the tool is unregistered, turned off or replaced by a tool with another description or schema. |
| `collapseCallOnAllow` | No | For an `action`, during a voice call that was a bubble: the call goes back to the bubble once the customer allows the card, so your page is in view while the steps run. See [In a voice call](#in-a-voice-call). On any other kind, `anecdoteLiveChatRegisterTool` returns `false` and the tool is not registered. |
| Arguments schema | No | A JSON Schema of type `object` for the tool's arguments. Default: no arguments. |
| Result schema | No | A JSON Schema the result must match. A result that does not match reaches the agent as an error. |
| Timeout | No | Seconds your code has to answer, from 1 to 90. Default 20 for a read and 30 for a safe action or an action. For an action it starts when the customer taps **Allow**. |
| Function | Yes | The code that runs the tool. It gets the arguments as an object and returns JSON. |

Register the tools before the customer opens the chat, for example at startup. Registering the same name again replaces the tool in its place.

The agent is offered your tools in the order you register them, and it reaches for the ones listed first: register the tools it should use most, first.

If a customer has the chat open in more than one place, such as two browsers or your website and your app, the agent uses the tools of the chat they are writing in, and its calls run there.

The web widget adds `window.anecdoteLiveChatRegisterTool` once its script has loaded. Call it from the snippet's `onload`, after `anecdoteLiveChatInit`, or from `window.onAnecdoteSuccess`.

```js theme={null}
window.anecdoteLiveChatRegisterTool({
  name: 'list_devices',
  description: "Lists the devices signed in to the customer's account.",
  kind: 'read',
  execute: async () => {
    const response = await fetch('/api/devices', { credentials: 'include' });
    return { devices: await response.json() };
  },
});

window.anecdoteLiveChatRegisterTool({
  name: 'open_devices_page',
  description: 'Opens the devices page, when the customer asks to see their devices.',
  kind: 'safe_action',
  execute: async () => {
    router.push('/devices');
    return { opened: '/devices' };
  },
});

window.anecdoteLiveChatRegisterTool({
  name: 'remove_device',
  description: "Signs a device out of the customer's account.",
  kind: 'action',
  inputSchema: {
    type: 'object',
    properties: {
      device_id: { type: 'string', description: 'An id returned by list_devices' },
    },
    required: ['device_id'],
  },
  execute: async ({ device_id }) => {
    const response = await fetch(`/api/devices/${device_id}`, {
      method: 'DELETE',
      credentials: 'include',
    });
    return { removed: response.ok };
  },
});
```

`execute` may return a value or a promise. A thrown error reaches the agent as the tool's error message. `anecdoteLiveChatRegisterTool` returns `false` and logs the reason to the browser console when a declaration is invalid. `window.anecdoteLiveChatUnregisterTool(name)` removes a tool.

> [!TIP]
>
> On a single-page app, open pages through your router (as `router.push` above) rather than with a full page load. The chat then stays open, and the customer sees the agent's reply right away.

### Argument and result schemas

Schemas use a subset of JSON Schema that every model provider accepts:

* Keywords: `type`, `properties`, `required`, `items`, `enum` and `description`.
* Types: `object`, `array`, `string`, `number`, `integer` and `boolean`.
* At most 4 levels of nesting, 32 properties per object and 50 `enum` values.
* A `description` inside a schema is at most 300 characters, and an `enum` value at most 100.
* Property names follow the same rule as tool names.
* `confirmation_text` is reserved for the consent card and cannot be an argument.

The agent's arguments are checked against the schema before your code runs.

### Limits

| Limit | Value |
| - | - |
| Tools per chat | 32. The Android and iOS SDKs offer the first 32 you register and log a warning naming the rest. A longer list that reaches the platform is refused whole, and then none are offered. |
| Declaration size | 8 KB: description and schemas together |
| Result size | 16 KB of JSON |
| Error message | 300 characters, longer ones are cut |

A tool that breaks a rule is left out, and the others are still offered. The web widget logs the reason to the browser console.

## Ready-made tools

On a website, the web widget registers common tools for you, with no code: every group in the table below is on. It does so only once an admin has turned **Use for chat** on for the channel in the cabinet (see [Turn it on for a channel](#turn-it-on-for-a-channel)); both switches are off by default, and with both off the widget registers no built-in tool.

Which groups are on is set in your site's code, not in the cabinet, so your developers keep control of it. Add `inAppToolTemplates` to the `o` object of the [widget snippet](https://docs.support.onclarity.com/sdks/web-widget.md#configuration-options):

* **Nothing passed:** all seven groups are on.
* **`{ location: false, ... }`:** those groups are off, the rest stay on.
* **`{}` or `false`:** all built-in tools are off.

A group can also take an object of its options (see [Options](#options)), which keeps it on. Your own tools work whichever you choose.

```js theme={null}
var o = {
  "integrationKey": "YOUR_INTEGRATION_KEY",
  "organizationId": "YOUR_ORGANIZATION_ID",
  "mode": "live_chat",
  "widgetType": "live_chat",
  // All groups except location and forms.
  "inAppToolTemplates": {
    "location": false,
    "forms": false
  }
};
```

The Android SDK (since 1.9.0) and the iOS SDK (since 1.6.0) offer the same groups and tools for the screen of the app the customer is on, set in `ClarityConfig.inAppToolTemplates` by the same rules. There, `siteSearch` needs your website's address and `navigation` the app's screens before they offer anything. See [Ready-made tools](https://docs.support.onclarity.com/sdks/android.md#ready-made-tools) for Android and [Ready-made tools](https://docs.support.onclarity.com/sdks/ios.md#ready-made-tools) for iOS. The Flutter plugin (since 0.6.0) offers `siteSearch`, `navigation`, `diagnostics` and, on Android, `location`; see [Ready-made tools](https://docs.support.onclarity.com/sdks/flutter.md#ready-made-tools).

The kind of a tool decides whether the customer is asked first. A **read** changes nothing on screen and runs at once. A **safe action** (open a page, point at something, fill a field) runs at once, because the customer can see it and undo it. Important actions (kind **action**, such as pressing a button with `click_element`) run only after the customer allows them: they wait until the customer taps **Allow** on the card, or says yes in a call. `get_location` then also gets the browser's own permission prompt.

| Group | On by default | Tools | Kind | What they do |
| - | - | - | - | - |
| `page` | Yes | `read_page`, `show_on_page`, `get_page_text` | read, safe action, action | Read the page the customer is on in one call: its title, heading, selected text, the alerts, toasts and dialogs it shows, and the links of the menu and of the page; on request its schema.org data and the pages they opened in this tab before. Show them where something is: a dimmed frame closes in on a button, link, field or heading, the agent's note appears next to it, and it fades after a few seconds. Read the page's whole visible text, with the customer's consent (see below). |
| `siteSearch` | Yes | `find_on_site`, `read_link` | read | Search the site or list its pages from its sitemap, and read a page of the site without opening it. |
| `navigation` | Yes | `open_link` | safe action | Open a page of the site when the customer asks to go there, by reloading or through your site's router (see [Opening pages](#opening-pages)). |
| `forms` | Yes | `get_form`, `focus_form_field`, `fill_form_fields` | read, safe action | Help with a form one field at a time: the page scrolls to the field, keeps it highlighted while the agent asks for it, and the agent fills in what the customer says. See which fields the page marks as wrong and why. The form tools never submit it. |
| `click` | Yes | `click_element` | action | Press a button, link or other control of the page for the customer, after they allow it: "Save my profile", "press Continue". The page scrolls to the element and it is pressed the way a click would, so a form's submit button sends it through your page's own handlers and validation. It never types, and refuses what only the customer can do (see [Pressing for the customer](#pressing-for-the-customer)). |
| `location` | Yes | `get_location` | action | The customer's location from the browser, rounded to about 1 km, for questions like "where is the nearest store?". The agent reads the store addresses on your site first, then asks for the location. The browser also asks the customer for permission. |
| `diagnostics` | Yes | `get_diagnostics`, `reload_page` | read, action | In one read: the browser, language and time zone; what the browser lets the site use (microphone, camera, location, notifications) and, once the customer allowed the microphone or camera, which devices it sees; JavaScript errors since the chat loaded. And a page reload. |

To give the agent a tool of your own, register it with `window.anecdoteLiveChatRegisterTool` (see [Declare your tools](#declare-your-tools)). It works with or without the ready-made groups.

The default (every group on) makes 13 tools, so there is room for your own: a page offers at most 32. Your own tools always come first, so if the total is over 32 the ready-made ones that do not fit are left out; if your own tools alone are more than 32, the ones past 32 are left out too. Either way the widget names what it left out in the browser console.

### Options

`true` turns a group on with its defaults. An object turns it on with options; `{}` is the same as `true`:

| Option | Values | Default | What it does |
| - | - | - | - |
| `navigation.mode` | `"reload"`, `"history"`, `"handler"` | `"reload"` | How `open_link` opens a page. See [Opening pages](#opening-pages). |
| `siteSearch.searchUrl` | a path of your site with `{query}`, such as `"/search?q={query}"` | none | Your site's search. Without it, `find_on_site` looks for the search your pages publish (see below). |
| `<group>.collapseCallOnAllow` | `true`, `false` | `true` | For the group's actions (`click_element`, `get_location`, `reload_page`, `get_page_text`): a voice call that was a bubble goes back to it once the customer allows the card. `false` keeps the call open. See [In a voice call](#in-a-voice-call). |
| `page.pageText` | `"ask"`, `false` | `"ask"` | `get_page_text` reads the whole visible text of the page as the customer's browser shows it, including their own details on screen. With `"ask"` the customer is asked to allow every read. `false` leaves the tool out. |

```js theme={null}
"inAppToolTemplates": {
  "page": { "pageText": false },
  "siteSearch": true
}
```

#### Opening pages

`open_link` opens a page of your site in one of three ways, set with `navigation.mode`:

| Mode | How the page opens | The chat | During a voice call |
| - | - | - | - |
| `reload` (default) | A full page load. Works on any site. | Opens again on the same conversation after the load. | Refused: a reload would end the call. The agent tells the customer where to go instead. |
| `history` | The address changes in the browser history and your router draws the page, with no reload. Works with routers that follow the browser history (React Router, Vue Router, Angular). If the page does not change within 3 seconds, the address is put back (unless your router has already set its own) and the agent is told. | Stays open. | Works. |
| `handler` | Your app opens the page itself. | Stays open. | Works. |

While a page opens, the page dims slightly and a soft sheen passes over it, so the customer sees the agent is doing it.

In `handler` mode, register how your app navigates once its router exists. A returned promise is awaited, and an error reaches the agent:

```js theme={null}
// React Router
const navigate = useNavigate();
useEffect(() => {
  window.anecdoteLiveChatSetNavigator?.((path) => navigate(path));
  return () => window.anecdoteLiveChatSetNavigator?.(null);
}, [navigate]);

// Vue Router, after creating the router
window.anecdoteLiveChatSetNavigator((path) => router.push(path));
```

Or handle the `anecdote-live-chat:navigate` event, and call `done()` or `fail(reason)` when the page has opened or could not:

```js theme={null}
window.addEventListener('anecdote-live-chat:navigate', (event) => {
  event.preventDefault(); // "my app opens it"
  router.push(event.detail.path).then(event.detail.done, event.detail.fail);
});
```

If your app does neither, `open_link` tells the agent the site has no handler. Your app has 10 seconds to open the page.

#### Sites drawn by JavaScript

The page tools read the page as the browser shows it, so they work the same on a page your server renders and on one a JavaScript app (React, Vue, Angular) draws.

`read_link` and `find_on_site` read **other** pages by fetching them, and a fetch runs no scripts. On a single-page app that HTML is an empty shell, so:

* `read_link` tells the agent the page cannot be read without opening it.
* `find_on_site` without words reads your **sitemap** (`/sitemap.xml`, or the sitemaps `robots.txt` names), a static file even on a single-page app, so the agent learns which pages your site has.
* `find_on_site` with words uses your site's own search when it can read the results. It takes, in order, the `searchUrl` option, an [OpenSearch](https://developer.mozilla.org/en-US/docs/Web/OpenSearch) description the page links to, or a schema.org `SearchAction`. When there is none, or its results are drawn by JavaScript, it matches the words against the page addresses of your sitemap.

Publishing a sitemap with descriptive addresses (`/help/return-policy`) is what helps the agent most on a single-page app.

The tools stay within the limits of the browser and of your site:

* **Your site only.** `read_link`, `find_on_site` and `open_link` reach pages of the same origin as the page. Other sites are refused, and so is a page of yours that redirects to another site.
* **Public pages only.** `read_link` and `find_on_site` fetch a page without the customer's cookies, so the agent reads what a signed-out visitor sees and never the customer's own account pages.
* **What `read_page` returns without asking.** The page's title, its main heading, the text the customer selected, the text of its links and menu, the names of its forms, fields and buttons, and the error alerts it shows (`role="alert"`, `role="alertdialog"`, `aria-live="assertive"`). On an account page that text can hold the customer's name or an order number ("Hi, Jane", "Order #1234"). No tool reads what they typed or picked in a field.
* **The rest of the screen only with consent.** `get_page_text` reads the page's whole visible text, account details included, and asks the customer first every time: dialogs, status lines, toasts and banners are part of that text, not of `read_page`. It never reads what they typed or picked in a field, hidden elements or scripts. With `page.pageText: false` it is left out.
* **Every field seen and filled, never read.** `get_form` lists every field of the page's forms the customer sees: the standard ones (a name, an organization, an email, a phone number, an address) by their `autocomplete` type, and the others (a date of birth, a free-text reason, a choice) by a field name taken from their label, with their kind, whether they are required and filled, and the choices a select or radio group offers. The agent fills any of them with what the customer says: it types a text or a date, picks a select option or a radio by the words the customer sees, and ticks or unticks a checkbox. It never returns what is typed or which choice is picked, never picks a file for the customer, and leaves out what is inside a private part (`data-clarity-mask`, `data-clarity-exclude`).
* **Errors as your page shows them.** `get_form` reads the conventions forms already use: `aria-invalid` with `aria-errormessage` or `aria-describedby`, the browser's own validation (`:user-invalid`), and the error classes of common UI kits (Bootstrap, Material UI, Ant Design, Chakra). `read_page` reads `role="alert"` and `role="status"`, live regions, open dialogs and toasts. Marking your errors this way, which accessibility asks for anyway, is all it needs.
* **One field at a time.** The agent focuses a field (the page scrolls to it, it pulses twice and stays highlighted in your widget's primary colour), asks for its value and fills it, then moves to the next. The customer can also type into the highlighted field and tell the agent when done. Several values given at once are filled together. `fill_form_fields` takes one field flat (`field` and `value`), the shape a light voice model manages, or several in a `fields` list; a call with neither or both is refused. The highlight goes when the field is filled, when the customer turns to something else, or after two quiet minutes. In a voice call the agent spells an email or a phone number back before filling it.

A tool you register yourself takes precedence over a ready-made one of the same name, so you can replace any of them. Calling `anecdoteLiveChatInit` again applies its own `inAppToolTemplates` by the same rule: with nothing passed, all seven groups are on again.

### Pressing for the customer

`click_element` presses an element by the name the customer sees, the same names `read_page` lists and `show_on_page` points at. It is an action, so it runs only after the customer taps **Allow** on the card (its line names the element, such as Press “Save profile”), or says yes in a voice call. The page scrolls to the element, which is checked again right before the press, and then pressed with a click, the same event your page's code gets from a real click. The agent gets back `pressed: true` and the element's `label`, and the new `url` when the address changed (a router that draws the next page).

A press that loads another page (a plain link, or a form your page lets the browser send) is held until the result has reached the agent, then made, and the chat opens again on the new page, as with `open_link` in `reload` mode. During a voice call such a press is refused, because the load would end the call.

It presses nothing, and tells the agent to point at the element with `show_on_page` and ask the customer to press it themselves, when the element:

* is not on the page (the agent is then told the names it can use), is hidden, or is not a button, link or other control;
* is disabled (`disabled`, or `aria-disabled="true"`);
* is a field to type in (a text input, a text area, a select): the agent is told to use `focus_form_field` or `fill_form_fields` instead;
* picks a file (`input type="file"`);
* opens a new window (`target="_blank"` on a link or a form) or another app (`mailto:`, `tel:`), which browsers allow only from a real press;
* leads to another site, or sends its form to one;
* is inside a frame, which the page cannot reach;
* is inside an element marked private with `data-clarity-mask` or `data-clarity-exclude`;
* carries `data-clarity-no-click`, or sits inside an element that does.

Mark anything the agent must never press, such as "Delete account" or a payment button, with `data-clarity-no-click`:

```html theme={null}
<button data-clarity-no-click>Delete account</button>
```

To turn pressing off on the whole site, pass `"inAppToolTemplates": { "click": false }`.

## What the customer sees

* **Read:** while the tool runs, the chat shows a short "checking in the app" status.
* **Safe action:** it runs at once, and the change shows on the page.
* **Action:** the chat shows a consent card. Each step on it is one sentence the agent writes, such as "Sign out the living room TV". Under it are the tool's name and the exact arguments, which the agent cannot word. The customer taps **Allow** or **Decline**.

The card is the question: the agent does not ask in words first and wait for a typed yes. Its reply next to the card is one short sentence asking the customer to allow it, and it answers right away anything they asked that does not depend on the action. The card stays until the customer answers, even across a page reload. If the customer writes a new message instead, the card closes as **Ignored** and can no longer be allowed. When the customer allows the action, the card shows the outcome: done, failed, or not finished in time. The agent then continues the conversation with the result. When the action was needed to answer the customer's question, the agent answers that question: "Is there a store in my city?", the customer allows `get_location`, and the reply names the nearest store. A plain change, such as a new phone number, gets a short confirmation.

The card belongs to the conversation the agent asked in. When one request takes several actions, such as "add the Kids pack" and "add the Cinema pack", the customer gets one card that lists every step. One **Allow** runs the steps one after another, in the order the agent asked for them. If a step fails, the steps after it do not run. One **Decline** declines them all.

A safe action runs at once, without a card. If the agent has already put a card in front of the customer in the same reply, the safe action joins that card as a step, so the steps keep their order.

Write descriptions that say when the agent should use each tool. An action or a safe action changes something for the customer and returns nothing for the agent to read, so the agent calls it only when the customer asks. To let the agent look something up, give it a read tool. For example, add a read tool that reads a page without opening it, next to a safe action that opens the page.

Every call leaves a line in the conversation with the tool and its outcome. Your team sees the same record in the ticket.

### In a mobile app, the chat steps aside

In the iOS and Android SDKs the chat is a sheet over your app, so a tool that opens a screen would do it out of sight. Give such a tool a `reveal`: the chat steps aside with a loader while the tool runs, the customer sees the screen open, and the chat comes back. See **Show what the tool did** in the [iOS](https://docs.support.onclarity.com/sdks/ios.md) and [Android](https://docs.support.onclarity.com/sdks/android.md) guides, and [In-app tools](https://docs.support.onclarity.com/sdks/flutter.md#in-app-tools) in the Flutter guide.

<video autoPlay muted loop playsInline poster="/images/in-app-tools/in-app-reveal-android-strip.jpg" src="https://mintcdn.com/on-clarity/UOcSidP_DcB_NJy0/images/in-app-tools/in-app-reveal-android-strip.mp4?fit=max&auto=format&n=UOcSidP_DcB_NJy0&q=85&s=02180f04dd1c9ade8efec08f969940cc" />

An Android app opens the plan screen while the chat steps aside

### In a mobile app, confirm once on the device

An app can mark an action as needing its own security check, such as Face ID, a fingerprint or a PIN. When the customer taps **Allow** on a card with such an action, the app runs the check once for the whole card, before anything runs: a card of three steps asks once. If the check does not pass, nothing runs and the card asks again. See **Confirm once** in the [iOS](https://docs.support.onclarity.com/sdks/ios.md) and [Android](https://docs.support.onclarity.com/sdks/android.md) guides; the Flutter plugin has the same `requiresSecurityCheck` and `Clarity.setSecurityCheck`.

## In a voice call

When your widget offers voice calling, the voice assistant can use the same tools during a call. A customer can say "show me my devices" or "sign out the TV" and the assistant does it in your website for them, and says what it found or what changed. This also helps a customer who cannot easily see or use the screen.

1. In the cabinet, open the channel's settings, where voice calling is turned on.
2. Under **In-app tools**, turn on **Use for chat** and **Use for voice**. Voice is greyed out while chat is off.
3. Click **Save changes**.

The call can use the tools your website declared when the call started, and runs them only in the chat that placed the call. The kinds work as in a chat:

* **Read:** the assistant looks the data up and tells the customer the answer.
* **Safe action:** it runs at once, for example the devices page opens behind the call.
* **Action:** the call steps up and makes room for a short confirmation with **Allow** and **Decline**, with **Allow** focused, so a keyboard or screen reader user can answer with one key, and the assistant says once what the action will do and asks whether to go ahead. The confirmation lists each step on its own line. The customer can tap **Allow**, or just say yes. Only a clear yes said once the confirmation is up counts: a question, a hesitation, the request that led to the action or a yes said before never allows it. All the actions of one request share one confirmation. When they answer, the assistant tells them the outcome. A confirmation still waiting when the call ends is declined.

In a call the customer cannot read an answer on the screen, so when they ask to see something, the assistant shows it with your tools: it opens the page, sets the filter they asked for, or scrolls to it, and says in a sentence what is there. Give it the tools to do that. Declare a filter as one `safe_action` that chooses the value and applies it, rather than one tool to choose and another to press the apply button.

If the chat is open in several tabs, only the tab the call is in runs the action. The conversation keeps the same record of every tool call as in a chat, marked **In a voice call**. The records reach the chat's ticket when the call ends, with the call's transcript, each at the time it was made, so a call placed before the customer wrote anything is recorded too. When the customer answered out loud, the record says **Allowed by voice** or **Declined by voice**, with the words they said.

### In a mobile app, the call folds to a bubble

On a website the call collapses to a bubble over your page in the same way: when the assistant points at something with `show_on_page`, a call in the panel collapses, and the bubble moves off what is pointed at. See [Collapse the call to a bubble](https://docs.support.onclarity.com/sdks/web-widget.md#collapse-the-call-to-a-bubble).

On a website, a card opens a call that is a bubble, so the customer can answer it. Once they allow it, the call goes back to the bubble when every step on the card has `collapseCallOnAllow`, and your page is in view while the steps run. The ready-made actions have it unless you turn it off with `<group>.collapseCallOnAllow: false`. A call the customer opened themselves stays open: only the call the card opened goes back, and only for that card's Allow.

In the iOS and Android SDKs a call can collapse to a bubble over your app, so the customer keeps using the app while they talk. During a call in the sheet, a tool with a `reveal` collapses the call this way instead of stepping the chat aside, and the customer sees the screen open; a full-screen chat stays as it is, and the screen opens behind the call. An action that shows its result in your app can fold the call too: register it with `collapseCallOnAllow`, and once the customer allows the card, the call folds back to its bubble while the steps run. The bubble comes back as the call when the assistant needs another answer on screen. See **Voice calls** in the [iOS](https://docs.support.onclarity.com/sdks/ios.md#voice-calls) and [Android](https://docs.support.onclarity.com/sdks/android.md#voice-calls) guides.

<video autoPlay muted loop playsInline poster="/images/in-app-tools/voice-fold-android.jpg" src="https://mintcdn.com/on-clarity/Fb7mHdpitdSdMk5y/images/in-app-tools/voice-fold-android.mp4?fit=max&auto=format&n=Fb7mHdpitdSdMk5y&q=85&s=c1b6c1fdd048a9762951110574d0f924" />

An Android app: the customer allows a purchase, and the call folds back to its bubble

## Security

Declarations, arguments and results all come from the customer's device, so treat them like any other input from a browser or app:

* **Authorize in your API.** A tool can do only what your API lets this customer do. Check every change on your server, as you would for the customer clicking the button themselves.
* **Keep it least-privilege.** Expose what a support conversation needs. Make anything that changes the customer's data, money or settings an `action`, so the customer confirms it. Keep `safe_action` for what only changes the screen.
* **The agent treats app data as data.** Clarity marks every result and every error from your app as untrusted before the agent sees it, and the agent is told never to follow instructions inside it. A description is shown to the agent as your app's description of the tool, capped in length, and an app tool cannot replace one of the agent's own tools.
* **Nothing is unlocked by a result.** A result never grants the agent access to anything on the Clarity side, such as a tool your team configured or another customer's data.
* **The kind you register is the kind that runs.** Clarity refuses a call below the kind your page or app registered, so a declaration altered on its way cannot turn an `action` into a `read` and skip the customer's **Allow**.
* **One Allow covers the steps it showed.** **Allow** starts only the steps the card showed when the customer tapped it. A step that reached the card later asks again on a card of its own.
* **Only your page and the chat reach your tools.** The web widget takes tools and events only from the page that embeds it. The Android SDK (since 1.9.0) and the iOS SDK (since 1.6.0) do the same: only the chat page itself reaches your app, and a script inside a frame of the chat cannot call your tools.
* **Logs keep out the data.** Clarity logs the tool name and the call id, never the arguments, the result, or the chat's session.

## Troubleshooting

| Symptom | Check |
| - | - |
| The agent never calls the tool | **Use for chat** is on under **In-app tools** for the channel of this chat. The tool was registered before the chat opened. The browser console shows no rejection. |
| The agent says the tool is unavailable | The app registered the tool under a different name, or unregistered it. |
| The call ends in an error | Your function threw, returned something that is not JSON, returned more than 16 KB, or did not match the result schema. |
| The call times out | Your code did not answer within the timeout. Raise `timeoutSeconds` (90 at most) or answer sooner. |
| The voice assistant does not use the tools | **Use for voice** is on under **In-app tools** for the channel. The tools were registered before the call started. |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.