Tool Resolve API
All endpoints verified working via curl. Document updated only after curl confirmation.
POST /tools/resolve turns a list of tool names into the function blocks you
put in an LLM request. It is the read counterpart to
sync.mdx: the SDKs call sync on start-up to register what the code
defines, then call resolve to get back what the model should actually see.
Names, not ids — the caller is a decorated function in a source file and has no id
to hand. It is a POST because the payload is a list of objects, not because it
writes anything; resolve requires only an authenticated role, not editor.
One call resolves up to 50 refs. Batching matters: an agent with a dozen tools would otherwise make a dozen round-trips before it can send its first prompt.
POST /api/v1/tools/resolve
- curl
- TypeScript
- Python
curl -X POST $ACRUXCORE_BASE_URL/tools/resolve \
-H "Authorization: Bearer $ACRUXCORE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"refs":[{"name":"get_weather","alias":"production"}]}'
const resolved = await hub.tools.resolve([
{ name: 'get_weather', alias: 'production' },
]);
resolved = await hub.tools.resolve([
{'name': 'get_weather', 'alias': 'production'},
])
Response (status 200):
{
"data": [
{
"toolId": "eb1df7c0-778c-4b89-8536-8c08a1f6d406",
"versionNumber": 2,
"executorType": "client",
"function": {
"name": "get_weather",
"description": "Get the current weather, in Celsius, for a city.",
"parameters": {"type":"object","required":["city"],"properties":{"city":{"type":"string","description":"City name, e.g. Lahore."}}}
}
}
]
}
alias defaults to production, so it can be left out. Results come back in the
order the refs were sent, and a batch may mix executor types freely:
curl -X POST $ACRUXCORE_BASE_URL/tools/resolve \
-H "Authorization: Bearer $ACRUXCORE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"refs":[{"name":"get_weather"},{"name":"lookup_order"}]}'
Response (status 200):
{
"data": [
{
"toolId": "eb1df7c0-778c-4b89-8536-8c08a1f6d406",
"versionNumber": 2,
"executorType": "client",
"function": {
"name": "get_weather",
"description": "Get the current weather, in Celsius, for a city.",
"parameters": {"type":"object","required":["city"],"properties":{"city":{"type":"string","description":"City name, e.g. Lahore."}}}
}
},
{
"toolId": "801b77d2-29c9-4050-9488-31a311eb7e68",
"versionNumber": 1,
"executorType": "http",
"function": {
"name": "lookup_order",
"description": "Look up an order by id.",
"parameters": {"type":"object","required":["order_id"],"properties":{"order_id":{"type":"string"}}}
}
}
]
}
Request fields
| Field | Required | Notes |
|---|---|---|
refs | yes | 1–50 refs. |
refs[].name | yes | Tool name, resolved within your team. |
refs[].alias | no | Alias to follow. Defaults to production. Not with version. |
refs[].version | no | Exact version to pin, instead of following an alias. Not with alias. |
Response fields
| Field | Notes |
|---|---|
toolId | Useful for linking to the tool in the dashboard. |
versionNumber | The immutable version the alias resolved to. Log it and a trace tells you exactly which spec the model saw. |
executorType | client or http. Tells the caller who runs the tool. |
function | Ready to drop into an OpenAI-style tools[].function. |
Pinning one exact version
A ref normally follows an alias, so it resolves to whatever that alias points at
right now. Send version instead to name one exact build — the alias is never
consulted, which is the whole point of a pin: a prompt pinned to v1 keeps getting
v1 after production moves on to v3.
curl -X POST $ACRUXCORE_BASE_URL/tools/resolve \
-H "Authorization: Bearer $ACRUXCORE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"refs":[{"name":"get_weather","version":1}]}'
Response (status 200) — v1, even though production points at v3:
{
"data": [
{
"toolId": "0d84d6c3-dc86-4db2-aee3-776bdd35ea34",
"versionNumber": 1,
"executorType": "client",
"function": {
"name": "get_weather",
"description": "Get the current weather for a city.",
"parameters": {"type":"object","required":["city"],"properties":{"city":{"type":"string","description":"City name, e.g. Lahore."}}}
}
}
]
}
alias and version on the same ref is a 400 rather than a precedence rule: a
caller sending both has two different builds in mind, and picking one would
silently run the wrong tool.
curl -X POST $ACRUXCORE_BASE_URL/tools/resolve \
-H "Authorization: Bearer $ACRUXCORE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"refs":[{"name":"get_weather","alias":"production","version":1}]}'
Response (status 400):
{"error":{"code":"VALIDATION_ERROR","message":"a ref takes either alias or version, not both."}}
A pinned ref that does not resolve is reported by its version, not by a phantom alias it never asked for:
{
"error": {
"code": "TOOL_REF_NOT_FOUND",
"message": "Could not resolve 1 tool ref(s): 'get_weather' v99",
"refs": [{"name":"get_weather","version":99}]
}
}
Why executorType is here and the executor is not
executorType is the one thing a tool loop needs in order to route a tool call:
client— your own code runs it. The loop looks the function up in its local registry and calls it.http— the platform runs it. The loop posts the arguments toexecute.mdxand never touches the target service itself.
The executor definition is deliberately absent from the response. A tool's
http executor holds its target url, its headers, and {{secret.NAME}}
references that resolve to real credentials. Those stay server-side. Returning them
so a client could call the service directly would make every tool's credentials
readable by anything holding an API key, and would defeat the point of running
http executors on the platform at all.
Resolving the http tool above returns its schema and nothing else — no url, no
header names, no secret name, no executor key:
{"toolId":"801b77d2-29c9-4050-9488-31a311eb7e68","versionNumber":1,"executorType":"http","function":{"name":"lookup_order","description":"Look up an order by id.","parameters":{"type":"object","required":["order_id"],"properties":{"order_id":{"type":"string"}}}}}
Every failure in one 404
A partial success would be worse than useless here: a tool loop that silently starts without one of its tools produces a model that cannot do what it was asked and no error explaining why. So resolve is all-or-nothing, and when refs fail it reports all of them at once rather than stopping at the first — one round-trip tells you everything that is wrong.
curl -X POST $ACRUXCORE_BASE_URL/tools/resolve \
-H "Authorization: Bearer $ACRUXCORE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"refs":[{"name":"ghost_one"},{"name":"get_weather"},{"name":"shell_no_version"},
{"name":"get_weather","alias":"canary"},{"name":"get_weather","version":99}]}'
Response (status 404) — note get_weather@production resolved fine and is not listed:
{
"error": {
"code": "TOOL_REF_NOT_FOUND",
"message": "Could not resolve 4 tool ref(s): 'ghost_one'@production — no tool named 'ghost_one' in this team; 'shell_no_version'@production — tool 'shell_no_version' has no versions, so alias 'production' does not exist yet — commit one with POST /tools/:id/versions; 'get_weather'@canary — tool 'get_weather' has no alias 'canary'; it has 'production', 'staging'; 'get_weather' v99 — tool 'get_weather' has no version 99; the latest is 1",
"refs": [
{"name":"ghost_one","alias":"production"},
{"name":"shell_no_version","alias":"production"},
{"name":"get_weather","alias":"canary"},
{"name":"get_weather","version":99}
],
"failures": [
{
"name":"ghost_one","alias":"production",
"reason":"no_such_tool",
"message":"no tool named 'ghost_one' in this team"
},
{
"name":"shell_no_version","alias":"production",
"reason":"no_versions",
"message":"tool 'shell_no_version' has no versions, so alias 'production' does not exist yet — commit one with POST /tools/:id/versions"
},
{
"name":"get_weather","alias":"canary",
"reason":"unknown_alias",
"message":"tool 'get_weather' has no alias 'canary'; it has 'production', 'staging'",
"availableAliases":["production","staging"]
},
{
"name":"get_weather","version":99,
"reason":"unknown_version",
"message":"tool 'get_weather' has no version 99; the latest is 1",
"latestVersion":1
}
]
}
}
Which of the four things went wrong
refs carries each failure as it was effectively looked up — a pin by its version,
an alias-follower with the default production filled in — so an SDK can name them
without parsing the message.
failures is that same list with the reason attached. There are four, and they are
four different mistakes with four different fixes:
reason | What it means | What to do |
|---|---|---|
no_such_tool | Nothing in your team has that name, including a soft-deleted tool | Fix the name, or create the tool |
no_versions | The tool exists but nothing has been committed to it, so it has no aliases at all | POST /tools/:id/versions |
unknown_alias | Versions exist; that alias was never promoted. availableAliases lists the ones that were | Use one of those, or promote |
unknown_version | A pinned version number that was never committed. latestVersion is the highest that was | Pin an existing version |
no_versions is worth knowing about specifically, because it is the most likely
failure on a tool you just made. Creating a tool is two calls: POST /tools makes
the shell, and POST /tools/:id/versions commits the version that makes it callable.
Aliases are minted by that first version, so a shell has none — and "no alias
production" on a tool that plainly exists reads as "no such tool" unless the error
says otherwise.
Response (status 400) — an empty refs array:
curl -X POST $ACRUXCORE_BASE_URL/tools/resolve \
-H "Authorization: Bearer $ACRUXCORE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"refs":[]}'
{"error":{"code":"VALIDATION_ERROR","message":"refs must contain at least one ref."}}