> For the complete documentation index, see [llms.txt](https://docs.rootcause.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.rootcause.ai/api-and-integrations/rest-running-simulations.md).

# 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](https://docs.rootcause.ai) (`/api/v1/docs` on your platform domain) has request and response schemas and a try-it console.

## The one endpoint

```http
POST /api/v1/workspaces/{workspaceId}/simulations
Authorization: Bearer pk_...
Content-Type: application/json
```

```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:

```json
{
  "data": {
    "runId": "VUFGM4KXpvFQQQGyhBpUR",
    "status": "pending",
    "links": {
      "self": "/api/v1/workspaces/{workspaceId}/simulations/VUFGM4KXpvFQQQGyhBpUR",
      "results": "/api/v1/workspaces/{workspaceId}/simulations/VUFGM4KXpvFQQQGyhBpUR/results"
    }
  }
}
```

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:

```json
{
  "data": {
    "1.0.0": {
      "results": {
        "Churn": [
          {
            "prediction": "No",
            "probabilities": [0.4225, 0.5775],
            "lowerBound": [0.3520, 0.5070],
            "upperBound": [0.4930, 0.6480],
            "confidenceLevel": 0.95
          }
        ]
      }
    }
  }
}
```

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.

```json
{
  "type": "prediction",
  "sample": [
    { "tenure": 3, "MonthlyCharges": 85.0, "Contract": "Month-to-month" },
    { "tenure": 40, "MonthlyCharges": 20.0, "Contract": "Two year" }
  ],
  "targetVars": ["Churn"],
  "confidenceLevel": 0.95
}
```

| Field             | Required | Meaning                                                                                            |
| ----------------- | -------- | -------------------------------------------------------------------------------------------------- |
| `sample`          | yes      | Input records, keyed by variable name. Leave the targets out: they are what the model answers with |
| `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.

## Forecast

Project variables forward in time. Temporal twins only.

```json
{
  "type": "forecast",
  "forecastH": 24,
  "targetVars": ["revenue"],
  "confidenceLevel": 0.95
}
```

`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`.

```json
{
  "type": "intervention",
  "interventions": [
    {
      "variable": "MonthlyCharges",
      "valueSpec": { "type": "relative_change", "mode": "percentage", "value": -10 },
      "conditions": [{ "variable": "Contract", "operator": "eq", "value": "Month-to-month" }]
    }
  ],
  "metrics": [
    {
      "name": "churn_share",
      "sqlQuery": "SELECT AVG(CASE WHEN Churn = 'Yes' THEN 1.0 ELSE 0.0 END) AS value FROM df",
      "unit": "percent_0_to_1",
      "higherIsBetter": false
    }
  ]
}
```

`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             |

```json
{
  "type": "explanation",
  "explanationMode": "discovery",
  "effectVariable": "Churn",
  "causeVariable": null
}
```

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.

```json
{
  "type": "optimisation",
  "objectives": [
    {
      "direction": "minimise",
      "variable": "Churn share",
      "metricSqlQuery": "SELECT AVG(CASE WHEN Churn = 'Yes' THEN 1.0 ELSE 0.0 END) AS value FROM df"
    }
  ],
  "decisionVars": ["Contract", "MonthlyCharges"],
  "variableConstraints": [
    { "variable": "MonthlyCharges", "type": "range", "minValue": 18.25, "maxValue": 118.75 },
    { "variable": "Contract", "type": "set", "allowedValues": ["Month-to-month", "One year", "Two year"] }
  ],
  "interventionCountConfig": { "maxInterventions": 2 }
}
```

`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.

```json
{
  "type": "counterfactual",
  "samples": [{ "tenure": 3, "MonthlyCharges": 92.0, "Contract": "Month-to-month" }],
  "targets": [{ "variable": "Churn", "value": "No" }],
  "maxChanges": 3,
  "constraints": { "tenure": { "type": "fixed" } }
}
```

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

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`.

## Root Cause Analysis

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

```json
{
  "type": "root_cause_analysis",
  "targetVariable": "Churn",
  "samples": [{ "tenure": 2, "MonthlyCharges": 105.0, "Churn": "Yes" }],
  "targetFpr": 0.005
}
```

`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.

```json
{
  "type": "anomaly_detection",
  "samples": [{ "tenure": 2, "MonthlyCharges": 105.0, "Churn": "Yes" }],
  "targetFpr": 0.005,
  "autoRca": true
}
```

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`:

```json
{
  "type": "panel_anomaly_detection",
  "panelSamples": { "uk": [{ "revenue": 400 }], "france": [{ "revenue": 120 }] }
}
```

## 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}/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 |

Each run in the listing carries `exportFormats`, so you never have to guess which formats it 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](/api-and-integrations/api-access/mcp-integration.md) 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](/api-and-integrations/sdk-getting-started/sdk-working-with-twins.md) has a verb per family, so none of these shapes have to be built by hand
* [Simulation Types](/more-details/digital-twin/simulation-types.md) explains what each family is for
* [API Access](/api-and-integrations/api-access.md) covers keys, scopes, and the interactive reference
