Custom tools technical deep dive
Custom tools in LIVOI
Section titled “Custom tools in LIVOI”Custom tools connect a LIVOI agent to any HTTP API. A tool describes which inputs the model may provide and, separately, how those inputs become a concrete HTTP request. The model only sees the input schema. URL, method, headers, API key, and fixed body content remain configuration the agent cannot change.
Tool call overview
Section titled “Tool call overview”When the agent is loaded, every tool definition is validated again. Each tool call then passes through validation, request building, the HTTP call, and evaluation. The result and a redacted request snapshot are stored separately in the chat history.
The diagram shows the standard path of a custom tool call. Tool search is only active when at least one of the agent’s tools uses defer_loading. Preview and test run without an agent or a model, but through the same request builder.
Summary
Section titled “Summary”- A custom tool consists of
request_config,input_schema, and optional runtime settings. - Inputs are declared as
path,query, orbodyparameters. - A
body_templatedefines the body structure;{"$livoi_input": "name"}marks where validated inputs are inserted. - Unknown arguments are rejected. The model cannot override fixed template values.
- Configuration errors are rejected on save with 422 and the exact field path, not at runtime.
- The preview builds the request without contacting the upstream API; the test request sends exactly one real request after explicit confirmation.
- Each tool call stores a redacted inspection on the
tool_callhistory part. The model never receives it. - With
defer_loading: true, tools stay out of the initial context and are found throughsearch_tools. - Upstream failures are classified and returned as terminal results, never retried automatically.
- The HTTP client for custom tools neither stores nor sends cookies.
Anatomy of a tool definition
Section titled “Anatomy of a tool definition”Custom tools always belong to exactly one agent and are stored as an agent tool. Tool names are unique per agent.
| Part | Purpose |
|---|---|
tool_name, tool_description | Name in lower_snake_case, at most 128 characters, not identical to built-in or internal tools. The description is the model’s main signal for when the tool fits. |
request_config | Target URL, HTTP method, authentication, additional headers, optional body_template, and timeout. |
input_schema | The parameters the model fills in, each with a type, a location, and optionally a default or choices. |
| Runtime settings | response_config for a success condition, max_retries for invalid arguments, and defer_loading for tool search. |
api_key | Optional key for the target system. It is never returned; responses only contain has_api_key. |
{ "tool_name": "search_companies", "tool_description": "Searches companies by name and returns their ID, name, and email address.", "request_config": { "api_url": "https://api.example.com/v1/contacts/search", "http_method": "POST", "api_key_location": "header", "auth_param": "Authorization", "api_key_prefix": "Bearer", "additional_headers": { "X-Workspace-Id": "example" }, "timeout_seconds": 15, "body_template": { "filter": { "name": { "contains": { "$livoi_input": "name" } }, "type": "company" }, "fields": ["id", "name", "email"], "limit": 10 } }, "input_schema": { "parameters": [ { "name": "name", "type": "string", "location": "body", "description": "Company name to search for." } ] }, "response_config": { "success_condition": { "path": ["errors"], "operator": "absent_or_empty_list" } }, "max_retries": 1, "defer_loading": true, "api_key": "sk_tool_live_123456"}request_config
Section titled “request_config”| Field | Default | Meaning |
|---|---|---|
api_url | – | Must start with http:// or https://. {name} marks path placeholders. |
http_method | – | GET, POST, PUT, PATCH, or DELETE. |
api_key_location | header | Whether the API key is sent as a header or a query parameter. |
auth_param | Authorization | Name of the header or query parameter carrying the API key. |
api_key_prefix | Bearer | Prefix before the key. Empty or null sends the key without a prefix. |
additional_headers | {} | Fixed headers. Names and values are trimmed; line breaks are rejected. |
body_template | – | Optional fixed JSON structure of the request body with references to body parameters. |
unwrap_single_body_parameter | false | Without a template: sends a single body parameter as the entire body instead of an object. |
timeout_seconds | platform | Per-request timeout in seconds, greater than 0. Without a value, the platform default (30 seconds) applies. |
input_schema
Section titled “input_schema”Each parameter becomes a field in the tool’s input model. That model produces the JSON schema the model sees, and the same definition later validates the arguments.
Fields per parameter: name, type, location, description, required (default true), default (optional parameters only), choices (string, integer, number only), properties for object, and items for array.
Types: string, integer, number, boolean, object, and array.
| Location | Rules |
|---|---|
path | Required, with a scalar type. Every placeholder in api_url needs exactly one matching path parameter and vice versa. Values are inserted URL-encoded. |
query | No object. Arrays are allowed if their items are scalar. |
body | Any type. With a template, every body parameter must be referenced in it; without a template, all body parameters together form a JSON object. |
Body templates
Section titled “Body templates”Many APIs expect nested request bodies with filters, field lists, or limits. Without a template, the model would have to produce that structure itself. With body_template, the structure belongs to the tool and the model only supplies the declared variables, such as {"name": "Acme"}.
{ "filter": { "name": { "contains": { "$livoi_input": "name" } }, "type": "company" }, "fields": ["id", "name", "email"], "limit": 10}With the argument {"name": "Acme"}, LIVOI sends exactly this body:
{"filter":{"name":{"contains":"Acme"},"type":"company"},"fields":["id","name","email"],"limit":10}- Type-preserving substitution: a reference is replaced by the validated value, not by text. Numbers stay numbers, objects stay objects. Strings are always inserted as a single JSON string, even if they contain quotes or JSON-like content.
- Fixed values: everything except the references is fixed configuration. It does not appear in the input schema, and because unknown arguments are rejected, the model cannot override it.
nullmeans “not given”: for tools with a template, anullfrom the model is treated like a missing argument, including in nested objects. The default applies; otherwise the referencing object member is removed.
Validation rules
Section titled “Validation rules”| Rule | Limit |
|---|---|
| Root | A JSON object or array. To send a single value as the whole body, use unwrap_single_body_parameter. |
| Size | At most 65,536 bytes as compact JSON. |
| Depth | At most 32 levels. |
| References | At most 64. A reference object contains only $livoi_input. |
| Reference target | Only existing parameters with location: body. |
| Completeness | Every body parameter must be referenced in the template. |
| Arrays | An optional parameter without a default cannot be referenced as an array element, because no member can be removed there. |
| Combination | Not together with unwrap_single_body_parameter: true. |
Request building
Section titled “Request building”- Validated arguments are split into
path,query, andbody. - Path placeholders in
api_urlare replaced URL-encoded. - Query parameters and, with
api_key_location: query, the API key are appended to the URL. additional_headersand, withapi_key_location: header, the prefixed API key are set.- The body is rendered from the template, from the parameter object, or from the unwrapped single value.
- A configured
timeout_secondsis attached to the request.
Response evaluation
Section titled “Response evaluation”The response body is parsed as JSON where possible and passed on as text otherwise. HTTP errors are mapped to stable error types so the model can react sensibly without interpreting raw upstream errors.
| Trigger | error.type |
|---|---|
| HTTP 408, 425, 429, 5xx | upstream_unavailable |
| HTTP 401, 403 | authorization_error |
| HTTP 404 | not_found |
| HTTP 400, 409, 422 | request_rejected |
| Other 4xx | request_failed |
| Timeout | timeout_error |
| Connection failure | connection_error |
| Success condition not met | application_error |
| Success condition not evaluable | unexpected_response |
{ "status": "completed", "status_code": 200, "response": [{ "id": 7, "name": "Acme GmbH" }] }{ "status": "failed", "retryable": false, "status_code": 404, "error": { "type": "not_found", "message": "The connected service could not find the requested resource." } }Success condition
Section titled “Success condition”Some APIs respond with HTTP 200 even when the operation failed. response_config.success_condition checks a path in the parsed body after HTTP error handling. Paths consist of object keys and non-negative array indexes, at most 16 segments.
equalscompares a scalar value without coercion. Integers and floats compare numerically; booleans never equal numbers.absent_or_empty_listsucceeds when the path is missing or points to an empty list, for example["errors"]. Other types count as an unexpected response.
Preview and test request
Section titled “Preview and test request”Both endpoints accept a complete tool definition plus sample arguments, so unsaved drafts can be checked as well. With agent_tool_id, missing values are filled in from the saved tool.
- Preview validates the configuration and arguments, builds the method, URL, headers, and body, and does not contact the target system. The stored API key is not even loaded from the database;
<redacted>takes its place. - Test request sends exactly one real request and returns the status, duration, redacted headers, a bounded body, and the result the agent would receive. Nothing is stored.
{ "agent_tool_id": "7300000000000000002", "tool_name": "search_companies", "tool_description": "Searches companies by name and returns their ID, name, and email address.", "request_config": { "api_url": "https://api.example.com/v1/contacts/search", "http_method": "POST" }, "input_schema": { "parameters": [{ "name": "name", "type": "string", "location": "body" }] }, "arguments": { "name": "Acme" }, "confirm_upstream_request": true}| Request field | Behavior |
|---|---|
api_key omitted | Uses the tool’s stored key (requires agent_tool_id). |
api_key: null | Builds the request without a key. |
api_key: "…" | Uses the given key. |
body_template, response_config omitted | With agent_tool_id, the stored value is used; null removes it for this call. |
confirm_upstream_request | Test only; must be exactly true. |
Test request results
Section titled “Test request results”outcome | Meaning |
|---|---|
completed | Successful response, success condition met. |
http_error | The target system responded with an HTTP error status. |
response_error | HTTP success, but the success condition failed. |
timeout | The request timed out. |
connection_error | No connection to the target system. |
response_too_large | Response larger than 1 MiB; no agent_result. |
Upstream errors, timeouts, and connection failures are returned with HTTP 200 in the response body. request_possibly_processed is only false when the request certainly did not reach the target system, for example on connect, pool, or proxy errors. Argument validation errors return 422 with a path under body.arguments.
Inspection in the chat history
Section titled “Inspection in the chat history”Each custom tool call stores what was actually sent under metadata.custom_tool_inspection on the tool_call history part. Anyone who can read the chat history can read it. The model never receives this data.
Stored: validated arguments (up to 8 KiB, otherwise marked as omitted); method, URL, headers, body, and timeout of the request; status code, duration, and body size of the response; transport error type and request_possibly_processed; result status and error as the agent saw them.
Redacted: the API key, including URL-encoded, JSON-escaped, and byte-escaped forms; all values from additional_headers; Authorization, Proxy-Authorization, Cookie, and Set-Cookie; passwords in the URL.
| Limit | History | Preview and test |
|---|---|---|
| URL | 2,048 characters | 8,192 characters |
| Headers | 32 headers, 512 characters each | 64 headers, 1,024 characters each |
| Body | 16,384 characters | 65,536 characters |
Truncated values are flagged by url_truncated, headers_truncated, and body_truncated.
Tool search with defer_loading
Section titled “Tool search with defer_loading”Every tool costs context: its name, description, and schema are sent with each model request. With large catalogs, such as full CRUD on an ERP with more than a hundred tools, this crowds out the actual conversation. With defer_loading: true, such tools stay out of the first request.
- If at least one tool uses
defer_loading, the agent receives thesearch_toolstool. - The model searches names and descriptions with keywords, up to 10 matches per search.
- Discovered tools become available with their original schema and run through the normal execution path.
- Matches across searches are combined. Without a match, the model can try other terms or explain what is unavailable.
- Persistence: search exchanges are stored with
tool_kind: tool-searchand restored as typed messages on the next run. Discovered tools stay available in that chat; new chats start without them. Deleted tools stay unreachable. - Visibility: searches are visible to the model but hidden from user-facing tool logs.
- Catalog stays local: the provider’s native tool search is disabled, because it would send catalog names and descriptions to the provider. For the default OpenAI endpoint, new schemas are appended; custom endpoints receive ordinary function definitions.
API endpoints
Section titled “API endpoints”All endpoints live under /v1/agent/tools and use the agent_tools resource from the RBAC permission catalog.
| Operation | Endpoint | Permission |
|---|---|---|
createCustomToolForAgent | POST /{agent_id}/custom/create | agent_tools.create |
getCustomToolForAgent | GET /{agent_tool_id}/custom | agent_tools.read |
updateCustomToolForAgent | PATCH /{agent_tool_id}/custom/update | agent_tools.update |
previewCustomToolRequest | POST /{agent_id}/custom/preview | agent_tools.read |
sendCustomToolTestRequest | POST /{agent_id}/custom/test | agent_tools.update |
deleteAgentTools | DELETE /{agent_id}/delete | agent_tools.delete |
- Create: several tools per call are possible. Duplicate names within the request return 422; names that already exist on the agent return 409.
- Read for editing:
getCustomToolForAgentreturns the stored configuration with JSON types preserved, includingresponse_config,max_retries,defer_loading, andhas_api_key. Dynamic Query returns tool metadata scalars as strings and is therefore not suitable for loading abody_templateinto an editor. - Update: the definition is sent in full. Omitted
body_template,response_config,defer_loading, andapi_keykeep their stored value;nullremovesbody_template,response_config, andapi_key.
Security boundaries at a glance
Section titled “Security boundaries at a glance”| Risk | Countermeasure |
|---|---|
| The model manipulates fixed request values | Only declared inputs are part of the schema; unknown arguments are rejected. |
| Injection through string inputs | Type-preserving template substitution, URL encoding in paths, no line breaks in headers. |
| Key leaks in logs or history | Redaction in all common encodings; the API only returns has_api_key. |
| Key redirection through test requests | Stored keys only go to the stored origin, including a Host header check. |
| Cookie carry-over between tenants | Dedicated HTTP client without a cookie jar. |
| Duplicate write operations | Upstream failures are terminal and never retried automatically. |
| Catalog leaking to the provider | Tool search runs locally; native provider search is disabled. |
Compatibility
Section titled “Compatibility”body_templateanddefer_loadinglive in the existing tool metadata; older tools need no changes.- Tools without a
body_templatebuild their requests as before; tools withoutdefer_loadingstay eager. - In OpenAPI, some tool schemas appear as
-Inputand-Outputvariants. The wire format is identical, but generated clients see separate types.
Design principles
Section titled “Design principles”- One builder: runtime, preview, and test build requests through the same code. Preview and reality never diverge.
- The tool owns the structure: the model supplies values, not request structures.
- Validate early: errors in configuration, templates, and defaults are rejected on save, not in a running chat.
- Traceable: every call leaves a redacted snapshot of what was actually sent.
- Context-efficient: large catalogs are discovered on demand instead of being sent in full.
- Safe on failure: upstream failures are terminal, and keys stay bound to their origin.