Skip to main content

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 parentRunId is its experiment's runId;
  • a nested run has its parent run's runId as parentRunId.

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
}
FieldDescription
statuspending, running, completed, failed or cancelled. A record created running has startedAt; ending it (completed, failed, cancelled) sets finishedAt and durationSeconds.
paramsEach param as logged: a string, number or boolean.
latestMetricsEach metric's value at the highest step logged. Every point is kept: read them with GET /api/v1/mlops/experiments/:experimentId/metrics/:key.
artifactsThe files recorded on the run. path is relative to the run's artifacts.
registeredModelsThe Model Registry versions registered from this run (POST /api/v1/mlops/experiments/:id/models).
systemInfo, gitInfoThe machine the run ran on, and the git commit its code came from (the SDK sends both).
experimentIdThe 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

ParameterTypeRequiredDescription
parentRunIdstringNoList the runs of the experiment (or run) with this runId. Without it, the top-level experiments are listed
qstringNoSearch by name or description
statusstringNoFilter by status
tagstringNoFilter by tag
pinnedbooleanNotrue lists only pinned experiments, false only unpinned ones
limitintegerNoResults per page (default 50, max 200)
cursorstringNometa.nextCursor of the previous page; omit for the first page
sortstringNo-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

FieldTypeRequiredDescription
namestringYesThe experiment's or run's name
parentRunIdstringNoFor a run: its experiment's (or parent run's) runId. You need edit access to it; the run is shared as it is
statusstringNopending (default) or running (records startedAt)
descriptionstringNoWhat it is
tagsstring[]NoTags
paramsobjectNoParams to log: {key: value}
metricsarrayNoMetric points to log: [{key, value, step?, timestamp?}]
systemInfoobjectNoThe machine it runs on
gitInfoobjectNo{commit, branch, repo, dirty}: where its code came from
runIdstringNoIts 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

FieldTypeRequiredDescription
namestringYesExperiment name
descriptionstringNoIts description, when it is created
tagsstring[]NoIts 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

FieldTypeDescription
namestringNew name
descriptionstringNew description
statusstringpending, running, completed, failed or cancelled
tagsstring[]The tags (replaces them)
frameworkstringThe framework of the model the run trained, e.g. sklearn (the SDK's log_model sets it)
paramsobjectParams to log
metricsarrayMetric 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.

MethodPathDoesScope
GET/api/v1/mlops/experiments/:id/permissionsOwner, members (userId, role) and visibilityml-workbench:read
POST/api/v1/mlops/experiments/:id/permissions/membersShare with a user: { "userId", "role": "editor" | "user" }ml-workbench:write
DELETE/api/v1/mlops/experiments/:experimentId/permissions/members/:userIdStop sharing with a userml-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

FieldTypeRequiredDescription
pathstringYesThe path the file was uploaded at
typestringNofile (default), model, dataset or figure
namestringNoDisplay name (default: the file name)
descriptionstringNoWhat 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

FieldTypeRequiredDescription
namestringOne ofThe name of a new registry model
modelIdstringOne ofA registry model to add a version to
artifactPathstringNoThe folder log_model wrote to (default model)
descriptionstringNoWhat the model (or version) is

Response 201 Created: {"modelId": "ffwNZkidYBkRrycof", "version": 1}

ErrorWhen
400Neither or both of name and modelId; a tabular model with no feature names; a Keras model (not a single servable file)
404No MLmodel.json at artifactPath on the run, or a modelId you do not own
409You already have a model with this name