---
title: Client tools
description: Tools that your own app runs.
---

Some tools cannot run anywhere but in your app: reading the form the user is looking at, highlighting a row in their table, opening the page the agent is talking about. A **client** registers on a session, declares tools like these, and answers the calls the agent makes to them. A browser tab is a client; so is a backend worker.

The model sees a client's tools as `client__<name>__<tool>`, and the console's conversation shows their calls as coming from that client.

## Registering

```http
POST /v1/sessions/{session_id}/clients
```

```json
{
  "name": "web",
  "lifetime": { "type": "ttl", "data": { "seconds": 60 } },
  "tools": [
    {
      "name": "highlight_row",
      "description": "Highlight a row in the table the user is looking at.",
      "parameters": {
        "type": "object",
        "properties": { "row_id": { "type": "string" } },
        "required": ["row_id"]
      }
    }
  ]
}
```

| Field | |
| --- | --- |
| `name` | 1 to 32 characters: lowercase letters, digits, and `-`. If the session already spent the name, a counter is appended — the response says which name the client got. |
| `lifetime` | `{"type": "never"}` ends only when the client leaves. `{"type": "ttl", "data": {"seconds": N}}`, 5 to 3600, ends N seconds after the last ping, so a closed tab stops offering tools nothing can answer. |
| `tools` | Optional, up to 64. Each has a `name` (1 to 64 characters of `A`–`Z`, `a`–`z`, `0`–`9`, `_`, `-`), a `description` up to 4,096 characters, and a JSON Schema object for its `parameters`. |

The answer is the client's `id`, its assigned `name`, and when it expires. A session holds up to 32 live clients. The session's token reaches these routes, as does a key with **Sessions** › **Manage**.

## Staying registered

**Browser tab**

Give a tab a TTL and ping while it is open, with a new `Idempotency-Key`
each time. When the pings stop, the registration lapses.

```http
POST /v1/sessions/{session_id}/clients/{client_id}/ping
```

A ping that answers 404 means the registration lapsed: register again.

**Backend worker**

A worker that lives as long as the session can use `never` and skip the
pings. It leaves when it says so, which frees its name and fails any call
it still owed.

```http
DELETE /v1/sessions/{session_id}/clients/{client_id}
```

## Answering a call

Watch the stream for `client_tool_dispatched`: it names your `client_id`, the `call_id`, the tool as **you** declared it, and the arguments.

1. **Acknowledge it**

    An ack says the client is working on the call. A call must be acked
    within 60 seconds; once acked, it has five minutes to be answered.

    ```http
    POST /v1/sessions/{session_id}/clients/{client_id}/calls/{call_id}/ack
    ```

2. **Post the result**

    ```http
    POST /v1/sessions/{session_id}/clients/{client_id}/calls/{call_id}/result
    ```

    ```json
    { "text": "Row 42 is highlighted.", "is_error": false }
    ```

    `text` is up to 65,536 characters; `is_error` marks a failure the model
    should read.

If the client goes away, or misses a deadline, the call settles as `client_tool_failed` with its reason — `client_gone`, `ack_timeout`, `result_timeout`, or `run_gone` — and the model gets an error result, so the run carries on instead of stalling.

## Changing what you offer

The list a client declares replaces the one before it, so a tool you stop naming is a tool you stopped offering:

```http
PUT /v1/sessions/{session_id}/clients/{client_id}/tools
```

A page that navigates keeps its tools honest this way: replace the list on every screen change — before answering the call that navigated — and the model is only offered what is there.

:::note
Calls belong to the registration they were sent to, never to its name. A later client that gets a freed name never inherits outstanding work.
:::

The TypeScript SDK's [`@subako-ai/tools`](https://typescript-sdk-docs.subako.ai/) ships ready-made client tools that drive a page: fill a form, submit it, navigate.
