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", "...": "..." }
}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, notscenarioType. Inside the scenario object the field naming the family istype.scenarioTypeis the filter onGET /simulations, and naming it inside a scenario is refused by name with a422— 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,runningTerminal:
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.
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.
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
samplehere is singular. The families that take baseline or observed data (best action, root cause, anomaly scan) usesamples, 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 withCASE 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
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_twinwhile 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
Python SDK: Working with Digital Twins has a verb per family, so none of these shapes have to be built by hand
Simulation Types explains what each family is for
API Access covers keys, scopes, and the interactive reference
Last updated

