For the complete documentation index, see llms.txt. This page is also available as Markdown.

Running Simulations over REST

Every simulation the platform can run is reachable from one endpoint. The New Simulation wizard, the Python SDK, a connected assistant, and your own code all submit the same thing: a twin, a version, and a scenario object describing the question.

This page is the scenario reference. For the endpoints themselves, the Interactive API Reference (/api/v1/docs on your platform domain) has request and response schemas and a try-it console.

The one endpoint

POST /api/v1/workspaces/{workspaceId}/simulations
Authorization: Bearer pk_...
Content-Type: application/json
{
  "digitalTwinId": "G3TiknGOFCXUoM6E9lvt6",
  "digitalTwinVersionId": "guyKVNbpp9TPkHgYmaNan",
  "scenario": { "type": "prediction", "...": "..." }
}
Field
Required
Meaning

digitalTwinId

yes

The twin to simulate

digitalTwinVersionId

yes

The trained version to run against

scenario

yes

The question, in the shape its family expects

environmentGroupIds

no

Multi-environment twins: saved groups to narrow the run to

webhookUrl

no

Called when the run finishes, instead of polling

The key needs the simulations:execute scope to submit and simulations:read to read results back. GET /simulations/{id}/export/{format} is the exception: it needs exports:read, for both formats.

The discriminator is type, not scenarioType. Inside the scenario object the field naming the family is type. scenarioType is the filter on GET /simulations, and naming it inside a scenario is refused by name with a 422 — as is carrying both spellings, or naming a family that does not exist.

The run loop

Simulations are asynchronous. The submit returns 202 immediately with a run id and the links to follow:

Poll links.self until status is terminal, then read links.results:

  • In flight: pending, configuring, configured, running

  • Terminal: completed, failed, cancelled

GET .../results is 404 while a run is unfinished, so poll the run rather than the results. Supply webhookUrl to be called instead.

Results come back under the version label the run used:

For a family that answers per variable (prediction and forecast), the records under each variable are positionally aligned with the input you sent: the first record answers for the first input row, and so on. Nothing in the payload repeats the input, so keep your own ordering.

Which type to send

The same question is spelled differently depending on what kind of twin you are asking. Sending the wrong spelling is a rejection, not a wrong answer, so read the row for your twin's kind. A twin's kind is on GET /api/v1/workspaces/{workspaceId}/digital-twins/{id} as type.

Question
static
temporal
multi-environment static
multi-environment temporal

Prediction

prediction

—

prediction

—

Forecast

—

forecast

—

panel_forecast

Intervention

intervention

temporal_intervention

panel_intervention

panel_intervention

Optimization

optimisation

temporal_optimisation

panel_optimisation

panel_optimisation

Best Action

counterfactual

temporal_counterfactual

panel_counterfactual

panel_counterfactual

Explanation

explanation

temporal_explanation

panel_explanation

panel_explanation

Root Cause Analysis

root_cause_analysis

temporal_root_cause_analysis

static_panel_root_cause_analysis

panel_root_cause_analysis

Anomaly Scan & Diagnosis

anomaly_detection

temporal_anomaly_detection

static_panel_anomaly_detection

panel_anomaly_detection

Causal Health Monitor

—

causal_health_monitor

—

panel_causal_health_monitor

Note the British spelling of optimisation: the field values are the engine's vocabulary, not display labels.

Every panel_* scenario also takes environments (a list of environment keys; omit for all of them), and every panel scenario can be narrowed instead by environmentGroupIds on the request body, which records on the run exactly what the group resolved to.

Prediction

What is the most likely outcome for this specific case? One answer per input record, with an uncertainty interval around each. Static twins only: a temporal twin projects forward with a forecast instead.

Field
Required
Meaning

sample

either

Input records, keyed by variable name. Leave the targets out: they are what the model answers with

sampleFrom

either

{"datasetId", "limit"}: read the records from a dataset on the server instead of sending them inline. limit is at most 10000, which is also its default. Each row keeps its id column, so an answer can be traced back to the record it came from

