Experiments
HTTP API routes for experiments.
| Route | Use | CLI equivalent |
|---|---|---|
GET /experiments | List registered experiments. | ax experiment list --json |
POST /experiments | Register YAML without running it. | ax experiment push |
GET /experiments/{id} | Inspect one experiment. | ax experiment view --json |
POST /experiments/{id}/resources | Attach titled links. | ax experiment resource add |
PATCH /experiments/{id}/resources/{resourceId} | Change a resource's title or URL. | ax experiment resource edit |
DELETE /experiments/{id}/resources/{resourceId} | Remove a resource. | ax experiment resource delete |
GET /experiments/{id}/versions | List stored versions. | ax experiment versions --json |
GET /experiments/{id}/yaml | Download registered YAML. | ax experiment pull |
POST /experiments/{id}/fork | Copy an experiment onto the current data pipeline. | ax experiment fork |
POST /experiments/{id}/runs | Submit a run. | ax experiment run <id> |
POST /experiments/{id}/query | Query results in experiment scope. | ax experiment query |
Every route takes org_id as a query parameter (required only when the key
can see several orgs; see Organization scope); it is
not repeated in the tables below. Parameters are optional unless marked
required.
GET /api/v1/experiments
List registered experiments in the selected org, newest first.
curl -H "Authorization: Bearer $AX_API_KEY" \
"https://app.514.ax/api/v1/experiments?search=install&limit=20"| Parameter | What it does |
|---|---|
search | Case-insensitive match on experiment name or id. |
owner | Owner email or id filter. |
agent / model / product / environment | Dimension filters (repeatable or comma-separated). Same axis ORs; different axes AND. |
since | RFC 3339 updated-at floor (updated_since is an alias). |
limit | Max experiments to return. |
POST /api/v1/experiments
Register experiment YAML without running it; each push stores a content-addressed version. By default the body is the raw YAML, not JSON:
curl -X POST \
-H "Authorization: Bearer $AX_API_KEY" \
-H "Content-Type: text/yaml" \
--data-binary @experiment.yaml \
"https://app.514.ax/api/v1/experiments"An experiment that declares required dependencies (files:, setup.files,
setup.skills, or tests[*].files entries with a source) needs its
complete package to become a runnable version — ax experiment push resolves
and uploads those files for you. A raw-YAML push of the same experiment with
nothing else attached is only accepted when it matches the YAML of an
existing stored version (an idempotent re-push); otherwise it is rejected with
400 invalid_argument rather than registering an incomplete version.
A caller that has already staged each dependency's content-addressed blob for
this org can send the package alongside the YAML with a JSON body instead —
send Content-Type: application/json:
curl -X POST \
-H "Authorization: Bearer $AX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"experiment_yaml": "...",
"replace_version_files": true,
"version_files": [
{
"dest": "inputs/data.txt",
"tar_sha256": "<64-hex sha256 of the staged blob>",
"size_bytes": 1234,
"source": "data.txt"
}
]
}' \
"https://app.514.ax/api/v1/experiments"| JSON body field | What it does |
|---|---|
experiment_yaml | Verbatim experiment YAML (required with a JSON body). |
replace_version_files | true to replace the version's dependency manifest with version_files, including an empty list. Required whenever version_files is non-empty. |
version_files | The dependency entries for this version: dest, tar_sha256, size_bytes, source, and optionally variant_id, test_names, name. |
Responds with the pushed experiment version metadata. Empty bodies return
400 invalid_argument.
GET /api/v1/experiments/{id}
Inspect one registered experiment: metadata, resolved axes, and
resources. URL-encode {id} if it contains reserved
characters. Each resource is {id, title, url, createdAt, updatedAt}. An
experiment with none returns "resources": []. Resources are not in the YAML
download.
curl -H "Authorization: Bearer $AX_API_KEY" \
"https://app.514.ax/api/v1/experiments/my-first-experiment"POST /api/v1/experiments/{id}/resources
Attach one titled link, or several in one request. Title and URL are both
required. Same title or same URL twice is allowed. URL-encode {id} if it
contains reserved characters.
curl -X POST \
-H "Authorization: Bearer $AX_API_KEY" \
-H "Content-Type: application/json" \
-d '{"title":"Experiment plan","url":"https://docs.google.com/document/d/abc"}' \
"https://app.514.ax/api/v1/experiments/my-first-experiment/resources"JSON body: a single {title, url}, or { "resources": [ ... ] } for several.
Send one shape, not both.
| Body field | What it does |
|---|---|
title | Required with url. Display title, 1 to 200 characters after trim. |
url | Required with title. http:// or https:// URL, max 2048 characters. |
resources | Array of {title, url}. Instead of top-level title / url, not alongside them. |
Responds 201 with { "resources": [ ... ] } (the created rows). Sending both
shapes, a non-http URL, or a missing title returns 400 invalid_argument.
Unknown experiment: 404 not_found.
PATCH /api/v1/experiments/{id}/resources/{resourceId}
Change a resource's title, URL, or both. At least one field is required.
Copy {resourceId} (a UUID) from GET /experiments/{id}.
curl -X PATCH \
-H "Authorization: Bearer $AX_API_KEY" \
-H "Content-Type: application/json" \
-d '{"title":"v2 experiment plan"}' \
"https://app.514.ax/api/v1/experiments/my-first-experiment/resources/11111111-1111-4111-8111-111111111111"| Body field | What it does |
|---|---|
title | New title. Same length rules as create. |
url | New URL. Same http(s) rules as create. |
Responds with the updated resource and previousTitle. Unknown experiment
or resource: 404 not_found. Empty body: 400 invalid_argument.
DELETE /api/v1/experiments/{id}/resources/{resourceId}
Remove a resource. A missing resource id is 404 not_found, not a silent
success. Responds 204.
curl -X DELETE \
-H "Authorization: Bearer $AX_API_KEY" \
"https://app.514.ax/api/v1/experiments/my-first-experiment/resources/11111111-1111-4111-8111-111111111111"GET /api/v1/experiments/{id}/versions
List stored versions, last-pushed first. Each version carries fingerprint,
timestamps, creator, runCount, and a first-created ordinal (the vN the
CLI shows; re-pushing an older definition keeps its ordinal).
curl -H "Authorization: Bearer $AX_API_KEY" \
"https://app.514.ax/api/v1/experiments/my-first-experiment/versions"GET /api/v1/experiments/{id}/yaml
Download the registered YAML.
curl -H "Authorization: Bearer $AX_API_KEY" \
"https://app.514.ax/api/v1/experiments/my-first-experiment/yaml"| Parameter | What it does |
|---|---|
version | Version fingerprint to pull. Omit for the latest. |
POST /api/v1/experiments/{id}/fork
Copy a registered experiment onto the current data pipeline as a new
experiment. Responds 201 with the new experiment's experimentId, name,
experimentFingerprint, url, and dataSchema (session_data_v1), plus the
sourceExperimentId and sourceExperimentFingerprint it was copied from. No
runs are created.
curl -X POST \
-H "Authorization: Bearer $AX_API_KEY" \
-H "Content-Type: application/json" \
-d '{"newExperimentId": "my-first-experiment-2026", "newName": "My first experiment (2026)"}' \
"https://app.514.ax/api/v1/experiments/my-first-experiment/fork"The JSON body is optional.
| Field | What it does |
|---|---|
newExperimentId | Kebab-case id for the new experiment. Omit to derive <id>-v2 (a counter is appended when that id is taken). An id that already exists responds 409. |
newName | Display name for the new experiment. Omit to derive the source name followed by (V2). |
The fork registers the source's latest stored version (YAML with only id and
name rewritten, plus its dependencies) under the new id. The source
experiment is only read: its runs, versions, and results are unchanged. Use it
when an experiment's results are held in the retired data pipeline and you
want to run the same definition again, then submit runs against the new id.
POST /api/v1/experiments/{id}/runs
Submit a run for a registered experiment. Returns 202 Accepted with the
submit receipt (runRequestId, pinned version fingerprint and ordinal,
variantCount, selected variantIds); run failures surface asynchronously on
the request. Poll GET /api/v1/runs/{id} to
watch it; there is no watch stream.
curl -X POST \
-H "Authorization: Bearer $AX_API_KEY" \
-H "Content-Type: application/json" \
-d '{"repeat":3,"variantIds":["claude::baseline"]}' \
"https://app.514.ax/api/v1/experiments/my-first-experiment/runs"JSON body (optional):
| Body field | What it does |
|---|---|
version | Version fingerprint to run. Omit for the latest. |
repeat | Repeat count per variant (0 means 1). |
variantIds | Exact variant ids to run. |
promptIds / agents / models / productIds / environmentIds | Axis selectors. Selectors are a union: a variant runs when it matches any of them. |
flags | Registered run flags. |
mock | Boolean; deterministic mock driver, no model spend. |
Snake_case spellings (variant_ids, prompt_ids, ...) are also accepted.
Unknown selectors, unknown versions, or a selection matching no variants
return 400 invalid_argument. Registered experiments that declare files: use
the dependencies stored by ax experiment push; the HTTP route does not upload
new local files.
POST /api/v1/experiments/{id}/query
Query results in experiment scope: SQL, or the experimental flag-based modeled
query. The HTTP mirror of
ax experiment query.
curl -X POST \
-H "Authorization: Bearer $AX_API_KEY" \
-H "Content-Type: application/json" \
-d '{"sql":"SELECT test_name, exit_code FROM test_analysis WHERE exit_code != 0"}' \
"https://app.514.ax/api/v1/experiments/my-first-experiment/query"JSON body:
| Body field | What it does |
|---|---|
sql | Raw ClickHouse SELECT; the server applies the experiment scope. Cannot be combined with the modeled fields below. |
limit | Row limit, up to 10,000 (0 uses the default of 100). |
dryRun | Validate and return normalized SQL plus an estimate without executing. Not with saveInsight. |
includeTestAware | Boolean; count runs classified as "test aware" (the agent noticed it was being tested), which both query modes leave out by default. Those runs may not mimic real-world agent behavior. |
saveInsight | Save the query as an insight: {heading, slug?, description?, labels?, force?}. |
Experimental modeled fields (no sql; the surface may change):
| Body field (experimental) | What it does |
|---|---|
metrics | testPassRate, testsPassed, testsFailed, cost, wallClockTime, tokens, toolCalls, toolFailures. Default testPassRate. |
groupBy | Ordered array of variant, agent, model, product, environment, prompt, test. Default variant. |
stat | Aggregate statistic. Only mean is supported today (the default). |
agents / products / environments / prompts | Dimension filters. Same axis ORs; different axes AND. |
experimentVersion | latest (default) or a version fingerprint. |
discoverDimension | "" lists all dimensions and metrics; a dimension name lists its observed values. Not with dryRun or saveInsight. |
The CLI-style aliases version, dimensions, and save are also accepted;
include_test_aware works as well as includeTestAware. Responds with query
rows, projection-order columns, compiled SQL, and (when requested) the dry-run
estimate, discovered dimensions, or saved-insight receipt. A modeled response
also carries excludedTestAwareRuns, the number of runs the default exclusion
held back.