Experiments & Runs API (Phase 5 E3)
All endpoints verified working via curl against a real dev server (real
Postgres, a real BullMQ worker process — apps/worker — consuming a real
local Redis, and a real OpenAI account behind the gateway connection — no
mocked fetch, no mocked queue). Document updated only after curl
confirmation, per CLAUDE.md's API-reference rule — except the
prompt-mismatch-warning field's shape, documented from source; see
"Prompt-mismatch warning" below.
An experiment ties a dataset (examples to evaluate against) to a
(prompt-version × model) grid to sweep. Starting a run freezes the
dataset's examples into exampleSnapshot, resolves the full grid — including
an automatic baseline cell per model (the prompt's production alias if one
exists, otherwise its latest committed version), added whenever the
experiment names a promptId and the resolved baseline version isn't
already an explicit version_ids entry — and enqueues a BullMQ Flow
(one finalize job with one cell job per grid × example pair). Each cell
job renders the prompt version against the example's input variables and
calls the gateway in-process (GatewayService.complete), so budgets, rate
limits, and the cost ledger apply exactly like live traffic. The finalize
job (a BullMQ Flow parent) only runs once every child cell has settled, and
marks the run succeeded or failed.
Auth: requireAnyAuth (session cookie or personal/team API key) on every
route, no role restriction. Team-scoped throughout.
Verified with apps/api running via npm run dev and a real, separately
booted apps/worker process (node dist/index.js), both pointed at the same
Redis instance so the API's enqueued jobs are actually picked up by the
worker — not an in-process/inline stub.
POST /api/v1/experiments
Creates an experiment: a dataset to evaluate, an optional prompt under test
(enables the automatic baseline cell), and the explicit (version × model)
grid to sweep. version_ids and models must each have at least one
entry.
Two more fields exist alongside those, both optional:
alias(request) — which alias's version the automatic baseline cell (added whenprompt_idis set and that version isn't already among the explicitversion_ids) points to. Omitted, the baseline resolves to the prompt'sproductionalias if one exists, otherwise its latest committed version. Has no effect whenprompt_idis omitted, since no baseline cell is ever injected. See "Alias-based baseline" below.promptMismatchWarning(response only) — a non-blocking notice returned when the dataset's examples were sourced from a prompt other thanprompt_id. See "Prompt-mismatch warning" below.
- curl
- TypeScript
- Python
curl -X POST $ACRUXCORE_BASE_URL/experiments \
-H "Authorization: Bearer $ACRUXCORE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"dataset_id": "4595c340-d7fa-4d76-977e-76b60ebefb62", "prompt_id": "173ad5cd-969c-4a04-8896-fbf625bbc2e5", "name": "E3 Task7 greeting sweep", "version_ids": ["b5327b3e-3c45-4858-ac62-1e5852279624"], "models": ["gpt-4o-mini-e3t7"]}'
const experiment = await hub.experiments.create({
datasetId: '4595c340-d7fa-4d76-977e-76b60ebefb62',
promptId: '173ad5cd-969c-4a04-8896-fbf625bbc2e5',
name: 'E3 Task7 greeting sweep',
versionIds: ['b5327b3e-3c45-4858-ac62-1e5852279624'],
models: ['gpt-4o-mini-e3t7'],
});
experiment = await hub.experiments.create(
dataset_id='4595c340-d7fa-4d76-977e-76b60ebefb62',
version_ids=['b5327b3e-3c45-4858-ac62-1e5852279624'],
models=['gpt-4o-mini-e3t7'],
prompt_id='173ad5cd-969c-4a04-8896-fbf625bbc2e5',
name='E3 Task7 greeting sweep',
)
Response (status 201) — runs is empty right after creation:
{
"id": "9462c9b7-62a7-4f01-80fe-71f142cdd02b",
"teamId": "c2e28d00-c7ed-4c27-85d9-434927282f67",
"datasetId": "4595c340-d7fa-4d76-977e-76b60ebefb62",
"promptId": "173ad5cd-969c-4a04-8896-fbf625bbc2e5",
"name": "E3 Task7 greeting sweep",
"config": { "models": ["gpt-4o-mini-e3t7"], "versionIds": ["b5327b3e-3c45-4858-ac62-1e5852279624"] },
"createdBy": "6937e9cb-6754-4f45-aac8-a780d2e3ef57",
"createdAt": "2026-07-07T14:26:50.940Z",
"runs": []
}
Error responses
Missing version_ids (status 400):
curl -X POST $ACRUXCORE_BASE_URL/experiments \
-H "Authorization: Bearer $ACRUXCORE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"dataset_id": "4595c340-d7fa-4d76-977e-76b60ebefb62", "models": ["gpt-4o-mini-e3t7"]}'
{ "error": { "code": "VALIDATION_ERROR", "message": "Required" } }
Alias-based baseline (alias field)
By default (no alias given), the automatic baseline cell (when prompt_id
is set) resolves to the prompt's production alias if one exists, otherwise
its latest committed version. Passing alias points it at a different named
alias's version instead — verified here against a prompt whose production
alias is pinned to v1 while a staging alias was separately promoted to v2,
requesting v2 explicitly plus alias: "staging":
curl -X POST $ACRUXCORE_BASE_URL/experiments \
-H "Authorization: Bearer $ACRUXCORE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"dataset_id": "c4629544-565c-4e34-9d76-6074f525c596", "prompt_id": "e7ec9bcd-e5c1-4d28-bd48-1da7ad3c5027", "name": "Task7 alias baseline demo", "version_ids": ["06c027e3-7f6e-4b1d-8696-6868bae24f7d"], "models": ["gpt-4o-mini"], "alias": "staging"}'
Response (status 201) — alias is stored verbatim on config, alongside
models/versionIds:
{
"id": "b819389b-6422-4bce-87a4-e7901d2052e2",
"teamId": "13a4f9a6-cd2a-47bf-b937-4e18675d6b48",
"datasetId": "c4629544-565c-4e34-9d76-6074f525c596",
"promptId": "e7ec9bcd-e5c1-4d28-bd48-1da7ad3c5027",
"name": "Task7 alias baseline demo",
"config": { "alias": "staging", "models": ["gpt-4o-mini"], "versionIds": ["06c027e3-7f6e-4b1d-8696-6868bae24f7d"] },
"createdBy": "39fcb676-6296-4960-9388-a02cab78d9f9",
"createdAt": "2026-08-04T09:28:05.299Z",
"runs": []
}
alias is only consulted when POST /experiments/:id/runs resolves the
automatic baseline cell (AliasesService.resolveBaselineVersion) — it has
no effect on the explicit version_ids grid itself. An alias that
doesn't exist on this prompt fails the request synchronously: RunsService .startRun awaits resolveGrid (which calls resolveBaselineVersion)
before creating the run row, so the NotFoundError it throws propagates
straight out of POST /experiments/:id/runs as a synchronous 404 — no run
is ever created, and there is nothing to look up afterwards on
GET /runs/:id.
Prompt-mismatch warning (promptMismatchWarning, response only)
Not curl-verified in this environment: reproducing it needs a dataset whose
examples were built via POST /datasets/from-feedback, which requires a
real POST /gateway/chat/completions call against a configured LLM
provider to populate a trace's sourcePromptVersionId — no provider key
(OPENAI_TEST_KEY/ANTHROPIC_TEST_KEY) is configured in this environment.
Documented from source instead
(experiments.service.ts/experiments.types.ts on this branch): when the
dataset's examples were sourced from a prompt other than prompt_id, the
201 response carries an extra top-level promptMismatchWarning object
(camelCase, unlike the optimize endpoint's snake_case
prompt_mismatch_warning — this endpoint's whole response is camelCase):
{
"id": "9462c9b7-62a7-4f01-80fe-71f142cdd02b",
"teamId": "c2e28d00-c7ed-4c27-85d9-434927282f67",
"datasetId": "4595c340-d7fa-4d76-977e-76b60ebefb62",
"promptId": "173ad5cd-969c-4a04-8896-fbf625bbc2e5",
"name": "E3 Task7 greeting sweep",
"config": { "models": ["gpt-4o-mini-e3t7"], "versionIds": ["b5327b3e-3c45-4858-ac62-1e5852279624"] },
"createdBy": "6937e9cb-6754-4f45-aac8-a780d2e3ef57",
"createdAt": "2026-07-07T14:26:50.940Z",
"runs": [],
"promptMismatchWarning": {
"mismatchedPrompts": [
{ "promptId": "1e5e7cd3-7570-402c-97ff-e5b0c4eadfb3", "name": "support-reply", "exampleCount": 2 }
]
}
}
mismatchedPrompts groups by the other prompt each mismatched example
was sourced from, with exampleCount counting only that prompt's examples
in this dataset. It's informational only — the experiment is created
either way — and the field is omitted entirely (not null) when every
example either matches prompt_id or carries no resolvable lineage
(manually-added examples, or a since-deleted source version). It is only
computed when prompt_id is given, since without a target prompt there is
nothing to compare an example's lineage against.
POST /api/v1/experiments/:id/runs
Starts a run: freezes the dataset's current examples, resolves the grid (here: the explicit v2 cell + an automatic production-baseline cell, since production still pointed at v1 — not among the explicit version_ids), and enqueues a BullMQ Flow. Returns immediately — the actual gateway calls happen asynchronously in the worker process.
- curl
- TypeScript
- Python
curl -X POST $ACRUXCORE_BASE_URL/experiments/9462c9b7-62a7-4f01-80fe-71f142cdd02b/runs \
-H "Authorization: Bearer $ACRUXCORE_API_KEY"
const run = await hub.runs.start({
experimentId: '9462c9b7-62a7-4f01-80fe-71f142cdd02b',
});
run = await hub.experiments.start_run(
'9462c9b7-62a7-4f01-80fe-71f142cdd02b',
)
Response (status 202):
{ "run_id": "891710e4-1ac4-4bb7-b618-0f81e31aa291", "status": "queued" }
Error responses
Nonexistent experiment id (status 404):
curl -X POST $ACRUXCORE_BASE_URL/experiments/00000000-0000-0000-0000-000000000000/runs \
-H "Authorization: Bearer $ACRUXCORE_API_KEY"
{ "error": { "code": "NOT_FOUND", "message": "Experiment not found." } }
GET /api/v1/runs
Lists the team's runs, newest first — the run history. Each row carries the dataset and prompt the run evaluated, the shape of its frozen grid, and the scores it produced, so you can find the run worth opening without fetching each one's report.
kind is "optimize" when the run swept optimizer-drafted candidates
(POST /prompts/:id/optimize) and "evaluation" otherwise.
avgScore, passRate and topVariantLabel are null — never 0 — until the
run has at least one judge-scored result, so an unscored run is never mistaken
for one that scored badly. avgScore is the mean across every scored result in
the run, weighted by example (a 1-example variant does not count the same as a
100-example one), and topVariantLabel names the highest-mean variant.
Query params (all optional):
| Param | Type | Default | Meaning |
|---|---|---|---|
status | queued | running | succeeded | failed | — | Lifecycle filter |
dataset_id | uuid | — | Only runs whose experiment used this dataset |
prompt_id | uuid | — | Only runs whose experiment named this prompt |
page | integer ≥ 1 | 1 | 1-based page number |
limit | integer 1–100 | 20 | Rows per page |
- curl
- TypeScript
- Python
curl -H "Authorization: Bearer $ACRUXCORE_API_KEY" $ACRUXCORE_BASE_URL/runs
const runs = await hub.runs.list();
runs = await hub.runs.list()
Response (status 200) — one optimize run and one evaluation run, both finished:
{
"data": [
{
"id": "df727319-f64d-4ebd-8e3d-fa1b43d0f377",
"status": "succeeded",
"kind": "optimize",
"experimentId": "fc90fd51-4f00-400f-b547-8a17017434dd",
"experimentName": "optimize",
"datasetId": "d95a4068-3415-48fe-959a-ba7cfd3d96f5",
"datasetName": "support replies",
"promptId": "1e5e7cd3-7570-402c-97ff-e5b0c4eadfb3",
"promptName": "support-reply",
"variantCount": 3,
"modelCount": 1,
"exampleCount": 3,
"results": { "total": 9, "succeeded": 9, "errored": 0, "scored": 9 },
"avgScore": 83.9,
"passRate": 1,
"topVariantLabel": "candidate-A",
"startedBy": { "id": "035f4fca-ec77-47e2-b6de-755b0111c798", "name": "Run History", "email": "runhist@example.com" },
"createdAt": "2026-08-04T03:32:57.141Z",
"startedAt": "2026-08-04T03:33:00.093Z",
"endedAt": "2026-08-04T03:33:08.434Z",
"durationMs": 8341
},
{
"id": "54a06825-dce7-4c53-9c4a-7115ebd91ace",
"status": "succeeded",
"kind": "evaluation",
"experimentId": "01009014-6f50-41a8-9089-ddcc8aca75e9",
"experimentName": "support reply sweep",
"datasetId": "d95a4068-3415-48fe-959a-ba7cfd3d96f5",
"datasetName": "support replies",
"promptId": "1e5e7cd3-7570-402c-97ff-e5b0c4eadfb3",
"promptName": "support-reply",
"variantCount": 2,
"modelCount": 1,
"exampleCount": 3,
"results": { "total": 6, "succeeded": 6, "errored": 0, "scored": 6 },
"avgScore": 81.7,
"passRate": 1,
"topVariantLabel": "production",
"startedBy": { "id": "035f4fca-ec77-47e2-b6de-755b0111c798", "name": "Run History", "email": "runhist@example.com" },
"createdAt": "2026-08-04T03:32:35.362Z",
"startedAt": "2026-08-04T03:32:35.398Z",
"endedAt": "2026-08-04T03:32:43.461Z",
"durationMs": 8063
}
],
"total": 2,
"page": 1,
"limit": 20
}
A run still in flight lists the partial numbers it has produced so far rather than nothing — polled while the run above was mid-flight, 4 of its 6 results were already scored:
{ "status": "running", "results": { "total": 6, "succeeded": 6, "errored": 0, "scored": 4 }, "avgScore": 82.5, "durationMs": null }
Paging (total is the full filtered count, not the page's length):
curl -H "Authorization: Bearer $ACRUXCORE_API_KEY" "$ACRUXCORE_BASE_URL/runs?limit=1&page=2"
{ "total": 2, "page": 2, "limit": 1 }
A filter that matches nothing returns an empty page, not a 404:
curl -H "Authorization: Bearer $ACRUXCORE_API_KEY" "$ACRUXCORE_BASE_URL/runs?status=failed"
{ "data": [], "total": 0, "page": 1, "limit": 20 }
Error responses
A status outside the four lifecycle values (status 400):
curl -H "Authorization: Bearer $ACRUXCORE_API_KEY" "$ACRUXCORE_BASE_URL/runs?status=cancelled"
{ "error": { "code": "VALIDATION_ERROR", "message": "Invalid enum value. Expected 'queued' | 'running' | 'succeeded' | 'failed', received 'cancelled'" } }
A limit above the 100 ceiling (status 400):
curl -H "Authorization: Bearer $ACRUXCORE_API_KEY" "$ACRUXCORE_BASE_URL/runs?limit=500"
{ "error": { "code": "VALIDATION_ERROR", "message": "Number must be less than or equal to 100" } }
GET /api/v1/runs/:id
Polled immediately after starting the run above — the real worker had not yet picked up any of the 4 cell jobs (2 grid cells × 2 dataset examples):
- curl
- TypeScript
- Python
curl -H "Authorization: Bearer $ACRUXCORE_API_KEY" $ACRUXCORE_BASE_URL/runs/891710e4-1ac4-4bb7-b618-0f81e31aa291
const run = await hub.runs.get('891710e4-1ac4-4bb7-b618-0f81e31aa291');
run = await hub.runs.get('891710e4-1ac4-4bb7-b618-0f81e31aa291')
Response (status 200) — in-flight, queued, zero results so far:
{
"id": "891710e4-1ac4-4bb7-b618-0f81e31aa291",
"experimentId": "9462c9b7-62a7-4f01-80fe-71f142cdd02b",
"status": "queued",
"startedAt": null,
"endedAt": null,
"error": null,
"createdAt": "2026-07-07T14:27:16.920Z",
"grid": [
{ "model": "gpt-4o-mini-e3t7", "cellKey": "v2|gpt-4o-mini-e3t7", "variantKind": "version", "variantLabel": "v2", "promptVersionId": "b5327b3e-3c45-4858-ac62-1e5852279624", "isProductionBaseline": false },
{ "model": "gpt-4o-mini-e3t7", "cellKey": "production|gpt-4o-mini-e3t7", "variantKind": "version", "variantLabel": "production", "promptVersionId": "8250ce7b-cc5f-463b-83be-3a497f3f6f0a", "isProductionBaseline": true }
],
"exampleCount": 2,
"results": { "total": 0, "succeeded": 0, "errored": 0 }
}
Polled again ~2 seconds later, same run id — the real worker process had by then dequeued and processed all 4 cell jobs (2 grid cells x 2 examples) against the real OpenAI account, and the finalize job marked it succeeded:
curl -H "Authorization: Bearer $ACRUXCORE_API_KEY" $ACRUXCORE_BASE_URL/runs/891710e4-1ac4-4bb7-b618-0f81e31aa291
Response (status 200) — succeeded, all 4 results in:
{
"id": "891710e4-1ac4-4bb7-b618-0f81e31aa291",
"experimentId": "9462c9b7-62a7-4f01-80fe-71f142cdd02b",
"status": "succeeded",
"startedAt": null,
"endedAt": "2026-07-07T14:27:18.799Z",
"error": null,
"createdAt": "2026-07-07T14:27:16.920Z",
"grid": [
{ "model": "gpt-4o-mini-e3t7", "cellKey": "v2|gpt-4o-mini-e3t7", "variantKind": "version", "variantLabel": "v2", "promptVersionId": "b5327b3e-3c45-4858-ac62-1e5852279624", "isProductionBaseline": false },
{ "model": "gpt-4o-mini-e3t7", "cellKey": "production|gpt-4o-mini-e3t7", "variantKind": "version", "variantLabel": "production", "promptVersionId": "8250ce7b-cc5f-463b-83be-3a497f3f6f0a", "isProductionBaseline": true }
],
"exampleCount": 2,
"results": { "total": 4, "succeeded": 4, "errored": 0 }
}
Note: startedAt stayed null through this observed run — the run engine
does not appear to stamp it on transition to succeeded in this build; only
endedAt is populated. Documented as observed, not as a defect judgment.
Drift note (re-verified 2026-07-12): isProductionBaseline on each grid
entry above was missing from this doc's original example even though the
live server includes it for runs started via POST /experiments/:id/runs
— added back here to match observed behavior. By contrast, a run started
via the optimize loop (POST /prompts/:promptId/optimize, documented in
optimize.mdx) does NOT include isProductionBaseline on its grid entries
(only GET /runs/:id/report's variants/cells carry that field for an
optimize-originated run) — a real, observed difference between the two
run-creation code paths, not a doc error.
Each of the 4 result rows landed in eval_results with real, distinct model
output and its own real gateway trace_id (verified via a read-only SELECT,
not part of the HTTP surface but confirming the worker really called the
gateway rather than stubbing it):
v2 / Alice -> "Hey Alice! 🌟 It's a fantastic day and I'm so excited to see you shine! 🎉"
v2 / Bob -> "Hey there, Bob! 🎉 Get ready to seize the day and make it amazing!"
production / Alice -> "Hello, Alice! It's wonderful to see you!"
production / Bob -> "Hello, Bob! It's great to see you!"
Error responses
Nonexistent run id (status 404):
curl -H "Authorization: Bearer $ACRUXCORE_API_KEY" $ACRUXCORE_BASE_URL/runs/00000000-0000-0000-0000-000000000000
{ "error": { "code": "NOT_FOUND", "message": "Run not found." } }
GET /api/v1/runs/:id/report
Comparison report (Phase 5 E5): the full (variant x model) matrix with
per-cell averages, each non-baseline cell's regression delta vs. the
same-model production-baseline cell, a leaderboard, and an advisory
winner. Computed on read from the run's eval_result rows — no dedicated
report table. Verified against a fresh experiment (prompt "support-reply",
v1 promoted to production with a vague, unconstrained system message;
v2 an explicit "reply in exactly two sentences, no exclamation marks"
instruction), run against 2 dataset examples whose criteria both demand
the same strict two-sentence format — arranged specifically so v2 and
production diverge sharply on the judge's score, rather than landing on a
flat/unknown delta that wouldn't demonstrate the feature.
Note: the judge model itself is a fixed gpt-4o-mini public name
(JUDGE_MODEL in judge.service.ts, overridable via EVAL_JUDGE_MODEL) —
distinct from whatever public model name(s) the experiment's grid sweeps.
It must be registered separately via POST /gateway/models (same as any
other model) or every judge call fails with Model 'gpt-4o-mini' is not registered. (observed firsthand on a first attempt before registering it;
see the cell drill-down section below for that raw error text).
- curl
- TypeScript
- Python
curl -H "Authorization: Bearer $ACRUXCORE_API_KEY" $ACRUXCORE_BASE_URL/runs/6e8bcaaf-8b5b-40f5-9b59-6d83eec71596/report
const report = await hub.runs.getReport('6e8bcaaf-8b5b-40f5-9b59-6d83eec71596');
report = await hub.runs.get_report('6e8bcaaf-8b5b-40f5-9b59-6d83eec71596')
Response (status 200) — v2 clearly beats the production baseline: avgScore 85 vs 20, passRate 1 vs 0, deltaVsBaseline.label "improved" (+65 score points), and the winner is v2:
{
"runId": "6e8bcaaf-8b5b-40f5-9b59-6d83eec71596",
"status": "succeeded",
"models": ["gpt-4o-mini-e5t3"],
"variants": [
{ "variantKind": "version", "promptVersionId": "3bbf603b-e99e-475a-adf6-ab9edcb0ecc0", "variantLabel": "v2", "isProductionBaseline": false },
{ "variantKind": "version", "promptVersionId": "2bd8f192-d4cf-4123-9816-4d8eba9158bb", "variantLabel": "production", "isProductionBaseline": true }
],
"cells": [
{
"cellKey": "v2|gpt-4o-mini-e5t3",
"variantLabel": "v2",
"model": "gpt-4o-mini-e5t3",
"isProductionBaseline": false,
"avgScore": 85,
"passRate": 1,
"exampleCount": 2,
"scoredCount": 2,
"unscoredCount": 0,
"deltaVsBaseline": { "score": 65, "passRate": 1, "label": "improved" }
},
{
"cellKey": "production|gpt-4o-mini-e5t3",
"variantLabel": "production",
"model": "gpt-4o-mini-e5t3",
"isProductionBaseline": true,
"avgScore": 20,
"passRate": 0,
"exampleCount": 2,
"scoredCount": 2,
"unscoredCount": 0,
"deltaVsBaseline": null
}
],
"leaderboard": ["v2|gpt-4o-mini-e5t3", "production|gpt-4o-mini-e5t3"],
"winner": { "cellKey": "v2|gpt-4o-mini-e5t3", "variantLabel": "v2", "model": "gpt-4o-mini-e5t3", "avgScore": 85 }
}
Error responses
Nonexistent run id (status 404) — same message/code as GET /runs/:id:
curl -H "Authorization: Bearer $ACRUXCORE_API_KEY" $ACRUXCORE_BASE_URL/runs/00000000-0000-0000-0000-000000000000/report
{ "error": { "code": "NOT_FOUND", "message": "Run not found." } }
GET /api/v1/runs/:id/cells/:cellKey
Drill-down (Phase 5 E5): one grid cell's per-example outputs, judge
reasoning, and traces. cellKey is ${variantLabel}|${model} (as minted
into the run's grid) and must be URL-encoded (| -> %7C) — decoded
again by Express before it reaches the controller. Each example in the
response also carries history: the same array frozen onto the dataset
example at run-start time (see history on GET /api/v1/datasets/:id in
the datasets reference), or null for a single-turn
example — this is the exact conversation that was replayed ahead of input
for that cell's completion, not the live (possibly since-edited) dataset
value. Not separately re-curled below: it's the same frozen JSON already
curl-verified on the dataset endpoint, and the run/judge wiring is covered
by the real-Postgres integration test
apps/api/src/evaluations/runs/run-engine.test.ts. Curled against the same
run as the report above, both of its two cells:
curl -H "Authorization: Bearer $ACRUXCORE_API_KEY" "$ACRUXCORE_BASE_URL/runs/6e8bcaaf-8b5b-40f5-9b59-6d83eec71596/cells/v2%7Cgpt-4o-mini-e5t3"
Response (status 200) — v2's two examples: one perfect (score 100), one
partial-credit (score 70, missing the concrete next step) — both passed:
{
"cellKey": "v2|gpt-4o-mini-e5t3",
"variantLabel": "v2",
"model": "gpt-4o-mini-e5t3",
"examples": [
{
"exampleId": "95f4e926-c036-4fa6-881a-4453b7c3d3fd",
"input": { "complaint": "My package arrived three days late and the box was crushed." },
"criteria": "Response must be EXACTLY two sentences: first sentence apologizes and names the specific issue (late delivery / crushed box), second sentence states one concrete next step (e.g. replacement or refund). No exclamation marks. No greeting or sign-off.",
"output": "I apologize for the delay and the condition of your package. Please provide your order number so we can initiate a replacement or refund for you.",
"score": 100,
"passed": true,
"reason": "The output consists of exactly two sentences: the first sentence apologizes for the delay and mentions the condition of the package, while the second sentence clearly states the next step of providing the order number for a replacement or refund.",
"traceId": "04e58fce-3fd0-4488-907e-d0a79a7cc47d",
"judgeTraceId": "2fd53824-36bd-48f9-9550-b9ba43ae1663"
},
{
"exampleId": "e4db8a4f-f530-49ac-87e7-f4f208b2c19b",
"input": { "complaint": "I was charged twice for the same order and nobody has responded to my emails." },
"criteria": "Response must be EXACTLY two sentences: first sentence apologizes and names the specific issue (double charge / no response to emails), second sentence states one concrete next step (e.g. refund of duplicate charge). No exclamation marks. No greeting or sign-off.",
"output": "I apologize for the inconvenience caused by the double charge and the lack of response to your emails. Please provide your order number so that I can investigate this issue further.",
"score": 70,
"passed": true,
"reason": "The output contains an apology for the double charge and the lack of response to emails, fulfilling the first sentence requirement. However, it does not provide a concrete next step, which is necessary to fully meet the criteria.",
"traceId": "2b8607f6-d7e8-43bb-92ff-3c105efb7ed4",
"judgeTraceId": "db82535f-3c88-478f-8ff2-489a8c2e5bb8"
}
]
}
The production-baseline cell, for contrast — both examples score 20,
passed: false, and the judge's reason names the exact violation (too many
sentences, greeting/sign-off present):
curl -H "Authorization: Bearer $ACRUXCORE_API_KEY" "$ACRUXCORE_BASE_URL/runs/6e8bcaaf-8b5b-40f5-9b59-6d83eec71596/cells/production%7Cgpt-4o-mini-e5t3"
Response (status 200) — excerpted to one example (the other is the same shape):
{
"cellKey": "production|gpt-4o-mini-e5t3",
"variantLabel": "production",
"model": "gpt-4o-mini-e5t3",
"examples": [
{
"exampleId": "e4db8a4f-f530-49ac-87e7-f4f208b2c19b",
"input": { "complaint": "I was charged twice for the same order and nobody has responded to my emails." },
"criteria": "Response must be EXACTLY two sentences: first sentence apologizes and names the specific issue (double charge / no response to emails), second sentence states one concrete next step (e.g. refund of duplicate charge). No exclamation marks. No greeting or sign-off.",
"output": "I'm really sorry to hear about the double charge and the delay in getting a response. We certainly want to resolve this issue for you as quickly as possible. \n\nTo assist you better, could you please provide me with the following details? \n\n1. The order number.\n2. The date of the order.\n3. Any reference numbers or transaction details for both charges.\n\nOnce I have that information, I'll be able to look into it right away and help you sort this out. Thank you for your patience, and I appreciate your understanding!",
"score": 20,
"passed": false,
"reason": "The output does not meet the requirement of being exactly two sentences; it contains multiple sentences and includes a greeting and sign-off, which are not allowed.",
"traceId": "c43822d7-5562-405e-8fa0-02a7acb3995a",
"judgeTraceId": "3e396879-160e-4f3d-9f05-95dde37ece79"
}
]
}
Error responses
Nonexistent run id (status 404) — same message/code as GET /runs/:id/report:
curl -H "Authorization: Bearer $ACRUXCORE_API_KEY" "$ACRUXCORE_BASE_URL/runs/00000000-0000-0000-0000-000000000000/cells/v2%7Cgpt-4o-mini-e5t3"
{ "error": { "code": "NOT_FOUND", "message": "Run not found." } }
A cellKey that does not match any of the run's grid cells (status 200,
not an error) — the run itself is real, the cell key is simply not one of
its grid entries, so it returns gracefully with an empty examples array:
curl -H "Authorization: Bearer $ACRUXCORE_API_KEY" "$ACRUXCORE_BASE_URL/runs/6e8bcaaf-8b5b-40f5-9b59-6d83eec71596/cells/nonexistent%7Cmodel-x"
{ "cellKey": "nonexistent|model-x", "variantLabel": "nonexistent", "model": "model-x", "examples": [] }
GET /api/v1/experiments
Lists the team's experiments. Note: unlike the JSDoc on ExperimentDto
(which says runs is "omitted (empty array) on list"), the list endpoint as
observed actually DOES populate runs per experiment (newest first) — this
doc reflects the real observed response, not the comment.
- curl
- TypeScript
- Python
curl -H "Authorization: Bearer $ACRUXCORE_API_KEY" $ACRUXCORE_BASE_URL/experiments
const experiments = await hub.experiments.list();
experiments = await hub.experiments.list()
Response (status 200) — abbreviated to one experiment with its 2 runs:
{
"data": [
{
"id": "9462c9b7-62a7-4f01-80fe-71f142cdd02b",
"teamId": "c2e28d00-c7ed-4c27-85d9-434927282f67",
"datasetId": "4595c340-d7fa-4d76-977e-76b60ebefb62",
"promptId": "173ad5cd-969c-4a04-8896-fbf625bbc2e5",
"name": "E3 Task7 greeting sweep",
"config": { "models": ["gpt-4o-mini-e3t7"], "versionIds": ["b5327b3e-3c45-4858-ac62-1e5852279624"] },
"createdBy": "6937e9cb-6754-4f45-aac8-a780d2e3ef57",
"createdAt": "2026-07-07T14:26:50.940Z",
"runs": [
{ "id": "891710e4-1ac4-4bb7-b618-0f81e31aa291", "experimentId": "9462c9b7-62a7-4f01-80fe-71f142cdd02b", "status": "succeeded", "startedAt": null, "endedAt": "2026-07-07T14:27:18.799Z", "error": null, "createdAt": "2026-07-07T14:27:16.920Z" },
{ "id": "0cb95fe0-5459-4287-a7b3-f8929ce3d0e1", "experimentId": "9462c9b7-62a7-4f01-80fe-71f142cdd02b", "status": "succeeded", "startedAt": null, "endedAt": "2026-07-07T14:26:59.929Z", "error": null, "createdAt": "2026-07-07T14:26:56.763Z" }
]
}
]
}
GET /api/v1/experiments/:id
Fetches one experiment with its runs (newest first).
- curl
- TypeScript
- Python
curl -H "Authorization: Bearer $ACRUXCORE_API_KEY" $ACRUXCORE_BASE_URL/experiments/9462c9b7-62a7-4f01-80fe-71f142cdd02b
const experiment = await hub.experiments.get('9462c9b7-62a7-4f01-80fe-71f142cdd02b');
experiment = await hub.experiments.get('9462c9b7-62a7-4f01-80fe-71f142cdd02b')
Response (status 200) — same shape as the list entry above for this experiment.
Error responses
Nonexistent experiment id (status 404):
curl -H "Authorization: Bearer $ACRUXCORE_API_KEY" $ACRUXCORE_BASE_URL/experiments/00000000-0000-0000-0000-000000000000
{ "error": { "code": "NOT_FOUND", "message": "Experiment not found." } }
DELETE /api/v1/experiments/:id
Deletes an experiment and every run under it, including each run's cells and reports. The dataset it swept and any prompt version promoted out of one of its runs are separate resources and are untouched; so is any optimizer candidate the runs drafted (the candidate's run link is simply cleared).
Refused while any of the experiment's runs is still queued or running —
the worker is mid-flight and would keep writing results for a row that no
longer exists. Wait for the run to settle, or delete the settled runs
individually with DELETE /api/v1/runs/:id.
- curl
- TypeScript
- Python
curl -X DELETE -H "Authorization: Bearer $ACRUXCORE_API_KEY" \
$ACRUXCORE_BASE_URL/experiments/ff661325-588f-4984-8825-6102acfe3161
await hub.experiments.delete('ff661325-588f-4984-8825-6102acfe3161');
await hub.experiments.delete('ff661325-588f-4984-8825-6102acfe3161')
Response (status 200):
{ "success": true }
Error responses
A run is still in flight (status 409):
curl -X DELETE -H "Authorization: Bearer $ACRUXCORE_API_KEY" \
$ACRUXCORE_BASE_URL/experiments/608693aa-e344-4c34-a5c0-12d9d18cbbb5
{
"error": {
"code": "RUN_IN_FLIGHT",
"message": "This experiment has 1 run still queued or running. Wait for it to finish, then delete."
}
}
Already deleted, nonexistent, or another team's id (status 404 — never confirms whether it exists elsewhere):
curl -X DELETE -H "Authorization: Bearer $ACRUXCORE_API_KEY" \
$ACRUXCORE_BASE_URL/experiments/ff661325-588f-4984-8825-6102acfe3161
{ "error": { "code": "NOT_FOUND", "message": "Experiment not found." } }
DELETE /api/v1/runs/:id
Deletes one run and every cell it produced, so its report goes with it. The parent experiment stays — a run is one execution of it — and so does any optimizer candidate the run drafted, which matters because a candidate may already have been promoted to a real prompt version.
Refused while the run is still queued or running, for the same reason as
the experiment delete above.
- curl
- TypeScript
- Python
curl -X DELETE -H "Authorization: Bearer $ACRUXCORE_API_KEY" \
$ACRUXCORE_BASE_URL/runs/f595cade-4145-49a9-b228-aa344dad15c8
await hub.runs.delete('f595cade-4145-49a9-b228-aa344dad15c8');
await hub.runs.delete('f595cade-4145-49a9-b228-aa344dad15c8')
Response (status 200):
{ "success": true }
A subsequent read of the same run 404s, while its experiment still resolves:
curl -H "Authorization: Bearer $ACRUXCORE_API_KEY" \
$ACRUXCORE_BASE_URL/runs/f595cade-4145-49a9-b228-aa344dad15c8
{ "error": { "code": "NOT_FOUND", "message": "Run not found." } }
Error responses
The run has not finished (status 409):
{
"error": {
"code": "RUN_IN_FLIGHT",
"message": "This run is still queued. Wait for it to finish, then delete it."
}
}