Experiments
Track model training: experiments, the runs in them, and what each run logs (params, every metric point, artifact files of any size). A run's logged model can be registered in the Model Registry.
All endpoints require authentication via the X-API-Key header and the appropriate scope. Inside a Strongly workspace or job no key is needed: the pod's auth-proxy signs the call in as its owner.
The Python SDK wraps these endpoints, and strongly.start_run gives the MLflow-style workflow over them (see Experiment Tracking).
Experiments and runs
Experiments and runs are the same kind of record:
- an experiment is a top-level record: it groups runs and does not run itself;
- a run is a record whose
parentRunIdis its experiment'srunId; - a nested run has its parent run's
runIdasparentRunId.
Every endpoint takes the record's _id. runId is the record's own run id, which its runs reference.
Run object
{
"_id": "Ad5bKJhy3Lr8Dfq2Z",
"experimentId": "Ad5bKJhy3Lr8Dfq2Z",
"runId": "exp_1791351763762",
"parentRunId": "exp_1791350608259",
"name": "rf-depth-8",
"description": "Random forest, depth 8",
"status": "completed",
"owner": "fJHgtbiLZyMkFqJC6",
"organizationId": "org-strongly-demo",
"tags": ["baseline"],
"pinned": false,
"params": { "max_depth": 8, "optimizer.lr": 0.01 },
"latestMetrics": {
"accuracy": { "value": 0.933, "step": 0, "timestamp": "2026-10-07T05:41:03.112Z" },
"val/accuracy": { "value": 0.933, "step": 80, "timestamp": "2026-10-07T05:41:02.870Z" }
},
"artifacts": [
{
"name": "model.joblib",
"path": "model/model.joblib",
"type": "model",
"size": 380505,
"contentType": "application/octet-stream",
"createdAt": "2026-10-07T05:41:04.010Z"
}
],
"registeredModels": [
{ "modelId": "ffwNZkidYBkRrycof", "version": 1, "artifactPath": "model", "registeredAt": "2026-10-07T05:41:06.220Z" }
],
"systemInfo": { "pythonVersion": "3.11.14", "platform": "Linux-6.1-x86_64", "cpuCount": 4, "memoryTotalGb": 15.4, "gpuCount": 1, "gpus": [{ "name": "NVIDIA L4", "memoryMb": 23034, "driver": "550.90" }] },
"gitInfo": { "commit": "9e4066697c1d…", "branch": "main", "repo": "git@github.com:acme/churn.git", "dirty": false },
"createdAt": "2026-10-07T05:40:58.204Z",
"updatedAt": "2026-10-07T05:41:06.220Z",
"startedAt": "2026-10-07T05:40:58.204Z",
"finishedAt": "2026-10-07T05:41:03.250Z",
"durationSeconds": 5
}
| Field | Description |
|---|---|
status | pending, running, completed, failed or cancelled. A record created running has startedAt; ending it (completed, failed, cancelled) sets finishedAt and durationSeconds. |
params | Each param as logged: a string, number or boolean. |
latestMetrics | Each metric's value at the highest step logged. Every point is kept: read them with GET /api/v1/mlops/experiments/:experimentId/metrics/:key. |
artifacts | The files recorded on the run. path is relative to the run's artifacts. |
registeredModels | The Model Registry versions registered from this run (POST /api/v1/mlops/experiments/:id/models). |
systemInfo, gitInfo | The machine the run ran on, and the git commit its code came from (the SDK sends both). |
experimentId | The record's _id (returned by create and register). |
Metric and param keys are 1 to 250 letters, digits, _, -, ., / or spaces, and are returned as logged.
GET /api/v1/mlops/experiments
List the experiments you can use, or the runs of one.
Scope: ml-workbench:read
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
parentRunId | string | No | List the runs of the experiment (or run) with this runId. Without it, the top-level experiments are listed |
q | string | No | Search by name or description |
status | string | No | Filter by status |
tag | string | No | Filter by tag |
pinned | boolean | No | true lists only pinned experiments, false only unpinned ones |
limit | integer | No | Results per page (default 50, max 200) |
cursor | string | No | meta.nextCursor of the previous page; omit for the first page |
sort | string | No | -created_at (default, newest first), createdAt, name, status; comma-separated for more than one |
Response 200 OK
{
"data": [ { "_id": "Ad5bKJhy3Lr8Dfq2Z", "name": "rf-depth-8", "status": "completed", "parentRunId": "exp_1791350608259" } ],
"meta": { "total": 2, "limit": 50, "nextCursor": null, "requestId": "req_abc123" }
}
POST /api/v1/mlops/experiments
Create an experiment, or a run of one.
Scope: ml-workbench:write
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | The experiment's or run's name |
parentRunId | string | No | For a run: its experiment's (or parent run's) runId. You need edit access to it; the run is shared as it is |
status | string | No | pending (default) or running (records startedAt) |
description | string | No | What it is |
tags | string[] | No | Tags |
params | object | No | Params to log: {key: value} |
metrics | array | No | Metric points to log: [{key, value, step?, timestamp?}] |
systemInfo | object | No | The machine it runs on |
gitInfo | object | No | {commit, branch, repo, dirty}: where its code came from |
runId | string | No | Its own run id (generated when omitted). Creating with an existing runId of yours updates that record |
Response 201 Created: the run object.
curl -X POST https://your-instance.strongly.ai/api/v1/mlops/experiments \
-H "X-API-Key: $STRONGLY_API_KEY" -H "Content-Type: application/json" \
-d '{"name": "rf-depth-8", "parentRunId": "exp_1791350608259", "status": "running", "params": {"max_depth": 8}}'
POST /api/v1/mlops/experiments/register
Your experiment with this name, created if you have none. Only top-level experiments match.
Scope: ml-workbench:write
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Experiment name |
description | string | No | Its description, when it is created |
tags | string[] | No | Its tags, when it is created |
Response 200 OK (found) or 201 Created: the experiment object, with exists (true when it was found).
GET /api/v1/mlops/experiments/:id
Get an experiment or run.
Scope: ml-workbench:read
Response 200 OK: the run object.
PATCH /api/v1/mlops/experiments/:id
Update an experiment or run. Ending a run (completed, failed or cancelled) records when it finished and how long it ran.
Scope: ml-workbench:write
Request Body
| Field | Type | Description |
|---|---|---|
name | string | New name |
description | string | New description |
status | string | pending, running, completed, failed or cancelled |
tags | string[] | The tags (replaces them) |
framework | string | The framework of the model the run trained, e.g. sklearn (the SDK's log_model sets it) |
params | object | Params to log |
metrics | array | Metric points to log |
The owner, organization and sharing of a run are never changed by an update.
Response 200 OK: the run object as it now is.
DELETE /api/v1/mlops/experiments/:id
Delete an experiment or run, with its runs, every metric point they logged and their artifact files. Models registered from a run stay in the registry.
Scope: ml-workbench:write
Response 204 No Content
GET /api/v1/mlops/experiments/compare
Compare runs: their params and latest metrics side by side.
Scope: ml-workbench:read
Query Parameters: ids (required): comma-separated _ids, at least 2.
Response 200 OK
{
"data": {
"runIds": ["Ad5bKJhy3Lr8Dfq2Z", "Bk2sLm9QpR4tUv6Wx"],
"runs": [ /* the run objects */ ],
"diffParameters": { "max_depth": { "Ad5bKJhy3Lr8Dfq2Z": 8, "Bk2sLm9QpR4tUv6Wx": 4 } },
"commonParameters": { "n_estimators": 80 },
"metricComparison": { "accuracy": { "Ad5bKJhy3Lr8Dfq2Z": 0.933, "Bk2sLm9QpR4tUv6Wx": 0.927 } }
}
}
A metric a run did not log is null in metricComparison, never 0.
GET /api/v1/mlops/experiments/stats
Your experiments, and their runs by status.
Scope: ml-workbench:read
Response 200 OK
{
"data": { "total": 12, "pinned": 2, "running": 1, "completed": 40, "failed": 3, "bestAccuracy": 0.951, "averageAccuracy": 0.902 }
}
total and pinned count experiments. running, completed and failed count everything that runs (runs, nested runs, AutoML runs). The accuracies are over the runs' latest accuracy metric, null when no run logged one.
Sharing
Who can reach a experiment is the platform's one sharing shape: its owner, its members
(role editor can use and change it, user can only use it) and its visibility
(public: every user can find and use it; in a multi-tenant deployment, every user of
its organization). Changing it stays with its owner and editors.
| Method | Path | Does | Scope |
|---|---|---|---|
| GET | /api/v1/mlops/experiments/:id/permissions | Owner, members (userId, role) and visibility | ml-workbench:read |
| POST | /api/v1/mlops/experiments/:id/permissions/members | Share with a user: { "userId", "role": "editor" | "user" } | ml-workbench:write |
| DELETE | /api/v1/mlops/experiments/:experimentId/permissions/members/:userId | Stop sharing with a user | ml-workbench:write |
| PATCH | /api/v1/mlops/experiments/:id/permissions | { "visibility": "public" | "private" } | ml-workbench:write |
Each, except a member's removal (204), answers the permissions as they are now:
{
"data": {
"resourceId": "<experiment id>",
"owner": "<user id>",
"members": [{ "userId": "<user id>", "role": "user" }],
"visibility": "private"
},
"meta": { "requestId": "req_abc123" }
}
Logging
POST /api/v1/mlops/experiments/:id/metrics
Log metric points. Every point is kept; the run's latestMetrics holds each key's value at its highest step.
Scope: ml-workbench:write
Request Body
{
"metrics": [
{ "key": "loss", "value": 0.21, "step": 3 },
{ "key": "val/accuracy", "value": 0.91, "step": 3, "timestamp": "2026-10-07T05:41:02Z" }
]
}
value is a finite number; step a whole number, 0 or more (default 0); timestamp an ISO date or epoch milliseconds (default now).
Response 200 OK: {"logged": 2}
GET /api/v1/mlops/experiments/:id/metrics
The metric keys a run has logged.
Scope: ml-workbench:read
Response 200 OK: the keys, in one list: ["loss", "val/accuracy"]
GET /api/v1/mlops/experiments/:experimentId/metrics/:key
Every logged point of one metric, in step order. URL-encode the key (val%2Faccuracy).
Scope: ml-workbench:read
Response 200 OK
{ "data": { "key": "val/accuracy", "history": [ { "value": 0.88, "step": 1, "timestamp": "…" }, { "value": 0.91, "step": 3, "timestamp": "…" } ] } }
POST /api/v1/mlops/experiments/:id/params
Log params. A key logged again takes the new value.
Scope: ml-workbench:write
Request Body: {"params": {"learning_rate": 0.01, "max_depth": 6, "bagged": true}}
Response 200 OK: {"logged": 3}
Artifacts
A run's artifact files are uploaded straight to storage through presigned URLs, so their size is not limited by a request. The platform names every storage key from the run and the artifact's path (relative segments of letters, digits, _, -, . or spaces).
POST /api/v1/mlops/experiments/:id/artifacts/uploads
A URL to upload one file (up to 5GB) to, valid one hour. PUT the file's bytes to it, with the same Content-Type if you gave one.
Scope: ml-workbench:write
Request Body: {"path": "model/model.joblib", "contentType": "application/octet-stream"}
Response 201 Created: {"path": "model/model.joblib", "uploadUrl": "https://…", "expiresIn": 3600}
POST /api/v1/mlops/experiments/:id/artifacts/multipart-uploads
Start uploading a file over 5GB (up to 5TB) in parts.
Scope: ml-workbench:write
Request Body: {"path": "checkpoints/weights.bin", "size": 21474836480, "contentType": "application/octet-stream"}
Response 201 Created
{ "data": { "path": "checkpoints/weights.bin", "uploadId": "…", "partSize": 67108864, "parts": [ { "partNumber": 1, "url": "https://…" } ], "expiresIn": 21600 }, "meta": { "requestId": "…" } }
PUT each partSize slice of the file (the last one shorter) to its part's URL and keep each response's ETag header.
POST /api/v1/mlops/experiments/:experimentId/artifacts/multipart-uploads/:id/complete
Join the uploaded parts into the file (:id is the uploadId).
Request Body: {"path": "checkpoints/weights.bin", "parts": [{"partNumber": 1, "etag": "\"9b2c…\""}]}
Response 200 OK: {"path": "checkpoints/weights.bin"}
POST /api/v1/mlops/experiments/:experimentId/artifacts/multipart-uploads/:id/abort
Give up a multipart upload; the parts already uploaded are dropped.
Request Body: {"path": "checkpoints/weights.bin"}
POST /api/v1/mlops/experiments/:id/artifacts
Record an uploaded file on the run. Its size and type are read from storage; a path recorded again replaces its entry. A path with no uploaded file is refused (404).
Scope: ml-workbench:write
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
path | string | Yes | The path the file was uploaded at |
type | string | No | file (default), model, dataset or figure |
name | string | No | Display name (default: the file name) |
description | string | No | What it is |
Response 201 Created: the artifact entry.
GET /api/v1/mlops/experiments/:id/artifacts
Scope: ml-workbench:read
Response 200 OK: {"artifacts": [ … ]}
GET /api/v1/mlops/experiments/:id/artifacts/download-url
A URL (valid one hour) to download a recorded artifact.
Scope: ml-workbench:read
Query Parameters: path (required).
Response 200 OK: {"path": "model/model.joblib", "url": "https://…", "expiresIn": 3600}. A path not recorded on the run is refused (404).
Registering a run's model
POST /api/v1/mlops/experiments/:id/models
Register the model a run logged in the Model Registry: a new model, or a new version of one you own. The SDK's log_model writes the model file and MLmodel.json (its framework, file, feature names, target and task) under a folder of the run's artifacts. The task (problem_type) becomes the model's training.problemType; one that is not a registry task type (classification, regression, multiclass, multilabel, timeseries, other) is refused. A model logged without one is registered without a task: set it with PATCH /api/v1/mlops/models/:id problemType before monitoring its drift.
The model file is copied into the registry, so deleting the run leaves the model. The run becomes the version's training record: its params as hyperparameters, its metrics named accuracy, precision, recall, f1, auc, logloss, rmse, mae, mse, r2 or mape on the model card, and its machine, start and end. scikit-learn, XGBoost and LightGBM models need their feature names; PyTorch and pickled models register too.
Scope: ml-workbench:write
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
name | string | One of | The name of a new registry model |
modelId | string | One of | A registry model to add a version to |
artifactPath | string | No | The folder log_model wrote to (default model) |
description | string | No | What the model (or version) is |
Response 201 Created: {"modelId": "ffwNZkidYBkRrycof", "version": 1}
| Error | When |
|---|---|
400 | Neither or both of name and modelId; a tabular model with no feature names; a Keras model (not a single servable file) |
404 | No MLmodel.json at artifactPath on the run, or a modelId you do not own |
409 | You already have a model with this name |