targetVars

yes

The variables to predict

confidenceLevel

no

Interval coverage, default 0.95

sample here is singular. The families that take baseline or observed data (best action, root cause, anomaly scan) use samples, plural. Sending the wrong one leaves the field unset and the run fails validation.

A continuous target answers with prediction, std, lowerBound and upperBound. A categorical target answers with the most likely class in prediction plus a probabilities array over the classes, and the bounds become arrays aligned with it.

A completed prediction of a categorical target reads back as a ranked worklist on GET /simulations/{id}/predictions: the records ordered by the model's chance of one value, lowest first, so the ones most at risk come first. targetVariable is required; value picks the value the chance is read for (default: the variable's first category) and below sets the line, above 0 and at most 1, defaulting to 0.5 — the model's own line between outcomes. Each row carries its 0-based row position in the run, its id-column label, the predicted value and the chance. data.recordCount is how many records the run scored and data.belowCount how many sit under the line; the pages walk the records below the line, counted by pagination.total. limit is 1-500, default 50. The run id then goes to scenario.samplesFrom.runId on a Best Action run, or to fromRunId on the scoring endpoint, to act on those records. The call is 404 when the run is not a prediction in this workspace, 400 while it is still running, and 422 when the variable or value is not one it predicted.

Forecast

Project variables forward in time. Temporal twins only.

originTimestamp (ms epoch) anchors the forecast start, which is how a backtest aligns a forecast against data the model never saw. On panel_forecast, environments narrows the run. Every panel forecast carries a combined series across environments, averaged by default; aggregateMode (sum, avg, min, max) changes which statistic it uses.

Intervention

What happens to my outcomes if I change something? Interventions measure their effect through metrics, which are SQL over the sampled frame, registered under the table names df, data, and dataset.

valueSpec is a required wrapper: {"variable": "price", "value": 12.5} is not valid. The types are:

valueSpec.type

Effect

set_value

Pin the variable to value

relative_change

Shift it, with mode of percentage or absolute

set_probability

Set a category to a probability

adjust_probability

Shift a category by delta percentage points

set_members

Set-valued columns: include, exclude, size

range

Sweep instead of pinning: from, to, steps

conditions scope an intervention to a subpopulation. Operators are eq, ne, gt, lt, ge, le, in, not_in.

At most one range per scenario, on a numeric variable. The run then evaluates the metric across the grid, and GET /simulations/{id}/sweep returns the curve.

On a temporal or panel twin each intervention also carries timing: timestamp (ms epoch, defaulting to the first forecast step), persistent, and durationSteps.

Explanation

Why does this happen? The explanationMode decides which variables you have to name:

explanationMode

Question

Needs

discovery

What drives this variable?

effectVariable

impact

What does this variable go on to affect?

causeVariable

directional

How does one reach the other?

both

The answer ranks the drivers with effect sizes and confidence intervals, gives a dose-response curve for numeric causes, and splits total effect into direct and indirect pathways. Optional segments, each {"id", "description", "conditions"}, repeat the explanation per subgroup so you can see where a driver bites hardest.

Optimization

What should I change? Objectives say what to move and which way; decisionVars are the only levers the optimizer may touch.

direction is maximise or minimise; weight trades several objectives off against each other. metricConstraints add guardrails every plan must respect: {"metricName", "sqlQuery", "constraintType", "value"} with a constraintType of min_value, max_value, max_decrease_percent, max_increase_percent, or fixed_boolean.

temporal_optimisation requires forecastHorizon, and takes interventionTiming to control when each lever is allowed to move.

A categorical outcome has to be counted, not averaged. SELECT AVG("Churn") over a text column is not a number: the run is accepted and then fails inside the engine. Count the category you care about with CASE WHEN, as above.

Best Action

How do I reach a goal? For every baseline record, the solver finds the smallest set of changes that reaches the targets.

