> 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/sdk-getting-started/sdk-api-reference/twin.md).

# Twin

Digital twins: forecast, simulate, intervene, score, update.

## Twin

A digital twin handle bound to one version (the latest unless told otherwise).

### Properties

* **id** (`str`)
* **name** (`str`)
* **kind** (`str`)
* **is\_panel** (`bool`)
* **is\_temporal** (`bool`)
* **versions** (`list[dict[str, Any]]`)
* **version** (`dict[str, Any]`)
* **version\_id** (`str`)
* **source** (`Any`): The raw source backing this version, or None when it trains off a dataset.
* **graph** (`Graph`)
* **roles** (`dict[str, Any]`): The version's variable roles: which variables are sources and which are targets.
* **update\_eligibility** (`dict[str, Any]`): Whether update() would find new data, and whether it can assimilate incrementally.
* **environments** (`pd.DataFrame`): Panel twins: the environments in the version's data, with sample sizes.
* **groups** (`list[Group]`): The twin's saved environment groups — the same ones the platform's picker lists.

### Twin.at\_version

```python
Twin.at_version(version_id: str) -> Twin
```

*Undocumented; the signature above is the contract.*

### Twin.delete

```python
Twin.delete() -> None
```

Delete this twin permanently: models, versions, runs and record.

Running discovery or simulation workflows are cancelled first. There is no undo — the platform's own delete confirmation exists for a reason. Requires the `digital-twins:delete` scope.

### Twin.link

```python
Twin.link() -> Any
```

This twin version's page on the platform, as a clickable URL.

### Twin.discover

```python
Twin.discover(*, webhook_url: str | None = None, timeout: float = 3600.0) -> Graph
```

Run causal discovery on this version and return the discovered graph.

| Parameter     | Type          | Default  | Description                                          |
| ------------- | ------------- | -------- | ---------------------------------------------------- |
| `webhook_url` | `str \| None` | `None`   | Called with the job result instead of waiting on it. |
| `timeout`     | `float`       | `3600.0` | Seconds to wait for the discovery job.               |

