Tool Versions API
All endpoints verified working via curl. Document updated only after curl confirmation.
All endpoints accept either a session cookie (connect.sid) or a Bearer API key.
Committing a version requires owner, admin, or editor role; reads require any
authenticated role.
A version's executor is a discriminated union: {"type":"client"} (the
customer's own app runs the tool — nothing to validate) or a declarative
{"type":"http", ...} executor that the platform itself can call (see
execute.mdx). For an http executor, commit-time validation deep-checks
requestTransform/responseTransform JS syntax, {{secret.NAME}} references,
and that url resolves to a public (non-private/loopback) address.
The first version committed for a tool (versionNumber === 1) auto-creates
production and staging aliases pointing at it; later commits do not move
any alias — use aliases.mdx's promote endpoint for that.
description, changelog, and source
These three fields are easy to confuse, and getting them wrong changes what the model does:
| Field | Who reads it | Effect |
|---|---|---|
description | The model. It decides whether to call the tool from this text. | Changing it changes the model's behaviour. |
changelog | Your team, in the dashboard's version list. | None. The model never sees it. |
source | The dashboard and the audit log. | Records who authored the version, so the dashboard can warn before a deploy overwrites a hand edit. |
source is one of code, dashboard, or api, and defaults to api on this
endpoint. code cannot be claimed here — it means "derived from a decorated
function" and is writable only by sync.mdx. Without that
restriction a hand-rolled call could forge code ownership and make the dashboard
warn about an edit that no deploy would ever supersede.
If you send a changelog and no description, the response carries a warnings
array saying so — a release note is not what the model reads. See the example below.
POST /api/v1/tools/:id/versions
- curl
- TypeScript
- Python
curl -X POST $ACRUXCORE_BASE_URL/tools/50c99ce0-cda1-4b49-81bc-d06e091fbcc5/versions \
-H "Authorization: Bearer $ACRUXCORE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"description": "v1 - calls httpbin GET",
"parametersSchema": {"type":"object","properties":{"city":{"type":"string"}},"required":["city"]},
"executor": {
"type": "http",
"url": "https://httpbin.org/get",
"method": "GET",
"headers": [],
"query": [{"name":"city","value":"placeholder"}],
"argMapping": []
}
}'
const version = await hub.tools.commitVersion('50c99ce0-cda1-4b49-81bc-d06e091fbcc5', {
description: 'v1 - calls httpbin GET',
parametersSchema: { type: 'object', properties: { city: { type: 'string' } }, required: ['city'] },
executor: {
type: 'http',
url: 'https://httpbin.org/get',
method: 'GET',
headers: [],
query: [{ name: 'city', value: 'placeholder' }],
argMapping: [],
},
});
version = hub.tools.commit_version(
'50c99ce0-cda1-4b49-81bc-d06e091fbcc5',
parameters_schema={'type': 'object', 'properties': {'city': {'type': 'string'}}, 'required': ['city']},
executor={
'type': 'http',
'url': 'https://httpbin.org/get',
'method': 'GET',
'headers': [],
'query': [{'name': 'city', 'value': 'placeholder'}],
'argMapping': [],
},
description='v1 - calls httpbin GET',
)
Response (status 201) — first commit: includes auto-created aliases:
{
"id": "b48008a0-d162-48cd-8ebe-e4b399dce756",
"toolId": "50c99ce0-cda1-4b49-81bc-d06e091fbcc5",
"versionNumber": 1,
"description": "v1 - calls httpbin GET",
"changelog": null,
"source": "api",
"parametersSchema": {"type":"object","required":["city"],"properties":{"city":{"type":"string"}}},
"executor": {"url":"https://httpbin.org/get","type":"http","query":[{"name":"city","value":"placeholder"}],"method":"GET","headers":[],"argMapping":[]},
"createdBy": "5b8d5fb8-a8da-4afc-9d5c-20e7c088d6e2",
"createdAt": "2026-07-27T16:21:16.535Z",
"aliases": [
{"id":"5a068857-05d1-435c-802b-983c2b7ca653","alias":"production","versionId":"b48008a0-d162-48cd-8ebe-e4b399dce756","versionNumber":1,"updatedAt":"2026-07-27T16:21:16.538Z"},
{"id":"9bce7731-321a-4cbd-9889-794357af6d9f","alias":"staging","versionId":"b48008a0-d162-48cd-8ebe-e4b399dce756","versionNumber":1,"updatedAt":"2026-07-27T16:21:16.538Z"}
]
}
A subsequent commit (v2) — no aliases field in the response:
curl -X POST $ACRUXCORE_BASE_URL/tools/50c99ce0-cda1-4b49-81bc-d06e091fbcc5/versions \
-H "Authorization: Bearer $ACRUXCORE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"description": "v2 - POST variant",
"parametersSchema": {"type":"object","properties":{"city":{"type":"string"}},"required":["city"]},
"executor": {"type":"http","url":"https://httpbin.org/post","method":"POST","headers":[],"query":[],"argMapping":[]}
}'
Response (status 201):
{
"id": "c1cafe54-edba-4200-b1ef-0a749c2f5b75",
"toolId": "50c99ce0-cda1-4b49-81bc-d06e091fbcc5",
"versionNumber": 2,
"description": "v2 - POST variant",
"changelog": null,
"source": "api",
"parametersSchema": {"type":"object","required":["city"],"properties":{"city":{"type":"string"}}},
"executor": {"url":"https://httpbin.org/post","type":"http","query":[],"method":"POST","headers":[],"argMapping":[]},
"createdBy": "5b8d5fb8-a8da-4afc-9d5c-20e7c088d6e2",
"createdAt": "2026-07-27T16:21:16.560Z"
}
A commit carrying a changelog but no description succeeds, and warns:
curl -X POST $ACRUXCORE_BASE_URL/tools/4d9b768c-8ccb-414d-86ab-c629b3cafff5/versions \
-H "Authorization: Bearer $ACRUXCORE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"changelog": "switched to the read replica",
"parametersSchema": {"type":"object","properties":{"table":{"type":"string"},"schema":{"type":"string"}},"required":["table"]},
"executor": {"type":"client"},
"source": "dashboard"
}'
Response (status 201):
{
"id": "9d05c296-bdc2-4d8a-b0eb-a15e8a7e91ae",
"toolId": "4d9b768c-8ccb-414d-86ab-c629b3cafff5",
"versionNumber": 3,
"description": null,
"changelog": "switched to the read replica",
"source": "dashboard",
"parametersSchema": {"type":"object","required":["table"],"properties":{"table":{"type":"string"},"schema":{"type":"string"}}},
"executor": {"type":"client"},
"createdBy": "5b8d5fb8-a8da-4afc-9d5c-20e7c088d6e2",
"createdAt": "2026-07-27T16:17:32.389Z",
"warnings": ["This version has a changelog but no description, so the model will read the tool-level description instead. `description` is what the model reads; `changelog` is a note for your team."]
}
warnings is present only when there is something to warn about — it is absent, not
empty, on a clean commit.
Response (status 400) — http executor's url is not a public address (SSRF guard, defense-in-depth):
curl -X POST $ACRUXCORE_BASE_URL/tools/347c9ef3-0be1-4603-827f-48a8dd42f49c/versions \
-H "Authorization: Bearer $ACRUXCORE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"description": "SSRF guard check",
"parametersSchema": {"type":"object","properties":{}},
"executor": {
"type": "http",
"url": "http://127.0.0.1:9/x",
"method": "GET",
"headers": [],
"query": [],
"argMapping": []
}
}'
{"error":{"code":"VALIDATION_ERROR","message":"The executor's URL cannot be called: Blocked address: 127.0.0.1"}}
The message names the field so a first-time author who pasted an example URL reads
their own typo rather than a platform fault — see assertExecutorDeepValid in
versions.service.ts.
Response (status 400) — the executor carries an argMapping. The field is stored and
never applied, so a tool relying on it called upstream with the argument missing. It is
rejected here instead, and bodyTemplate is rejected the same way:
{"error":{"code":"VALIDATION_ERROR","message":"argMapping is never applied. Put {{arg.NAME}} in a query value, a header value or the URL instead — for example \"query\": [{ \"name\": \"name\", \"value\": \"{{arg.city}}\" }]."}}
An empty argMapping still commits: every http executor already stored carries the
key, and @acruxcoreai/sdk 0.13.0 and earlier type it as required.
Response (status 404) — tool does not exist or belongs to another team:
{"error":{"code":"NOT_FOUND","message":"Tool not found."}}
Declaring a failure the HTTP status cannot show
An upstream API can answer 200 and still have failed the call: a geocoder that
finds no city returns 200 with an empty list. For a client executor the tool's
own code reports that with a ToolResult — see
Report a tool failure from your own code.
For an http executor the platform makes the call and the customer's code never
runs, so the check is stored on the version instead, as failureWhen.
failureWhen is a JavaScript predicate run against the raw upstream response on
every call. It receives { status, headers, body } and returns null for a
healthy call, or { type, message } to declare a failure. The returned type is
your own slug and lands on the span as errorCode.
The function must be named transform. All three JavaScript hooks on a version
— requestTransform, responseTransform and failureWhen — run in the same
sandbox, and that sandbox invokes whatever it is given under the name transform.
A predicate whose function has any other name is still valid JavaScript, so it
commits without complaint and then fails on every call, recording a transform
warning instead of classifying anything.
failureWhen runs before responseTransform, because a transform is free to
drop the very field that carried the error.
curl -X POST $ACRUXCORE_BASE_URL/tools/d8b5a7cf-bd28-4df5-b03a-e3ae8ea9e4aa/versions \
-H "Authorization: Bearer $ACRUXCORE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"description": "Look up a city'"'"'s coordinates.",
"parametersSchema": {
"type": "object",
"properties": { "city": { "type": "string" } },
"required": ["city"]
},
"executor": {
"type": "http",
"method": "GET",
"url": "https://geocoding-api.open-meteo.com/v1/search",
"query": [
{ "name": "name", "value": "{{arg.city}}" },
{ "name": "count", "value": "1" }
],
"failureWhen": "function transform(input) {\n if ((input.body.results || []).length === 0) {\n return { type: '"'"'location_not_found'"'"', message: '"'"'Open-Meteo has no coordinates for that city.'"'"' };\n }\n return null;\n}"
}
}'
Response (status 201), executor echoed back:
{
"id": "2bbeb8d3-9162-4e57-90f2-6d55f35e3faa",
"toolId": "d8b5a7cf-bd28-4df5-b03a-e3ae8ea9e4aa",
"versionNumber": 1,
"description": "Look up a city's coordinates.",
"source": "api",
"executor": {
"url": "https://geocoding-api.open-meteo.com/v1/search",
"type": "http",
"query": [{"name": "name", "value": "{{arg.city}}"}, {"name": "count", "value": "1"}],
"method": "GET",
"headers": [],
"argMapping": [],
"failureWhen": "function transform(input) {\n if ((input.body.results || []).length === 0) {\n return { type: 'location_not_found', message: 'Open-Meteo has no coordinates for that city.' };\n }\n return null;\n}"
}
}
Executing that version is in execute.mdx, which shows the 200
response carrying an error field.
resultSchema and the order the checks run in
resultSchema is a JSON Schema the result must match. It is checked after
responseTransform has run, because the transformed object is what the model
reads. A mismatch is recorded as a warning; set resultSchemaSeverity to
"error" to make a mismatch fail the call instead.
Three checks can fire on one call, and they run in this order: a non-2xx status,
then failureWhen, then resultSchema. Whichever fires first is the one the span
reports, and any other check that also fired is kept on the span in an
alsoDetected attribute.
GET /api/v1/tools/:id/versions
Lists all versions for a tool, newest first. parametersSchema/executor are
omitted from list items (fetch a specific version for those), but changelog and
source are included — they are what the dashboard's version list shows.
- curl
- TypeScript
- Python
curl $ACRUXCORE_BASE_URL/tools/4d9b768c-8ccb-414d-86ab-c629b3cafff5/versions \
-H "Authorization: Bearer $ACRUXCORE_API_KEY"
const versions = await hub.tools.listVersions('4d9b768c-8ccb-414d-86ab-c629b3cafff5');
versions = hub.tools.list_versions('4d9b768c-8ccb-414d-86ab-c629b3cafff5')
Response (status 200) — v1 came from a decorated function with no docstring, v2 and v3 were committed by hand:
{
"data": [
{
"id": "9d05c296-bdc2-4d8a-b0eb-a15e8a7e91ae",
"toolId": "4d9b768c-8ccb-414d-86ab-c629b3cafff5",
"versionNumber": 3,
"description": null,
"changelog": "switched to the read replica",
"source": "dashboard",
"createdBy": "5b8d5fb8-a8da-4afc-9d5c-20e7c088d6e2",
"createdAt": "2026-07-27T16:17:32.389Z"
},
{
"id": "11e7c6b4-c112-41d5-adcb-1d96921b3bea",
"toolId": "4d9b768c-8ccb-414d-86ab-c629b3cafff5",
"versionNumber": 2,
"description": "Count the rows in a table. Use for quick size checks, not for analytics.",
"changelog": null,
"source": "dashboard",
"createdBy": "5b8d5fb8-a8da-4afc-9d5c-20e7c088d6e2",
"createdAt": "2026-07-27T16:17:19.400Z"
},
{
"id": "b1f4bb07-5eb6-48f7-87b8-70ad2b1f3f80",
"toolId": "4d9b768c-8ccb-414d-86ab-c629b3cafff5",
"versionNumber": 1,
"description": null,
"changelog": null,
"source": "code",
"createdBy": "5b8d5fb8-a8da-4afc-9d5c-20e7c088d6e2",
"createdAt": "2026-07-27T16:17:19.355Z"
}
],
"total": 3,
"page": 1,
"limit": 20
}
GET /api/v1/tools/:id/versions/:version_number
Fetches a specific version, including its full parametersSchema and executor.
- curl
- TypeScript
- Python
curl $ACRUXCORE_BASE_URL/tools/4d9b768c-8ccb-414d-86ab-c629b3cafff5/versions/2 \
-H "Authorization: Bearer $ACRUXCORE_API_KEY"
const version = await hub.tools.getVersion('4d9b768c-8ccb-414d-86ab-c629b3cafff5', 2);
version = hub.tools.get_version('4d9b768c-8ccb-414d-86ab-c629b3cafff5', 2)
Response (status 200):
{
"id": "11e7c6b4-c112-41d5-adcb-1d96921b3bea",
"toolId": "4d9b768c-8ccb-414d-86ab-c629b3cafff5",
"versionNumber": 2,
"description": "Count the rows in a table. Use for quick size checks, not for analytics.",
"changelog": null,
"source": "dashboard",
"parametersSchema": {"type":"object","required":["table"],"properties":{"table":{"type":"string"}}},
"executor": {"type":"client"},
"createdBy": "5b8d5fb8-a8da-4afc-9d5c-20e7c088d6e2",
"createdAt": "2026-07-27T16:17:19.400Z"
}
Response (status 404) — version number does not exist for this tool:
{"error":{"code":"NOT_FOUND","message":"Version 99 not found for this tool."}}