Experiments

HTTP API routes for experiments.

RouteUseCLI equivalent
GET /experimentsList registered experiments.ax experiment list --json
POST /experimentsRegister YAML without running it.ax experiment push
GET /experiments/{id}Inspect one experiment.ax experiment view --json
POST /experiments/{id}/resourcesAttach 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}/versionsList stored versions.ax experiment versions --json
GET /experiments/{id}/yamlDownload registered YAML.ax experiment pull
POST /experiments/{id}/forkCopy an experiment onto the current data pipeline.ax experiment fork
POST /experiments/{id}/runsSubmit a run.ax experiment run <id>
POST /experiments/{id}/queryQuery 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"
ParameterWhat it does
searchCase-insensitive match on experiment name or id.
ownerOwner email or id filter.
agent / model / product / environmentDimension filters (repeatable or comma-separated). Same axis ORs; different axes AND.
sinceRFC 3339 updated-at floor (updated_since is an alias).
limitMax 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 fieldWhat it does
experiment_yamlVerbatim experiment YAML (required with a JSON body).
replace_version_filestrue to replace the version's dependency manifest with version_files, including an empty list. Required whenever version_files is non-empty.
version_filesThe 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 fieldWhat it does
titleRequired with url. Display title, 1 to 200 characters after trim.
urlRequired with title. http:// or https:// URL, max 2048 characters.
resourcesArray 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 fieldWhat it does
titleNew title. Same length rules as create.
urlNew 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"
ParameterWhat it does
versionVersion 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.

FieldWhat it does
newExperimentIdKebab-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.
newNameDisplay 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 fieldWhat it does
versionVersion fingerprint to run. Omit for the latest.
repeatRepeat count per variant (0 means 1).
variantIdsExact variant ids to run.
promptIds / agents / models / productIds / environmentIdsAxis selectors. Selectors are a union: a variant runs when it matches any of them.
flagsRegistered run flags.
mockBoolean; 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 fieldWhat it does
sqlRaw ClickHouse SELECT; the server applies the experiment scope. Cannot be combined with the modeled fields below.
limitRow limit, up to 10,000 (0 uses the default of 100).
dryRunValidate and return normalized SQL plus an estimate without executing. Not with saveInsight.
includeTestAwareBoolean; 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.
saveInsightSave 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
metricstestPassRate, testsPassed, testsFailed, cost, wallClockTime, tokens, toolCalls, toolFailures. Default testPassRate.
groupByOrdered array of variant, agent, model, product, environment, prompt, test. Default variant.
statAggregate statistic. Only mean is supported today (the default).
agents / products / environments / promptsDimension filters. Same axis ORs; different axes AND.
experimentVersionlatest (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.