Skip to main content

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:

FieldWho reads itEffect
descriptionThe model. It decides whether to call the tool from this text.Changing it changes the model's behaviour.
changelogYour team, in the dashboard's version list.None. The model never sees it.
sourceThe 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 -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": []
}
}'

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 $ACRUXCORE_BASE_URL/tools/4d9b768c-8ccb-414d-86ab-c629b3cafff5/versions \
-H "Authorization: Bearer $ACRUXCORE_API_KEY"

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 $ACRUXCORE_BASE_URL/tools/4d9b768c-8ccb-414d-86ab-c629b3cafff5/versions/2 \
-H "Authorization: Bearer $ACRUXCORE_API_KEY"

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."}}