**Returns** (`Graph`): The discovered [`Graph`](/api-and-integrations/sdk-getting-started/sdk-api-reference/graph.md#graph).

### Twin.train

```python
Twin.train(*, webhook_url: str | None = None, timeout: float = 7200.0) -> Twin
```

Train the model for this version, blocking until done.

An already-trained version is returned as-is: the platform's lifecycle retrains through a new version, not by re-fitting in place. To rebuild a model from scratch (after an engine fix, or a corrupt artifact), use rc.discover(df, force=True) and train the fresh twin it returns.

| Parameter     | Type          | Default  | Description                                          |
| ------------- | ------------- | -------- | ---------------------------------------------------- |
| `webhook_url` | `str \| None` | `None`   | Called with the job result instead of waiting on it. |
| `timeout`     | `float`       | `7200.0` | Seconds to wait for the training job.                |

**Returns** (`Twin`): This twin, trained. An already-trained version comes back unchanged.

### Twin.run\_pipeline

```python
Twin.run_pipeline(*, webhook_url: str | None = None, timeout: float = 7200.0) -> Twin
```

Discovery + dependencies + roles + training in one pass.

| Parameter     | Type          | Default  | Description                                          |
| ------------- | ------------- | -------- | ---------------------------------------------------- |
| `webhook_url` | `str \| None` | `None`   | Called with the job result instead of waiting on it. |
| `timeout`     | `float`       | `7200.0` | Seconds to wait for the whole pipeline.              |

**Returns** (`Twin`): This twin, trained.

### Twin.evaluate

```python
Twin.evaluate() -> dict[str, Any]
```

*Undocumented; the signature above is the contract.*

### Twin.new\_version

```python
Twin.new_version(
    *,
    bump: str = 'patch',
    base_version_id: str | None = None,
    dataset_id: str | None = None,
) -> Twin
```

Derive a fresh, untrained version from an existing one — the retrain primitive.

Inherits the base version's configuration and causal graph, resets every training output, and returns the twin pinned to the new version.

| Parameter         | Type          | Default   | Description                                                                |
| ----------------- | ------------- | --------- | -------------------------------------------------------------------------- |
| `bump`            | `str`         | `'patch'` | Which part of the version number to advance: `patch`, `minor`, or `major`. |
| `base_version_id` | `str \| None` | `None`    | Version to derive from. Defaults to the latest.                            |
| `dataset_id`      | `str \| None` | `None`    | Train the new version off a different dataset.                             |

**Returns** (`Twin`): A handle bound to the new, untrained version.

### Twin.retrain

```python
Twin.retrain(*, bump: str = 'patch', timeout: float = 7200.0) -> Twin
```

Create a new version off the latest and train it — the full retrain in one call.

| Parameter | Type    | Default   | Description                                                                |
| --------- | ------- | --------- | -------------------------------------------------------------------------- |
| `bump`    | `str`   | `'patch'` | Which part of the version number to advance: `patch`, `minor`, or `major`. |
| `timeout` | `float` | `7200.0`  | Seconds to wait for the training job.                                      |

**Returns** (`Twin`): A handle bound to the newly trained version.

### Twin.set\_roles

```python
Twin.set_roles(
    *,
    targets: list[str] | None = None,
    sources: list[str] | None = None,
) -> dict[str, Any]
```

Set the version's variable roles ahead of discovery and training.

Roles steer the causal engine: targets are the outcomes the model is for, sources are the levers. Pass either list to change just that side — the other keeps its current (or suggested) value. This is the per-version counterpart of the ontology-level `concept.override(suggested_role=...)`, which sets the default for every future twin built over that concept.

| Parameter | Type                | Default | Description        |
| --------- | ------------------- | ------- | ------------------ |
| `targets` | `list[str] \| None` | `None`  | Outcome variables. |
| `sources` | `list[str] \| None` | `None`  | Driver variables.  |

**Returns** (`dict[str, Any]`): The stored roles document.

### Twin.update

```python
Twin.update(*, webhook_url: str | None = None, timeout: float = 3600.0) -> UpdateResult
```

Fold data added to the backing source since the last train/update into the model.

No retrain: incremental assimilation. The job succeeds with a status rather than failing — `committed`, `up_to_date`, or `retrain_required` (the model can't take these rows incrementally; result.reasons says why — call retrain()). Static and temporal twins assimilate out of the box; panel twins need the v2 panel engine. Requires a trained version; extend or sync the source first so there is something new.

| Parameter     | Type          | Default  | Description                                          |
| ------------- | ------------- | -------- | ---------------------------------------------------- |
| `webhook_url` | `str \| None` | `None`   | Called with the job result instead of waiting on it. |
| `timeout`     | `float`       | `3600.0` | Seconds to wait for the update job.                  |

**Returns** (`UpdateResult`): An [`UpdateResult`](/api-and-integrations/sdk-getting-started/sdk-api-reference/results.md#updateresult). Never raises on `retrain_required`.

### Twin.env

```python
Twin.env(
    environments: str | dict[str, str] = (),
    *,
    where: list[tuple] | dict[str, Any] | None = None,
) -> EnvSubset
```

A handle pinned to a subset of this panel twin's environments.

Name them directly — environment names ("london"), envKeys, or exact {column: value} combos — or select them by data with where=, filtering on any twin column through per-environment statistics:

````python
twin.env("london", "berlin")                       # by name
twin.env(where=[("revenue", "avg", ">", 400)])     # by aggregate
twin.env(where=[("region", "==", "EMEA"),          # constant column
```python
            ("demand", "min", ">=", 0)])       # AND of filters
````

````

Everything on the handle — graph, environments, sample, intervene,
forecast — is scoped to the subset.

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `*environments` | `str \| dict[str, str]` | `()` | Environment names, envKeys, or `{column: value}` combos. |
| `where` | `list[tuple] \| dict[str, Any] \| None` | `None` | Stat filters instead of names — tuples of `(column, op, value)` for constant-per-environment columns, or `(column, reduce, op, value)` with reduce one of `avg`/`mean`, `min`, `max`, or `any` (at least one matching row). A dict filter group passes through as written. |

**Returns** (`EnvSubset`): An [`EnvSubset`](#envsubset) pinned to those environments.

**Raises**

- `RootCauseError`: Neither (or both) selection styles were passed.

### Twin.group

```python
Twin.group(name_or_id: str) -> Group
````

One saved environment group, by name or id.

| Parameter    | Type  | Default  | Description                                            |
| ------------ | ----- | -------- | ------------------------------------------------------ |
| `name_or_id` | `str` | required | The group's display name (case-insensitive) or its id. |

**Returns** (`Group`): A [`Group`](#group): the same scoped surface as [`env()`](#env), pinned to a saved membership rule.

**Raises**

* `RootCauseError`: The twin has no such group; the message names the ones it does have.

### Twin.score

```python
Twin.score(
    rows: pd.DataFrame | list[dict[str, Any]],
    targets: list[dict[str, Any]],
    *,
    max_changes: int = 3,
    constraints: dict[str, Any] | None = None,
    webhook_url: str | None = None,
    timeout: float = 3600.0,
) -> ScoreResult
```

Score rows against target outcomes: each row gets its smallest flip.

Static trained twins only. Blocks until the run completes.

| Parameter     | Type                                   | Default  | Description                                                     |
| ------------- | -------------------------------------- | -------- | --------------------------------------------------------------- |
| `rows`        | `pd.DataFrame \| list[dict[str, Any]]` | required | A DataFrame, or a list of dicts whose keys name twin variables. |
| `targets`     | `list[dict[str, Any]]`                 | required | The outcomes to reach, as `[{"variable": ..., "value": ...}]`.  |
| `max_changes` | `int`                                  | `3`      | Most variables any one row is allowed to flip.                  |
| `constraints` | `dict[str, Any] \| None`               | `None`   | Per-variable limits on what may change, and how far.            |
| `webhook_url` | `str \| None`                          | `None`   | Called with the run result instead of waiting on it.            |
| `timeout`     | `float`                                | `3600.0` | Seconds to wait for the run.                                    |

**Returns** (`ScoreResult`): A [`ScoreResult`](/api-and-integrations/sdk-getting-started/sdk-api-reference/results.md#scoreresult) covering every row.

### Twin.sample

```python
Twin.sample(
    n: int = 1000,
    do: dict[str, Any] | None = None,
    where: Any = None,
    environments: list[str] | None = None,
    seed: int | None = None,
) -> SampleDraws
```

Raw joint posterior draws — the primitive every simulation family wraps.

| Parameter      | Type                     | Default | Description                                                                                                                                                |
| -------------- | ------------------------ | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `n`            | `int`                    | `1000`  | Draws per sampling unit.                                                                                                                                   |
| `do`           | `dict[str, Any] \| None` | `None`  | Interventions to apply before sampling, as `{"variable": rc.set(value)}`. A bare value means `rc.set`.                                                     |
| `where`        | `Any`                    | `None`  | Scope the draws to a subpopulation: `{"region": "EMEA"}` for equality, or `{"income": ("<", 5000)}` with any of `== != > < >= <=`, plus `in` and `not_in`. |
| `environments` | `list[str] \| None`      | `None`  | Panel twins: which environments to sample. Each is sampled independently.                                                                                  |
| `seed`         | `int \| None`            | `None`  | Seed for reproducible draws. Panel twins derive stable per-environment child seeds from it.                                                                |

**Returns** (`SampleDraws`): The draws as [`SampleDraws`](/api-and-integrations/sdk-getting-started/sdk-api-reference/results.md#sampledraws).

**Raises**

* `RootCauseError`: `environments` was passed for a twin that is not a panel twin.

### Twin.intervene

```python
Twin.intervene(
    do: dict[str, Any],
    where: Any = None,
    metrics: list[dict[str, Any]] | None = None,
    outcomes: list[str] | None = None,
    environments: list[str] | None = None,
    *,
    timeout: float = 3600.0,
) -> SimulationResult
```

Run an intervention simulation and block for the result.

Interventions measure their effect through metrics: pass `metrics=[rc.metric(...)]` for full control, or `outcomes=["revenue"]` for mean-of-column metrics. For raw effect distributions without metrics, use `twin.sample(do=...)`.

| Parameter      | Type                           | Default  | Description                                                                                                                                                       |
| -------------- | ------------------------------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `do`           | `dict[str, Any]`               | required | The interventions, as `{"variable": rc.set(value)}`. A bare value means `rc.set`; `rc.range(...)` sweeps instead of pinning.                                      |
| `where`        | `Any`                          | `None`   | Scope the intervention to a subpopulation: `{"region": "EMEA"}` for equality, or `{"income": ("<", 5000)}` with any of `== != > < >= <=`, plus `in` and `not_in`. |
| `metrics`      | `list[dict[str, Any]] \| None` | `None`   | Metrics to measure the effect through, from `rc.metric()`.                                                                                                        |
| `outcomes`     | `list[str] \| None`            | `None`   | Column names to measure as mean-of-column metrics, when `metrics` is not given.                                                                                   |
| `environments` | `list[str] \| None`            | `None`   | Panel twins: which environments to simulate.                                                                                                                      |
| `timeout`      | `float`                        | `3600.0` | Seconds to wait for the run.                                                                                                                                      |

**Returns** (`SimulationResult`): A [`SimulationResult`](/api-and-integrations/sdk-getting-started/sdk-api-reference/results.md#simulationresult).

**Raises**

* `RootCauseError`: Neither `metrics` nor `outcomes` was given.

### Twin.forecast

```python
Twin.forecast(
    horizon: int,
    targets: list[str] | None = None,
    environments: list[str] | None = None,
    confidence: float = 0.95,
    origin_timestamp: int | None = None,
    aggregate: str | None = None,
    *,
    timeout: float = 3600.0,
) -> ForecastResult
```

Forecast `horizon` steps ahead for the target variables.

| Parameter          | Type                | Default  | Description                                                                                                                                                                   |
| ------------------ | ------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `horizon`          | `int`               | required | How many steps ahead to forecast.                                                                                                                                             |
| `targets`          | `list[str] \| None` | `None`   | Variables to forecast. Inferred from the twin when omitted.                                                                                                                   |
| `environments`     | `list[str] \| None` | `None`   | Panel twins: which environments to forecast.                                                                                                                                  |
| `confidence`       | `float`             | `0.95`   | Width of the prediction interval, as a probability.                                                                                                                           |
| `origin_timestamp` | `int \| None`       | `None`   | Anchor the forecast start (ms epoch). How a backtest aligns a forecast against data the twin never saw.                                                                       |
| `aggregate`        | `str \| None`       | `None`   | Panel twins: the statistic for the combined series across environments, one of `sum`, `avg`, `min`, `max`. Defaults to `avg`; every panel forecast carries a combined series. |
| `timeout`          | `float`             | `3600.0` | Seconds to wait for the run.                                                                                                                                                  |

**Returns** (`ForecastResult`): A [`ForecastResult`](/api-and-integrations/sdk-getting-started/sdk-api-reference/results.md#forecastresult), tidy long format.

**Raises**

* `RootCauseError`: The twin is not temporal, or `aggregate` was passed for a twin that is not a panel twin.

### Twin.predict

```python
Twin.predict(
    sample: pd.DataFrame | list[dict[str, Any]],
    targets: list[str] | None = None,
    confidence: float = 0.95,
    *,
    timeout: float = 3600.0,
) -> PredictionResult
```

Predict target outcomes for input records, with uncertainty intervals.

One prediction per input record: the model reads the values you supply as the drivers and answers for the targets you name. Static twins only: a temporal twin projects forward with `forecast()` instead.

| Parameter    | Type                                   | Default  | Description                                                                                                                                            |
| ------------ | -------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `sample`     | `pd.DataFrame \| list[dict[str, Any]]` | required | The input records, as a DataFrame or a list of dicts keyed by twin variable name. Leave the target columns out: those are what the model answers with. |
| `targets`    | `list[str] \| None`                    | `None`   | Variables to predict. Inferred from the twin when omitted.                                                                                             |
| `confidence` | `float`                                | `0.95`   | Width of the uncertainty interval, as a probability.                                                                                                   |
| `timeout`    | `float`                                | `3600.0` | Seconds to wait for the run.                                                                                                                           |

**Returns** (`PredictionResult`): A [`PredictionResult`](/api-and-integrations/sdk-getting-started/sdk-api-reference/results.md#predictionresult), one row per input record.

**Raises**

* `RootCauseError`: The twin is temporal, where `forecast()` is the verb.

**Examples**

```python
>>> twin.predict([{"tenure": 3, "MonthlyCharges": 85.0}], targets=["Churn"])
```

### Twin.explain

```python
Twin.explain(
    cause: str | None = None,
    effect: str | None = None,
    mode: str | None = None,
    environments: list[str] | None = None,
    *,
    timeout: float = 3600.0,
) -> SimulationResult
```

Explain a causal relationship: what a variable drives, or what drives it.

The mode follows from what you name, so you rarely pass it: `cause=` alone asks what that variable goes on to affect (`impact`), `effect=` alone asks what drives it (`discovery`), and both together explain the paths from one to the other (`directional`).

| Parameter      | Type                | Default  | Description                                                 |
| -------------- | ------------------- | -------- | ----------------------------------------------------------- |
| `cause`        | `str \| None`       | `None`   | The upstream variable, for `impact` and `directional`.      |
| `effect`       | `str \| None`       | `None`   | The downstream variable, for `discovery` and `directional`. |
| `mode`         | `str \| None`       | `None`   | Override the mode: `directional`, `discovery`, or `impact`. |
| `environments` | `list[str] \| None` | `None`   | Panel twins: which environments to explain.                 |
| `timeout`      | `float`             | `3600.0` | Seconds to wait for the run.                                |

**Returns** (`SimulationResult`): A [`SimulationResult`](/api-and-integrations/sdk-getting-started/sdk-api-reference/results.md#simulationresult).

**Raises**

* `RootCauseError`: Neither variable was named, the mode is unknown, the mode is missing a variable it needs, or `environments` was passed for a twin that is not a panel twin.

**Examples**

```python
>>> twin.explain(effect="Churn")
>>> twin.explain(cause="Contract", effect="Churn")
```

### Twin.optimise

```python
Twin.optimise(
    objectives: list[dict[str, Any]],
    decision_vars: list[str],
    horizon: int | None = None,
    environments: list[str] | None = None,
    variable_constraints: list[dict[str, Any]] | None = None,
    metric_constraints: list[dict[str, Any]] | None = None,
    max_changes: int | None = None,
    *,
    timeout: float = 3600.0,
) -> SimulationResult
```

Search for the actions that best move your objectives.

The optimizer may only touch the variables you list in `decision_vars`, and it measures every plan through the objectives' SQL, so an objective naming a variable nothing in `decision_vars` can reach has no plan to find.

| Parameter              | Type                           | Default  | Description                                                                                                      |
| ---------------------- | ------------------------------ | -------- | ---------------------------------------------------------------------------------------------------------------- |
| `objectives`           | `list[dict[str, Any]]`         | required | What to move and which way, from `rc.objective()`.                                                               |
| `decision_vars`        | `list[str]`                    | required | The variables the optimizer is allowed to change.                                                                |
| `horizon`              | `int \| None`                  | `None`   | Temporal twins: how many steps ahead the plan runs over. Required for a temporal twin, refused for a static one. |
| `environments`         | `list[str] \| None`            | `None`   | Panel twins: which environments to optimize over.                                                                |
| `variable_constraints` | `list[dict[str, Any]] \| None` | `None`   | Bounds on how far each variable may move.                                                                        |
| `metric_constraints`   | `list[dict[str, Any]] \| None` | `None`   | Guardrails every plan must respect.                                                                              |
| `max_changes`          | `int \| None`                  | `None`   | Cap on how many variables a single plan may change.                                                              |
| `timeout`              | `float`                        | `3600.0` | Seconds to wait for the run.                                                                                     |

**Returns** (`SimulationResult`): A [`SimulationResult`](/api-and-integrations/sdk-getting-started/sdk-api-reference/results.md#simulationresult).

**Raises**

* `RootCauseError`: No objectives or no decision variables, an objective that is not a `rc.objective()` payload, a horizon that this twin kind does not take (or a temporal twin given none), or `environments` on a twin that is not a panel twin.

**Examples**

```python
>>> churn = rc.objective(
...     "Churn share",
...     "SELECT AVG(CASE WHEN Churn = 'Yes' THEN 1.0 ELSE 0.0 END) AS value FROM df",
...     "minimise",
... )
>>> twin.optimise([churn], decision_vars=["Contract", "MonthlyCharges"])
```

### Twin.root\_cause

```python
Twin.root_cause(
    target: str,
    samples: pd.DataFrame | list[dict[str, Any]] | dict[str, list[dict[str, Any]]],
    environments: list[str] | None = None,
    timestep: int | None = None,
    target_fpr: float = 0.005,
    *,
    timeout: float = 3600.0,
) -> SimulationResult
```

Diagnose one variable: trace it upstream to what actually broke it.

Use this when you already know which variable is misbehaving. To find out whether anything is, scan every variable with `anomalies()`.

| Parameter      | Type                                                                      | Default  | Description                                                                                                                                                                         |
| -------------- | ------------------------------------------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `target`       | `str`                                                                     | required | The variable behaving unexpectedly.                                                                                                                                                 |
| `samples`      | `pd.DataFrame \| list[dict[str, Any]] \| dict[str, list[dict[str, Any]]]` | required | The observations to diagnose, as a DataFrame, a list of dicts, or, on a panel twin, a `{environment: rows}` mapping. A flat list on a panel twin is shared across its environments. |
| `environments` | `list[str] \| None`                                                       | `None`   | Panel twins: which environments to diagnose.                                                                                                                                        |
| `timestep`     | `int \| None`                                                             | `None`   | Temporal twins: the step to diagnose. Defaults to the one the scan flags.                                                                                                           |
| `target_fpr`   | `float`                                                                   | `0.005`  | Detection sensitivity, as a false-positive rate between 0.0001 and 0.1. Lower flags less.                                                                                           |
| `timeout`      | `float`                                                                   | `3600.0` | Seconds to wait for the run.                                                                                                                                                        |

**Returns** (`SimulationResult`): A [`SimulationResult`](/api-and-integrations/sdk-getting-started/sdk-api-reference/results.md#simulationresult).

**Raises**

* `RootCauseError`: No target, `environments` or per-environment samples on a twin that is not a panel twin, or `timestep` on a twin with no time axis.

**Examples**

```python
>>> twin.root_cause("Churn", observed_frame)
```

### Twin.anomalies

```python
Twin.anomalies(
    samples: pd.DataFrame | list[dict[str, Any]] | dict[str, list[dict[str, Any]]],
    environments: list[str] | None = None,
    start_step: int | None = None,
    end_step: int | None = None,
    target_fpr: float = 0.005,
    *,
    timeout: float = 3600.0,
) -> SimulationResult
```

Scan every variable for causal anomalies, and diagnose what it finds.

The counterpart to `root_cause()`: this one asks whether anything is broken rather than why a named variable is.

| Parameter      | Type                                                                      | Default  | Description                                                                                                                                                                     |
| -------------- | ------------------------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `samples`      | `pd.DataFrame \| list[dict[str, Any]] \| dict[str, list[dict[str, Any]]]` | required | The observations to scan, as a DataFrame, a list of dicts, or, on a panel twin, a `{environment: rows}` mapping. A flat list on a panel twin is shared across its environments. |
| `environments` | `list[str] \| None`                                                       | `None`   | Panel twins: which environments to scan.                                                                                                                                        |
| `start_step`   | `int \| None`                                                             | `None`   | Temporal twins: first step of the window to scan.                                                                                                                               |
| `end_step`     | `int \| None`                                                             | `None`   | Temporal twins: last step of the window to scan.                                                                                                                                |
| `target_fpr`   | `float`                                                                   | `0.005`  | Detection sensitivity, as a false-positive rate between 0.0001 and 0.1. Lower flags less.                                                                                       |
| `timeout`      | `float`                                                                   | `3600.0` | Seconds to wait for the run.                                                                                                                                                    |

**Returns** (`SimulationResult`): A [`SimulationResult`](/api-and-integrations/sdk-getting-started/sdk-api-reference/results.md#simulationresult).

**Raises**

* `RootCauseError`: `environments` or per-environment samples on a twin that is not a panel twin, or a step window on a twin with no time axis.

**Examples**

```python
>>> twin.anomalies(observed_frame)
```

### Twin.ask

```python
Twin.ask(query: str, *, timeout: float = 3600.0) -> SimulationResult
```

Natural-language question, turned into a scenario and executed.

| Parameter | Type    | Default  | Description                      |
| --------- | ------- | -------- | -------------------------------- |
| `query`   | `str`   | required | The question, in plain language. |
| `timeout` | `float` | `3600.0` | Seconds to wait for the run.     |

**Returns** (`SimulationResult`): A [`SimulationResult`](/api-and-integrations/sdk-getting-started/sdk-api-reference/results.md#simulationresult); `result.scenario` is what the translator produced.

### Twin.console

```python
Twin.console(*, height: int = 560, theme: str = '') -> Any
```

The interactive causal-graph console under the cell: the same app Claude renders.

Explore edges, type intervention values, and re-run scenarios; every control round-trips live through the platform's MCP gateway with this session's credentials. Needs: pip install "rootcause-sdk\[jupyter]".

| Parameter | Type  | Default | Description                                       |
| --------- | ----- | ------- | ------------------------------------------------- |
| `height`  | `int` | `560`   | Height of the mounted app, in pixels.             |
| `theme`   | `str` | `''`    | `light`, `dark`, or empty to follow the notebook. |

**Returns** (`Any`): The mounted widget, displayed by the notebook cell.

### Twin.sankey

```python
Twin.sankey(
    node: str | None = None,
    *,
    edge: tuple[str, str] | None = None,
    depth: int = 2,
    height: int = 520,
    theme: str = '',
) -> Any
```

The causal-flow Sankey under the cell: how influence propagates through the graph.

Pass a variable to see everything flowing into and out of it, or an edge to see the paths running through that one link. Needs a resolved or trained graph and: pip install "rootcause-sdk\[jupyter]".

| Parameter | Type                      | Default | Description                                                                       |
| --------- | ------------------------- | ------- | --------------------------------------------------------------------------------- |
| `node`    | `str \| None`             | `None`  | Variable to analyse paths around. Exactly one of `node` and `edge` must be given. |
| `edge`    | `tuple[str, str] \| None` | `None`  | A `(cause, effect)` pair to analyse paths through.                                |
| `depth`   | `int`                     | `2`     | How many hops to traverse on either side.                                         |
| `height`  | `int`                     | `520`   | Height of the mounted app, in pixels.                                             |
| `theme`   | `str`                     | `''`    | `light`, `dark`, or empty to follow the notebook.                                 |

**Returns** (`Any`): The mounted widget, displayed by the notebook cell.

### Twin.review

```python
Twin.review(*, height: int = 560, theme: str = '') -> Any
```

The graph-review console under the cell: structural findings with accept/reject controls.

Runs the platform's DAG review (cycles, isolated nodes, weak or wrong-direction edges, over-connected hubs) and mounts the interactive console over the findings; applying a fix round-trips live through the MCP gateway. Needs: pip install "rootcause-sdk\[jupyter]".

| Parameter | Type  | Default | Description                                       |
| --------- | ----- | ------- | ------------------------------------------------- |
| `height`  | `int` | `560`   | Height of the mounted app, in pixels.             |
| `theme`   | `str` | `''`    | `light`, `dark`, or empty to follow the notebook. |

**Returns** (`Any`): The mounted widget, displayed by the notebook cell.

### Twin.studio

```python
Twin.studio(
    query: str,
    *,
    targets: list[str] | None = None,
    horizon: int | None = None,
    environments: list[str] | None = None,
    aggregate: str | None = None,
    height: int = 640,
    theme: str = '',
) -> Any
```

Ask a what-if in plain English; the What-If Studio renders the answer under the cell.

The scenario is inferred from the question and executed server-side; the studio draws the result with the dials behind it, so a tweaked scenario re-runs live through the MCP gateway without leaving the notebook. Needs: pip install "rootcause-sdk\[jupyter]".

| Parameter      | Type                | Default  | Description                                                                                     |
| -------------- | ------------------- | -------- | ----------------------------------------------------------------------------------------------- |
| `query`        | `str`               | required | The question, stated completely — which variables change, to what, and the outcome of interest. |
| `targets`      | `list[str] \| None` | `None`   | Exact outcome variable names, when inference should not pick them from the question.            |
| `horizon`      | `int \| None`       | `None`   | Forecast scenarios only: exact number of steps.                                                 |
| `environments` | `list[str] \| None` | `None`   | Panel forecasts only: restrict to these environments.                                           |
| `aggregate`    | `str \| None`       | `None`   | Panel forecasts only: `sum`, `avg`, `min`, or `max` across environments.                        |
| `height`       | `int`               | `640`    | Height of the mounted app, in pixels.                                                           |
| `theme`        | `str`               | `''`     | `light`, `dark`, or empty to follow the notebook.                                               |

**Returns** (`Any`): The mounted widget, displayed by the notebook cell.

### Twin.save

```python
Twin.save(
    path: str | Path,
    *,
    include_runs: bool = False,
    timeout: float = 3600.0,
) -> Path
```

Export this twin (trained params included) as a portable .rctwin zip.

| Parameter      | Type          | Default  | Description                                                                           |
| -------------- | ------------- | -------- | ------------------------------------------------------------------------------------- |
| `path`         | `str \| Path` | required | Where to write the `.rctwin` file. A directory writes `<twin name>.rctwin` inside it. |
| `include_runs` | `bool`        | `False`  | Include the simulation run history in the export.                                     |
| `timeout`      | `float`       | `3600.0` | Seconds to wait for the export job.                                                   |

**Returns** (`Path`): The path written, ready for `rc.load_twin()`.

**Raises**

* `InvalidArgumentError`: The destination directory does not exist. The check runs before the export job starts.

## EnvSubset

A panel twin pinned to a subset of its environments.

Everything on the handle runs scoped to the subset: `graph` re-aggregates the causal adjacency over just these environments, and sample/intervene/ forecast delegate to the twin with environments= filled in.

### Properties

* **environments** (`pd.DataFrame`): The environments this handle covers, resolved to a DataFrame.
* **graph** (`pd.DataFrame`)

### EnvSubset.combos

```python
EnvSubset.combos() -> list[dict[str, str]]
```

The subset as exact {column: value} combos, resolved against the twin's environments.

The listing carries each environment's values as a list ordered by environmentColumns; zipping the two recovers the combo.

### EnvSubset.save

```python
EnvSubset.save(name: str) -> Group
```

Save this subset on the twin as a named environment group.

The group lives on the twin rather than on a version, so it survives retraining, appears in the platform's environment picker straight away, and comes back next session as `twin.group(name)`. What gets stored is the rule, not the answer: a `where=` subset saves its filters and re-selects environments as the data moves, while a named subset saves the exact combos it resolved to.

```python
eu = twin.env("london", "berlin").save("EU stores")
twin.group("EU stores").intervene({"price": rc.pct(-10)}, outcomes=["revenue"])
```

| Parameter | Type  | Default  | Description                    |
| --------- | ----- | -------- | ------------------------------ |
| `name`    | `str` | required | Display name, unique per twin. |

**Returns** (`Group`): The saved [`Group`](#group).

**Raises**

* `RootCauseError`: The twin already has a group with this name, or is at its group cap.

### EnvSubset.adjacency

```python
EnvSubset.adjacency(agreement_threshold: float | None = None) -> pd.DataFrame
```

The causal adjacency aggregated over just this subset of environments.

Returns the edges as a DataFrame (source, target, strength, agreementRate, …); frame.attrs carries envCount, sampleSize, totalEnvCount, and the threshold.

### EnvSubset.sample

```python
EnvSubset.sample(
    n: int = 1000,
    do: dict[str, Any] | None = None,
    where: Any = None,
    seed: int | None = None,
) -> SampleDraws
```

*Undocumented; the signature above is the contract.*

### EnvSubset.intervene

```python
EnvSubset.intervene(
    do: dict[str, Any],
    where: Any = None,
    metrics: list[dict[str, Any]] | None = None,
    outcomes: list[str] | None = None,
    *,
    timeout: float = 3600.0,
) -> SimulationResult
```

*Undocumented; the signature above is the contract.*

### EnvSubset.forecast

```python
EnvSubset.forecast(
    horizon: int,
    targets: list[str] | None = None,
    confidence: float = 0.95,
    origin_timestamp: int | None = None,
    aggregate: str | None = None,
    *,
    timeout: float = 3600.0,
) -> ForecastResult
```

*Undocumented; the signature above is the contract.*

### EnvSubset.explain

```python
EnvSubset.explain(
    cause: str | None = None,
    effect: str | None = None,
    mode: str | None = None,
    *,
    timeout: float = 3600.0,
) -> SimulationResult
```

*Undocumented; the signature above is the contract.*

### EnvSubset.optimise

```python
EnvSubset.optimise(
    objectives: list[dict[str, Any]],
    decision_vars: list[str],
    horizon: int | None = None,
    variable_constraints: list[dict[str, Any]] | None = None,
    metric_constraints: list[dict[str, Any]] | None = None,
    max_changes: int | None = None,
    *,
    timeout: float = 3600.0,
) -> SimulationResult
```

*Undocumented; the signature above is the contract.*

### EnvSubset.root\_cause

```python
EnvSubset.root_cause(
    target: str,
    samples: pd.DataFrame | list[dict[str, Any]] | dict[str, list[dict[str, Any]]],
    timestep: int | None = None,
    target_fpr: float = 0.005,
    *,
    timeout: float = 3600.0,
) -> SimulationResult
```

*Undocumented; the signature above is the contract.*

### EnvSubset.anomalies

```python
EnvSubset.anomalies(
    samples: pd.DataFrame | list[dict[str, Any]] | dict[str, list[dict[str, Any]]],
    start_step: int | None = None,
    end_step: int | None = None,
    target_fpr: float = 0.005,
    *,
    timeout: float = 3600.0,
) -> SimulationResult
```

*Undocumented; the signature above is the contract.*

### EnvSubset.link

```python
EnvSubset.link() -> Any
```

The parent twin's page on the platform, as a clickable URL.

## Group

A saved environment group: the same scoped surface as a subset, kept on the twin.

Everything an [`EnvSubset`](#envsubset) does, a group does against its current membership — the rule is stored, not the answer, so it is resolved against this handle's version on first use and re-resolved after an edit. Simulations name the group rather than expanding it, so the run records which group it covered and what that meant at submit time.

| Attribute | Type   | Description                |
| --------- | ------ | -------------------------- |
| `doc`     | `dict` | The stored group document. |

### Properties

* **id** (`str`)
* **name** (`str`)
* **definition** (`dict[str, Any]`): The stored membership rule: `environments`, `columnValues`, or `statFilters` mode.

### Group.rename

```python
Group.rename(name: str) -> Group
```

Rename the group in place.

| Parameter | Type  | Default  | Description                            |
| --------- | ----- | -------- | -------------------------------------- |
| `name`    | `str` | required | The new display name, unique per twin. |

**Returns** (`Group`): This group.

**Raises**

* `RootCauseError`: The twin already has a group with this name.

### Group.update

```python
Group.update(
    environments: str | dict[str, str] = (),
    *,
    where: list[tuple] | dict[str, Any] | None = None,
    definition: dict[str, Any] | None = None,
) -> Group
```

Replace the group's membership rule, in the same vocabulary as `twin.env()`.

```python
group.update("london", "berlin", "paris")            # exact environments
group.update(where=[("revenue", "avg", ">", 400)])   # a filter, re-selected as data moves
```

Runs already submitted keep the membership frozen on their snapshots; every later read of the group sees the new rule.

| Parameter       | Type                                    | Default | Description                                                                             |
| --------------- | --------------------------------------- | ------- | --------------------------------------------------------------------------------------- |
| `*environments` | `str \| dict[str, str]`                 | `()`    | Environment names, envKeys, or `{column: value}` combos, as [`env()`](#env) takes them. |
| `where`         | `list[tuple] \| dict[str, Any] \| None` | `None`  | Stat filters instead of names, as [`env()`](#env) takes them.                           |
| `definition`    | `dict[str, Any] \| None`                | `None`  | A raw definition document, when you have one already.                                   |

**Returns** (`Group`): This group, on the new rule.

**Raises**

* `RootCauseError`: None (or more than one) of the three selection styles was passed.

### Group.delete

```python
Group.delete() -> None
```

Delete the group from the twin.

Nothing downstream goes with it: runs scoped to the group keep their snapshots. Deleting a group that is already gone is a no-op.

### Group.intervene

```python
Group.intervene(
    do: dict[str, Any],
    where: Any = None,
    metrics: list[dict[str, Any]] | None = None,
    outcomes: list[str] | None = None,
    *,
    timeout: float = 3600.0,
) -> SimulationResult
```

*Undocumented; the signature above is the contract.*

### Group.forecast

```python
Group.forecast(
    horizon: int,
    targets: list[str] | None = None,
    confidence: float = 0.95,
    origin_timestamp: int | None = None,
    aggregate: str | None = None,
    *,
    timeout: float = 3600.0,
) -> ForecastResult
```

*Undocumented; the signature above is the contract.*

### Group.explain

```python
Group.explain(
    cause: str | None = None,
    effect: str | None = None,
    mode: str | None = None,
    *,
    timeout: float = 3600.0,
) -> SimulationResult
```

*Undocumented; the signature above is the contract.*

### Group.optimise

```python
Group.optimise(
    objectives: list[dict[str, Any]],
    decision_vars: list[str],
    horizon: int | None = None,
    variable_constraints: list[dict[str, Any]] | None = None,
    metric_constraints: list[dict[str, Any]] | None = None,
    max_changes: int | None = None,
    *,
    timeout: float = 3600.0,
) -> SimulationResult
```

*Undocumented; the signature above is the contract.*

### Group.root\_cause

```python
Group.root_cause(
    target: str,
    samples: pd.DataFrame | list[dict[str, Any]] | dict[str, list[dict[str, Any]]],
    timestep: int | None = None,
    target_fpr: float = 0.005,
    *,
    timeout: float = 3600.0,
) -> SimulationResult
```

*Undocumented; the signature above is the contract.*

### Group.anomalies

```python
Group.anomalies(
    samples: pd.DataFrame | list[dict[str, Any]] | dict[str, list[dict[str, Any]]],
    start_step: int | None = None,
    end_step: int | None = None,
    target_fpr: float = 0.005,
    *,
    timeout: float = 3600.0,
) -> SimulationResult
```

*Undocumented; the signature above is the contract.*

### Group.link

```python
Group.link() -> Any
```

The parent twin's page on the platform, as a clickable URL.
