> For the complete documentation index, see [llms.txt](https://docs.ojin.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.ojin.ai/apps/overview/tools.md).

# Tools & Events

Let your Human Agent call functions during a conversation, and react to the agent from your own page.

## Tool Calling

Tools (function calling) let the agent's LLM trigger actions while it talks: look something up, submit a form, update your UI, or notify a backend system. You define each tool's name, description, and JSON-schema parameters; the LLM decides when to call it and with what arguments.

{% hint style="warning" %}
**Tools work only on Ojin's default speech pipeline.** An agent configured with a speech-to-speech provider (OpenAI Realtime, ElevenLabs Agents, Hume) runs a different worker that never dispatches tool calls. You can still save tools on such an agent, but they will never fire.
{% endhint %}

Tools are configured **per agent**. You can manage them two ways:

* In the [Ojin dashboard](https://ojin.ai/dashboard) when [configuring an agent](/apps/overview/configure.md), the agent's tools editor.
* Programmatically via the [REST API](#managing-tools-via-the-rest-api).

Each tool has an **enabled** flag. A disabled tool keeps its configuration but is hidden from the LLM, so you can turn a tool off without losing its setup.

Limits worth knowing before you design a tool:

* The agent executes at most **3 tool calls per model response**. Further calls in the same response are dropped, and the model is told to answer with speech instead.
* Back-to-back identical calls (same tool, same arguments) inside one turn are collapsed to a single delivery. A tool meant to be called repeatedly with the same arguments should vary an argument.
* `end_call` is reserved for the built-in hang-up tool. Do not use it as a custom tool name.
* Only `properties` and `required` are read from your `parameters` object. Every other top-level JSON Schema keyword is accepted by the API and then dropped, including `type` (even `type: object`), `additionalProperties`, `$defs`, `oneOf`, `anyOf`, `allOf` and a top-level `description`. Express constraints inside each property rather than at the top level.

## Delivery Modes

Every tool declares how its calls are delivered. There is no agent-wide default. Each tool picks its own mode.

| Mode        | Where the call goes                                     | Your responsibility                        |
| ----------- | ------------------------------------------------------- | ------------------------------------------ |
| **Webhook** | Ojin POSTs the call to a URL you configure, server-side | Stand up the endpoint; no browser code     |
| **Client**  | The widget emits a DOM event in the user's browser      | Listen for the event on the embedding page |

Use **webhook** delivery for backend actions (write to a database, call an API, send a notification). Use **client** delivery for things that happen in the user's browser (navigate the page, open a modal, update on-page state).

## Webhook Delivery

When a webhook tool fires, Ojin sends an HTTP request to the tool's configured URL:

* Method `POST`, header `Content-Type: application/json`.
* A 30-second timeout.
* Redirects are not followed. Point the tool at the final URL: a `301` or `302` reaches the model as an error.
* The URL must be `http` or `https` and must resolve to a public address. Loopback and private addresses (`localhost`, `127.0.0.1`, `10.x`, `192.168.x`, `169.254.x`, `::1`) are rejected, and a tool with a rejected URL is dropped for that session. You cannot point a webhook at your laptop; use a tunnel with a public hostname.

The request body carries the call under a `tool_call_message` object. The fields you'll use:

```json
{
  "tool_call_message": {
    "name": "get_order_status",
    "parameters": "{\"order_id\":\"A123\"}",
    "tool_call_id": "call_abc",
    "response_required": true,
    "tool_type": "function"
  }
}
```

| Field               | Meaning                                                        |
| ------------------- | -------------------------------------------------------------- |
| `name`              | The tool that was called                                       |
| `parameters`        | The call arguments, as a **JSON string** (parse it before use) |
| `tool_call_id`      | Unique id for this call                                        |
| `response_required` | `true` when the agent is waiting for your response (see below) |

The body also includes additional session metadata fields alongside `tool_call_message`; you can ignore them for most integrations.

{% hint style="warning" %}
The webhook request is **not signed**. There is no secret header. Treat the webhook URL itself as a secret, serve it over HTTPS, and validate the payload before acting on it.
{% endhint %}

### Waiting for a response

Each webhook tool has a **wait for response** setting:

* **On** (`response_required: true`), the agent waits for your endpoint, then feeds the result back into the conversation so the LLM can use it. The result is always added to the conversation, but the agent only speaks about it in the same turn if it had not already started speaking. Otherwise the model uses it from the next turn onward. Return HTTP `2xx` with a JSON body:

  ```json
  { "result": "Your order shipped on Tuesday." }
  ```

  If you omit the `result` key, the entire JSON body is used as the result. An empty body (or `204`) is treated as an empty result. A non-`2xx` status, a timeout, or an unparseable body surfaces an error result to the LLM instead.
* **Off** (`response_required: false`), fire-and-forget. Ojin sends the request and does not wait; the LLM immediately continues with a neutral acknowledgement. Anything your endpoint returns is ignored.

## Client Delivery

When a client tool fires, the widget dispatches a DOM `CustomEvent` named `ojinToolCall`. The event fires on **`window`** (not on the `<ojin-agent>` element), so add the listener to `window`:

```js
window.addEventListener("ojinToolCall", (event) => {
  const { function_name, tool_call_id, arguments: args } = event.detail;
  // Run the browser-side action for `function_name` using `args`.
  // `args` is already an object, no parsing needed.
});
```

The `arguments` field is renamed to `args` above because `arguments` is a reserved word inside regular functions.

`event.detail` carries:

| Field           | Type     | Meaning                            |
| --------------- | -------- | ---------------------------------- |
| `function_name` | `string` | The tool that was called           |
| `tool_call_id`  | `string` | Unique id for this call            |
| `arguments`     | `object` | The call arguments, already parsed |

{% hint style="info" %}
Client delivery is **one-way**: the agent does not wait for your page, and there is no tool-result channel back from the browser. If the LLM needs to use a result, use **webhook** delivery with *wait for response* turned on. To feed information back for later turns, the widget exposes `appendSystemContext()`, which works on the default speech pipeline.
{% endhint %}

## Managing Tools via the REST API

Agent endpoints accept your API key, so you can manage tools programmatically. Authenticate with the `X-API-Key` header (see [Get your API key](/getting-started/authentication.md)).

Tools live on the agent itself. Set them with the agent update endpoint, `PATCH /v1/agents/{agent_id}`, using the `tools` array:

```bash
curl -X PATCH https://api.ojin.ai/v1/agents/$AGENT_ID \
  -H "X-API-Key: $OJIN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "tools": [
      {
        "enabled": true,
        "function": {
          "name": "open_pricing",
          "description": "Open the pricing page for the user.",
          "parameters": {
            "type": "object",
            "properties": { "plan": { "type": "string" } }
          }
        },
        "delivery": { "mode": "client" }
      },
      {
        "enabled": true,
        "function": {
          "name": "create_ticket",
          "description": "File a support ticket.",
          "parameters": {
            "type": "object",
            "properties": { "summary": { "type": "string" } }
          }
        },
        "delivery": {
          "mode": "webhook",
          "url": "https://example.com/hooks/ticket",
          "wait_for_response": true
        }
      }
    ]
  }'
```

{% hint style="warning" %}
The `tools` array is **full-replace**. Tools are matched by `function.name`: a name already on the agent is updated in place, a new name is created, and **any existing tool whose name is not in the array is deleted**. To change one tool, send the complete set. To leave tools untouched, omit `tools` from the request entirely.
{% endhint %}

To read an agent's current tools (each returned with its server-assigned `tool_id`), use `GET /v1/agents/{agent_id}`. For the full schema, see the [REST API](/getting-started/api.md) reference.

The API enforces these limits on each tool:

| Field                  | Constraint                                              |
| ---------------------- | ------------------------------------------------------- |
| `function.name`        | Max 64 characters, `^[a-zA-Z0-9_-]+$`, unique per agent |
| `function.description` | Max 1024 characters                                     |
| `delivery.url`         | Max 2048 characters, must be a valid URL                |

## Widget Events

Beyond `ojinToolCall`, the `<ojin-agent>` widget emits DOM events you can listen for to build custom UI, transcripts, or telemetry around the conversation.

All of them are observable on `window`, but they get there two different ways. `ojinToolCall`, `LatencyReport`, `ConnectionTimingReport` and `OjinSessionOutcome` are dispatched on `window` directly. The rest are dispatched on the `<ojin-agent>` element and bubble up to `window`.

### Conversation events

| Event                          | `detail`                                        | Fires when                                                                                                          |
| ------------------------------ | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `ojinToolCall`                 | `{ function_name, tool_call_id, arguments }`    | A client-delivered tool is called (see above)                                                                       |
| `botIsSpeaking`                | `{ isSpeaking: boolean }`                       | The agent starts or stops speaking                                                                                  |
| `ReceivedTranscriptTurn`       | `{ owner: "user" \| "bot", text, final: true }` | One finished conversational turn. This is the event to use for a transcript: one clean, de-duplicated line per turn |
| `ReceivedTranscriptUser`       | `{ text, timestamp, user, final }`              | A user speech transcript arrives (`final: false` for interim)                                                       |
| `ReceivedTranscriptBot`        | `{ text }`                                      | A chunk of the agent's response text arrives. Raw deltas, not a finished line                                       |
| `ReceivedTranscriptBotPartial` | `{ owner: "bot", text, final: false }`          | The cumulative text produced by the speech service for the agent's current turn updates                             |
| `BotTtsStopped`                | *no payload (`detail` is `null`)*               | The agent's current spoken turn ends or is interrupted                                                              |
| `UserStartedSpeaking`          | *no payload (`detail` is `null`)*               | The user starts speaking                                                                                            |
| `UserStoppedSpeaking`          | *no payload (`detail` is `null`)*               | The user stops speaking                                                                                             |
| `ConnectionTimeout`            | `{ duration_s, bot_state, connection_state }`   | Connection setup exceeds its timeout; the widget resets and shows an error                                          |
| `UpdateBotState`               | `{ botState }`                                  | The agent's lifecycle state changes                                                                                 |
| `UpdateConnectionState`        | `{ connectionState }`                           | The transport connection state changes                                                                              |
| `UpdateQueuing`                | `{ queuing: boolean }`                          | The session enters or leaves the waiting queue                                                                      |

```js
window.addEventListener("ReceivedTranscriptUser", (event) => {
  const { text, final } = event.detail;
  if (final) console.log("User said:", text);
});
```

### Advanced / telemetry events

These carry fine-grained audio and timing signals, useful for meters, latency dashboards, or debugging. They are more granular than the conversation events above and may change. Don't depend on them for core logic.

| Event                    | `detail`                                                                                                | Fires when                                             |
| ------------------------ | ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
| `userSpeakingChanged`    | `{ isSpeaking: boolean }`                                                                               | Browser voice-activity detection toggles               |
| `userVolume`             | `{ volume: number }`                                                                                    | The user's microphone level is sampled                 |
| `botVolume`              | `{ volume: number }`                                                                                    | The agent's playback level is sampled                  |
| `LatencyReport`          | per-utterance latency breakdown                                                                         | The agent reports timing for a turn                    |
| `ConnectionTimingReport` | connection timing breakdown                                                                             | The session finishes connecting                        |
| `OjinSessionOutcome`     | `{ outcome, phase, faultDomain, sessionId, agentId, instanceId, httpStatus, elapsedMs, widgetVersion }` | A connection attempt or a live session ends. See below |

#### `OjinSessionOutcome`

Fires once when a connection attempt finishes and again when a live session ends, so you can measure how often visitors reach a conversation and why they don't. `phase` is `connect` or `session`; `outcome` is a value such as `success`, `mic_permission_denied`, `agent_unavailable`, `transport_lost` or `ended_by_visitor`. `faultDomain` coarsely attributes it, `client_env`, `ojin`, `backend`, or `unknown` when the outcome carries no attribution (every normal ending does).

`instanceId` identifies which `<ojin-agent>` element it came from, since the event fires on `window` and a page may host several. `sessionId` is present only once a session exists, a microphone refused before the session starts has none.

Treat the `outcome` vocabulary as open: new values are added as new failure modes are identified, so match on the ones you care about rather than assuming the list is closed.

## Next Steps

* [**Create & Configure**](/apps/overview/configure.md): set up the agent these tools belong to
* [**Widget Integration**](/apps/overview/widget-integration.md): embed the agent on your page
* [**REST API**](/getting-started/api.md): full schema for programmatic management


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.ojin.ai/apps/overview/tools.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