A categorical target also takes minProbability, above 0 and at most 1: the chance of value an option has to reach before it counts as meeting the target. Leave it out and value only has to be the most likely outcome. Samples the model already puts above that line are not searched at all and come back with baselineAlreadyMeetsTarget: true and no counterfactuals, so an empty list there means nothing needs changing rather than nothing works. minProbability on a numeric target is rejected before the run starts.

A numeric target also takes matchMode (tolerance, orMore, orLess) and, for tolerance, a toleranceValue. Variables with no fixed constraint are eligible to change.

recommendBy is fewestChanges (the default) or strongestResult, and sets how far the search goes: fewestChanges stops one size past the smallest set of changes that reaches the target, strongestResult covers every size up to maxChanges.

Instead of samples, a static Best Action run can take its records from a completed prediction on the same twin with samplesFrom:

The server resolves the rows before the run starts, so the same rows reuse one run. targetVariable and value say which predicted variable and value rank the records — lowest chance first — and default to the first target's. below is the line: records whose chance of value falls under it are the ones searched, defaulting to that target's minProbability and then to 0.5. rows names 0-based row positions in the prediction run instead, at most 500, and makes below irrelevant. limit caps how many records are kept, at most 500, which is also its default, and the most at risk are kept first. The run records the prediction it came from as sourceRunId.

The run is refused, with the reason named, when the run id is not a completed prediction of this twin in this workspace, when the ranked variable is neither one of the targets nor accompanied by samplesFrom.value, when a row position is outside the run, or when no record sits below the line — the last of which means nothing needs changing rather than that the request was wrong.

There is a second route to the same engine for scoring a batch of rows against targets, with paged verdicts and row labels: POST /api/v1/workspaces/{workspaceId}/digital-twins/{id}/versions/{vId}/score, read back with GET /simulations/{runId}/score. It takes the same two sources: rows inline, or fromRunId naming a completed prediction on this twin, whose records below the line are scored — below sets that line, defaulting to the first target's minProbability and then to 0.5, and fromRows picks exact 0-based rows of the prediction instead. Send one or the other, not both. Either way at most 500 records are scored, the most at risk first.

Root Cause Analysis

Something is wrong and you know which variable. Trace it upstream through the causal graph.

samples is required: the analysis diagnoses observations you supply, not the training data. targetFpr is the detection sensitivity as a false-positive rate between 0.0001 and 0.1; lower flags less. Temporal variants take anomalyTimestep to pin the step being diagnosed.

Anomaly Scan and Diagnosis

Something is wrong and you do not know what. Every variable is scanned, and whatever is flagged is diagnosed.

Temporal variants take a startStep/endStep window and autoRcaTopK (how many flagged variables to diagnose) in place of autoRca.

Panel variants can either share one samples list across every environment, or give each environment its own with panelSamples:

Reading a run back

Endpoint
Returns

GET /simulations

The workspace's runs, newest first. Filter with status, scenarioType, digitalTwinId

GET /simulations/{id}

The run: status, timings, and the scenario it ran

GET /simulations/{id}/results

The outputs

GET /simulations/{id}/sweep

The dose-response curve, for a run with a range intervention

GET /simulations/{id}/predictions

Paged records ranked by their chance of one value, lowest first, for a completed prediction of a categorical target

GET /simulations/{id}/score

Paged per-row verdicts, for a scoring run

GET /simulations/{id}/export/{format}

json for any completed run; csv for a completed forecast, panel_forecast or prediction run against a multi-environment (panel) model

GET /simulations/{id} reports the formats a run will accept.

Letting the platform write the scenario

If you would rather describe the question than build the object, POST /api/v1/workspaces/{workspaceId}/digital-twins/{id}/versions/{vId}/scenario-from-query takes {"query": "..."} in plain English and returns a scenario ready to submit. It is the same translator the wizard's Generate button uses, and reading what it returns is a quick way to learn a shape.

A connected assistant reports this as query_digital_twin while it works. That is an MCP tool name, not a REST endpoint: there is nothing at /api/v1/.../query_digital_twin. The assistant is calling this same simulations endpoint on your behalf.

Next steps

Last updated