Pass a customer's value to a tool¶
The concept behind tools is explained in Tools.
Some values belong neither to the tool nor to the model, but to the request: your customer's
account id in your own system, the region you choose per customer, or your customer's own token for
your API. Such a value is a call variable. The tool declares which call variables it reads, your
application sends their values in tool_variables, and Core fills them in on every call of that
tool. The model never sees them and cannot change them.
A tool therefore takes three kinds of value:
| Kind | Marker in a template | Who supplies it | When |
|---|---|---|---|
| Argument | {name} |
the model | on each call, in its arguments |
| Install value | {$name} |
the tool's author, or the organisation that installs it from a bundle | when the tool is registered or installed (Values you set once) |
| Call variable | {@name} |
your application | on each request, in tool_variables |
You declare and change call variables through the API or in the console. The console pages describe the same fields for an API connector, an MCP server, a script, a workflow and an alias; a schedule holds the values it sends, and the console shows the variables an agent's tools need.
A call variable is not the same as an agent's or a prompt's variables. Those values
(agent.variables) go into the context the model reads. Never send your customer's token in
agent.variables; send it in tool_variables.
Declare call variables on an API connector¶
An API connector declares its call variables in call_variables: a JSON Schema object with one
property per variable. Each property is a string, number, integer or boolean, with an
optional description, enum and default. required names the variables a call cannot do
without. A name is lower-case letters, digits and _, and starts with a letter or a digit.
This example reads one customer's orders from your API, with that customer's own token:
curl -s -X POST "$CORE_URL/api_connectors" \
-H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
-d '{"name": "orders",
"http_url": "https://api.your-app.example/v1/accounts/{@account_id}/orders",
"description": "Open orders of this customer.",
"parameters": {"type": "object", "properties": {"status": {"type": "string"}}},
"request": {"method": "GET",
"query": {"status": "{status}"},
"headers": {"Authorization": "Bearer {@customer_token}"}},
"call_variables": {
"type": "object",
"properties": {
"account_id": {"type": "string", "description": "Account id of this customer with you"},
"customer_token": {"type": "string", "x-secret": true}},
"required": ["account_id", "customer_token"]},
"auth": {"type": "none"}}'
customer_token is a secret (x-secret: true, see Secret values), so it may
stand in the header only. auth stays none because the token differs per request; a credential
that is the same for everybody belongs in auth (Authenticate to your
endpoint).
A call variable may stand where an install value may: in the path and query of http_url, and in a
query value, header value or body string of request. In the URL the value is percent-encoded, in
a query or header value it is text, and a body string that is exactly one {@name} becomes the
value itself, of its own type. An optional variable with no value and no default fills in as
empty text, and as null where it is a whole body value. Core fills in the install values first,
then the call variables, then the model's arguments.
Registration returns 422 on call_variables for:
- a
{@name}that is not declared, or a declaration no{@name}uses; - a
{@name}in the scheme, host, port or fragment of the URL, or in a key; - a name that is also an argument or an install value;
- a secret variable in the URL or a query value.
A call variable therefore never chooses the host: a host that differs per request could send your organisation's credential to a host of the caller's choosing.
A PATCH with call_variables replaces the whole declaration and creates a new
version of the connector.
Declare call variables on an MCP server¶
An MCP server takes call variables in the values of headers only. Core reads a server's tool list
in the background, without a request, so a URL that needs a call variable could never be read.
curl -s -X POST "$CORE_URL/mcp_servers" \
-H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
-d '{"name": "crm", "url": "https://mcp.your-app.example/{$workspace}/mcp",
"headers": {"X-Api-Key": "{$api_key}", "Authorization": "Bearer {@customer_token}"},
"install_values": {
"workspace": {"type": "string", "value": "acme"},
"api_key": {"type": "secret", "value": "fake-api-key-for-docs-0001"}},
"call_variables": {
"type": "object",
"properties": {"customer_token": {"type": "string", "x-secret": true}},
"required": ["customer_token"]},
"auth": {"type": "none"}}'
headersare the headers every request to the server carries. A value may hold{$name}install values and{@name}call variables.install_valueswork as on an API connector. A plain install value may stand in the path and query of the URL and in a header value; asecretin a header value only.- Core leaves a header that holds a call variable off when it reads the tool list, because there is no request there to take a value from. Your server must therefore list its tools without that header.
Authorizationmay be inheadersonly while the server'sauthisnone, as above (TheAuthorizationheader).Proxy-Authorization,Cookie, a header Core or the transport sets, and the header the server's ownauthis sent in are always refused (422onheaders).- A
{@name}in the URL returns422oncall_variables.
A PATCH that names headers, install_values or call_variables replaces that field whole.
Send the values¶
Send the values in tool_variables: a flat object of names to strings, numbers or booleans.
curl -s -X POST "$CORE_URL/responses" \
-H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
-d '{"model": "<a model id>",
"input": "Which orders are still open?",
"tools": [{"type": "tool", "name": "api__orders"}],
"tool_variables": {"account_id": "A-10442",
"customer_token": "fake-token-for-docs-0001"}}'
The same field exists on:
| Where | How |
|---|---|
POST /responses and POST /messages |
tool_variables beside the rest of the request, a background turn's included |
POST /tools/{name}/call |
{"arguments": {...}, "tool_variables": {...}}, checked against that one tool (Call a tool directly) |
| A schedule | tool_variables in the stored request; every fire sends them |
| An evaluation scenario | tool_variables on the scenario, sent on every step; non-secret values only |
| A toolset served over MCP | in each call's params._meta (below) |
A workflow run takes its values in variables when it starts; see Scripts, workflows and
aliases.
What happens to a value¶
- The model never sees it. The values are not in the context, not in a tool's description and not in its arguments.
- Each tool receives only what it declares. A tool that declares no call variables receives none, and no tool sees the request's whole set.
- A name no tool declares is ignored. You can send the same set even when your key's policy or
a toolset leaves a tool out of the turn. The trace names the
ignored names in
interpretation.tool_variables_ignored, and on each tool call the names that tool received. A value is never in a trace, a log line or the audit trail. - The response does not repeat them. The stored response and
GET /responses/{id}carry notool_variables.
A value that is missing or does not fit¶
Core checks tool_variables against the declarations of every tool the turn offers before any
model is called. Nothing goes to the model and nothing is billed when a value is wrong. A tool your
key's policy withholds does not count.
Core refuses:
- a required variable with no value and no
default; - a value its declaration does not admit: the wrong type, or not in its
enum; - a value that cannot stand where it goes: a
{or}, a control character in a header, or.or..in a URL path.
The message names the variable and the tool. On POST /responses it is a 400 in OpenAI's shape:
{
"error": {
"message": "api__orders requires customer_token.",
"type": "invalid_request_error",
"param": "tool_variables.customer_token",
"code": "invalid_request"
}
}
POST /messages answers 400 in Anthropic's shape, with
tool_variables.customer_token: api__orders requires customer_token. as the message.
POST /tools/{name}/call answers 422 with tool_variables.customer_token as the field in its
errors list (Error format).
A background turn checks its values when it starts: when one is missing, the run fails before any model is called, with the same sentence as its error.
Nested agents, tasks and workflows¶
A turn passes its values on to what it starts, because the caller stays the same:
- an agent called as a tool and an agent started as a task receive the whole set and check it against their own tools;
- a tool running as a task in the background keeps the share it declares;
- a workflow started from a turn as a task keeps the values it declares itself;
core__call_tool, with which a turn calls another tool, carries the turn's values;- a tool behind the tool router receives its own share, like any other tool.
The model cannot set call variables itself. A request a core__ tool sends to Core on the model's
behalf may not carry tool_variables; Core refuses it. builtin__subagent offers no tools, so it
has nothing to pass on.
A toolset over MCP¶
When you offer a toolset as an MCP server, the client has no field
for tool_variables. It sends the values with each call in
params._meta["artific/tool_variables"]:
{
"jsonrpc": "2.0",
"id": 7,
"method": "tools/call",
"params": {
"name": "api__orders",
"arguments": {"status": "open"},
"_meta": {"artific/tool_variables": {"account_id": "A-10442",
"customer_token": "fake-token-for-docs-0001"}}
}
}
Core checks the call against that one tool, as POST /tools/{name}/call does. The tool list also
shows the tools that require a variable. When a required variable is missing, a value does not
fit, or _meta is not a flat object, the call answers as MCP reports a failed tool: the refusal's
sentence with isError, before anything runs or is billed.
Secret values¶
An API connector and an MCP server can keep both kinds of value secret.
A secret call variable is a property with x-secret: true: a string of at most 8,192
characters, with no default and no enum, because everyone who can read the tool reads those.
A secret install value is {"type": "secret", "value": "..."}, for an endpoint that needs
another credential besides auth. For an API connector it is described under Values you set
once; on an MCP server it works the same way:
- you write it once and never read it back:
GETanswers{"type": "secret", "has_value": true, "description": "..."}; - a
PATCHthat replacesinstall_valuesgives each secret again, or{"$keep": true}to keep the stored one; - a deployment without a sealing key answers
503to a body carrying a secret.
For both kinds:
- Only in a header value or a body string, never in the URL or a query value, because those end up in proxy and server logs. On an MCP server, in a header value only.
- Core masks them in results. Before the model or the trace sees a result, Core replaces each
exact echo of a secret value of 8 characters or more with
[secret]. A shorter value, or one the endpoint transformed (encoded in base64, hashed, split), is not recognised. Do not send a secret back in your answer. - Stored sealed. A background run and a schedule keep a secret value encrypted; the run drops it
when it ends. A schedule answers the names in
tool_variable_secretsas{"has_value": true}and leaves them out ofrequest.tool_variables; an update that leaves such a name out keeps the stored value. A background request carrying a secret on a deployment without a sealing key is a503. - Not in an evaluation scenario. A scenario is test data everyone with access to the suite
reads. A value for a secret name returns
422ontool_variables.<name>. Core counts a name as secret as soon as an API connector or MCP server of your organisation declares itx-secret. - Scripts, workflows and aliases take no secrets. Their registration refuses an
x-secretcall variable and asecretinstall value with422. A tool behind a workflow or an alias that needs a secret receives it with the request that reaches that tool.
The Authorization header¶
An API connector's template and an MCP server's headers may write the Authorization header
only when the tool's own credential, auth, is none. That is how both examples above work:
"Bearer {@customer_token}" with a secret call variable. Beside a credential it would be a second
one, and that returns 422, on request for a connector and on headers for an MCP server. A
connector or server shared through a publication whose credential travels with it or is filled in
by the installer counts as having a credential.
Cookie and Proxy-Authorization are always refused, and so is the header auth is sent in.
There is no way to replace auth per request: a per-request credential is a secret call variable,
placed where the tool's author put it in the template.
Scripts, workflows and aliases¶
These kinds declare install_values and call_variables too, without secrets:
| Kind | Install value | Call variable |
|---|---|---|
| Script | the file ARTIFIC_INSTALL names, read-only |
the file ARTIFIC_VARIABLES names, holding only the variables the version declares |
| Workflow | install.<name> in an expression |
run.variables.<name> in an expression; a tool step passes them on to its tool |
| Alias | a fixed argument that is exactly "{$name}" |
a fixed argument that is exactly "{@name}" |
A workflow run takes the values in variables when it starts (POST /workflows/{id}/runs), not in
tool_variables; a value that does not fit returns 422 on variables.<name>. A tool step whose
tool requires a variable the run lacks ends with the code variable_missing:<name>.
An alias gives any other tool a value per request without the model seeing it: a fixed argument
"{@customer_id}" gives a search tool a filter per customer, for example.
Limits¶
| What | Limit |
|---|---|
Names in tool_variables |
64 |
| One string | 4,096 characters; 8,192 for a secret |
| The whole object, as compact JSON | 64 KiB |
call_variables on one version |
64 |
A request with too many names, too many bytes in all, or a string over 8,192 characters is refused
on tool_variables before any tool is looked at. A plain string between 4,097 and 8,192 characters
is refused when the turn's tools are checked, on tool_variables.<name> naming the tool that
declares it; for a name no tool declares, it is ignored. Your deployment may have set other limits.