Skip to content
LIVOI
Dashboard

Custom tools technical deep dive

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.

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.

Custom tools · Flow
100%

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.

  • A custom tool consists of request_config, input_schema, and optional runtime settings.
  • Inputs are declared as path, query, or body parameters.
  • A body_template defines 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_call history part. The model never receives it.
  • With defer_loading: true, tools stay out of the initial context and are found through search_tools.
  • Upstream failures are classified and returned as terminal results, never retried automatically.
  • The HTTP client for custom tools neither stores nor sends cookies.

Custom tools always belong to exactly one agent and are stored as an agent tool. Tool names are unique per agent.

PartPurpose
tool_name, tool_descriptionName 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_configTarget URL, HTTP method, authentication, additional headers, optional body_template, and timeout.
input_schemaThe parameters the model fills in, each with a type, a location, and optionally a default or choices.
Runtime settingsresponse_config for a success condition, max_retries for invalid arguments, and defer_loading for tool search.
api_keyOptional 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"
}
FieldDefaultMeaning
api_url–Must start with http:// or https://. {name} marks path placeholders.
http_method–GET, POST, PUT, PATCH, or DELETE.
api_key_locationheaderWhether the API key is sent as a header or a query parameter.
auth_paramAuthorizationName of the header or query parameter carrying the API key.
api_key_prefixBearerPrefix 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_parameterfalseWithout a template: sends a single body parameter as the entire body instead of an object.
timeout_secondsplatformPer-request timeout in seconds, greater than 0. Without a value, the platform default (30 seconds) applies.

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.

LocationRules
pathRequired, with a scalar type. Every placeholder in api_url needs exactly one matching path parameter and vice versa. Values are inserted URL-encoded.
queryNo object. Arrays are allowed if their items are scalar.
bodyAny type. With a template, every body parameter must be referenced in it; without a template, all body parameters together form a JSON object.

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.
  • null means “not given”: for tools with a template, a null from the model is treated like a missing argument, including in nested objects. The default applies; otherwise the referencing object member is removed.
RuleLimit
RootA JSON object or array. To send a single value as the whole body, use unwrap_single_body_parameter.
SizeAt most 65,536 bytes as compact JSON.
DepthAt most 32 levels.
ReferencesAt most 64. A reference object contains only $livoi_input.
Reference targetOnly existing parameters with location: body.
CompletenessEvery body parameter must be referenced in the template.
ArraysAn optional parameter without a default cannot be referenced as an array element, because no member can be removed there.
CombinationNot together with unwrap_single_body_parameter: true.
  1. Validated arguments are split into path, query, and body.
  2. Path placeholders in api_url are replaced URL-encoded.
  3. Query parameters and, with api_key_location: query, the API key are appended to the URL.
  4. additional_headers and, with api_key_location: header, the prefixed API key are set.
  5. The body is rendered from the template, from the parameter object, or from the unwrapped single value.
  6. A configured timeout_seconds is attached to the request.

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.

Triggererror.type
HTTP 408, 425, 429, 5xxupstream_unavailable
HTTP 401, 403authorization_error
HTTP 404not_found
HTTP 400, 409, 422request_rejected
Other 4xxrequest_failed
Timeouttimeout_error
Connection failureconnection_error
Success condition not metapplication_error
Success condition not evaluableunexpected_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." } }

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.

  • equals compares a scalar value without coercion. Integers and floats compare numerically; booleans never equal numbers.
  • absent_or_empty_list succeeds when the path is missing or points to an empty list, for example ["errors"]. Other types count as an unexpected response.

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 fieldBehavior
api_key omittedUses the tool’s stored key (requires agent_tool_id).
api_key: nullBuilds the request without a key.
api_key: "…"Uses the given key.
body_template, response_config omittedWith agent_tool_id, the stored value is used; null removes it for this call.
confirm_upstream_requestTest only; must be exactly true.
outcomeMeaning
completedSuccessful response, success condition met.
http_errorThe target system responded with an HTTP error status.
response_errorHTTP success, but the success condition failed.
timeoutThe request timed out.
connection_errorNo connection to the target system.
response_too_largeResponse 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.

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.

LimitHistoryPreview and test
URL2,048 characters8,192 characters
Headers32 headers, 512 characters each64 headers, 1,024 characters each
Body16,384 characters65,536 characters

Truncated values are flagged by url_truncated, headers_truncated, and body_truncated.

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.

  1. If at least one tool uses defer_loading, the agent receives the search_tools tool.
  2. The model searches names and descriptions with keywords, up to 10 matches per search.
  3. Discovered tools become available with their original schema and run through the normal execution path.
  4. 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-search and 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.

All endpoints live under /v1/agent/tools and use the agent_tools resource from the RBAC permission catalog.

OperationEndpointPermission
createCustomToolForAgentPOST /{agent_id}/custom/createagent_tools.create
getCustomToolForAgentGET /{agent_tool_id}/customagent_tools.read
updateCustomToolForAgentPATCH /{agent_tool_id}/custom/updateagent_tools.update
previewCustomToolRequestPOST /{agent_id}/custom/previewagent_tools.read
sendCustomToolTestRequestPOST /{agent_id}/custom/testagent_tools.update
deleteAgentToolsDELETE /{agent_id}/deleteagent_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: getCustomToolForAgent returns the stored configuration with JSON types preserved, including response_config, max_retries, defer_loading, and has_api_key. Dynamic Query returns tool metadata scalars as strings and is therefore not suitable for loading a body_template into an editor.
  • Update: the definition is sent in full. Omitted body_template, response_config, defer_loading, and api_key keep their stored value; null removes body_template, response_config, and api_key.
RiskCountermeasure
The model manipulates fixed request valuesOnly declared inputs are part of the schema; unknown arguments are rejected.
Injection through string inputsType-preserving template substitution, URL encoding in paths, no line breaks in headers.
Key leaks in logs or historyRedaction in all common encodings; the API only returns has_api_key.
Key redirection through test requestsStored keys only go to the stored origin, including a Host header check.
Cookie carry-over between tenantsDedicated HTTP client without a cookie jar.
Duplicate write operationsUpstream failures are terminal and never retried automatically.
Catalog leaking to the providerTool search runs locally; native provider search is disabled.
  • body_template and defer_loading live in the existing tool metadata; older tools need no changes.
  • Tools without a body_template build their requests as before; tools without defer_loading stay eager.
  • In OpenAPI, some tool schemas appear as -Input and -Output variants. The wire format is identical, but generated clients see separate types.
  • 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.