Scenarios
What-if simulations over the discovered graph — move a driver, inject an event, and see the forecast respond.
A scenario asks the goal's champion a counterfactual: what does the forecast look like if a driver moves, or an event fires? The intervention is applied to the serving data, derived features are recomputed, and the champion re-infers — so the answer is the model's real response, not a rule of thumb.
Scenario runs are charged upfront and refunded if the run fails. Results are persisted — saved scenarios are free to view.
Run a scenario
A spec needs at least one driver override or one injected event:
curl -X POST "$API_BASE/v1/goals/$GOAL_ID/scenarios" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Workspace-Id: $WORKSPACE_ID" \
-H "Content-Type: application/json" \
-d '{
"name": "Ad spend +10%",
"overrides": [
{ "key": "ad_spend", "mode": "pct", "amount": 10, "shape": "step" }
],
"events": []
}'from predictai import PredictAI
client = PredictAI(token="pa_live_…", workspace_id="ws_…")
data = client.goals.post_goals_by_goal_id_scenarios(GOAL_ID, json={
"name": "Ad spend +10%",
"overrides": [
{
"key": "ad_spend",
"mode": "pct",
"amount": 10,
"shape": "step",
},
],
"events": [],
})
import { PredictAI } from "@predictai/sdk";
const client = new PredictAI({ token: "pa_live_…", workspaceId: "ws_…" });
const data = await client.goals.postGoalsByGoalIdScenarios(GOAL_ID, {
json: {
name: "Ad spend +10%",
overrides: [
{
key: "ad_spend",
mode: "pct",
amount: 10,
shape: "step",
},
],
events: [],
},
});
{
"data": {
"scenario_id": "d2f7…",
"name": "Ad spend +10%",
"spec": { "overrides": [{ "key": "ad_spend", "mode": "pct", "amount": 10.0, "shape": "step" }], "events": [] },
"mechanisms": ["override"],
"baseline": { "point": [13102.4, 13350.9, "…"] },
"fan": { "point": [13298.7, 13617.5, "…"] },
"delta": [196.3, 266.6, "…"],
"applied_overrides": [{ "key": "ad_spend", "mode": "pct", "amount": 10.0 }],
"attribution": [{ "key": "ad_spend", "share": 1.0 }],
"chains": [],
"events": [],
"caveats": [
"model response under intervention — a validated predictive relationship, not a causal guarantee"
],
"warnings": [],
"snapshot": {
"window_end": "2026-07-15T00:00:00+00:00",
"champion_pipeline_id": "9f2c41d8-…",
"horizon_bins": 14,
"interval_seconds": 86400,
"target_unit": "$"
},
"credits_charged": 5,
"created_at": "2026-07-15T12:30:00+00:00"
}
}The 201 body is the persisted scenario: the baseline forecast, the
counterfactual fan (with quantile bands when the champion produces
them), the per-step delta, and the provenance — which overrides applied,
which multi-hop chains carried an upstream move, and honest caveats.
Request body
| Field | Meaning |
|---|---|
overrides required | Driver moves — at least one override or one event. Up to a handful per scenario. |
events optional | Injected occurrences on event streams. |
name optional | Display name for the saved scenario. |
Each overrides[] entry:
| Field | Meaning |
|---|---|
key required | The driver to move — must be flagged simulatable in drivers-now. |
mode required | pct (percent), abs (absolute), or sigma (standard deviations). |
amount required | How much, in the chosen mode's units. |
shape optional | step (immediate, the default) or ramp (gradual). |
Each events[] entry:
| Field | Meaning |
|---|---|
stream required | The event stream to inject on. |
magnitude optional | Size versus a typical event, 0.1–5. Defaults to 1. |
direction optional | 1 or -1. The response replays the stream's measured response curve. |
at_offset_bins optional | When in the horizon the event lands, as a step offset (0 = now, the default). The response curve is applied from that step. |
at optional | Same thing as a timestamp (epoch or ISO 8601), converted to the nearest step. at_offset_bins wins when both are sent. |
Timing an injection answers "what if the promo runs mid-horizon instead of today?" For the full "which timing is best" question, use the timing sweep — one call instead of one scenario per candidate.
Errors
| Status | Why | Example message |
|---|---|---|
400 | Empty or invalid spec (validated before charging) | "Scenario needs at least one driver override or one injected event" · "Unknown override mode 'x' (pct | abs | sigma)" |
400 | No champion yet | "This goal has no live forecast model yet — run discovery first, then come back to simulate" |
400 | Overriding a derived feature | "'ad_spend_lag3' is a feature derived from 'ad_spend'. Override 'ad_spend' instead — …" |
402 | Not enough credits | error.code insufficient_credits, error.details.credits_needed 5 |
404 | Goal doesn't exist | "Goal not found" |
Only base signals flagged simulatable in
drivers-now can be
overridden — derived features are recomputed automatically.
Parse natural language
Turn a plain-English what-if into a structured spec. Parsing returns the spec for you to review or edit — it does not run the scenario:
curl -X POST "$API_BASE/v1/goals/$GOAL_ID/scenarios/parse" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Workspace-Id: $WORKSPACE_ID" \
-H "Content-Type: application/json" \
-d '{ "text": "What if ad spend rises 10% and we get a supply disruption?" }'from predictai import PredictAI
client = PredictAI(token="pa_live_…", workspace_id="ws_…")
data = client.goals.post_goals_by_goal_id_scenarios_parse(GOAL_ID, json={
"text": "What if ad spend rises 10% and we get a supply disruption?",
})
import { PredictAI } from "@predictai/sdk";
const client = new PredictAI({ token: "pa_live_…", workspaceId: "ws_…" });
const data = await client.goals.postGoalsByGoalIdScenariosParse(GOAL_ID, {
json: {
text: "What if ad spend rises 10% and we get a supply disruption?",
},
});
{
"data": {
"spec": {
"name": "Ad spend +10% with supply disruption",
"overrides": [{ "key": "ad_spend", "mode": "pct", "amount": 10.0, "shape": "step" }],
"events": [{ "stream": "supply_disruptions", "magnitude": 1.0, "direction": 1 }]
},
"explanation": "Mapped 'ad spend' to the ad_spend driver and the disruption to your supply_disruptions stream.",
"unmapped": [],
"warnings": []
}
}| Field | Meaning |
|---|---|
text required | The plain-English what-if, max 1000 characters. |
Anything that couldn't be mapped to a known driver lands in unmapped.
Over-long descriptions return 400
("Description is too long (max 1000 chars)"); an empty body returns
"Describe a what-if to parse". Charged per parse; refunded if parsing
fails.
Explain a scenario
A short written report on why the simulation moved, with the validated edges and measured event curves behind each claim. The numbers are computed deterministically from stored evidence; the report only phrases them:
curl -X POST "$API_BASE/v1/goals/$GOAL_ID/scenarios/$SCENARIO_ID/explain" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Workspace-Id: $WORKSPACE_ID"from predictai import PredictAI
client = PredictAI(token="pa_live_…", workspace_id="ws_…")
data = client.goals.post_goals_by_goal_id_scenarios_by_scenario_id_explain(GOAL_ID, SCENARIO_ID)
import { PredictAI } from "@predictai/sdk";
const client = new PredictAI({ token: "pa_live_…", workspaceId: "ws_…" });
const data = await client.goals.postGoalsByGoalIdScenariosByScenarioIdExplain(GOAL_ID, SCENARIO_ID);
{
"data": {
"scenario_id": "d2f7…",
"report": {
"summary": "The +10% ad spend move lifts the 14-day forecast by ~1.9%, arriving on the driver's 3-step lag.",
"provenance": [{ "edge_id": "7d3f…", "claim": "ad_spend → daily_sales, lag 3, lift 0.18" }]
}
}
}The report is cached on the scenario — the first generation is charged,
repeat calls return "cached": true for free. Returns 404
("Scenario not found") if the scenario doesn't belong to this goal.
Sensitivity (tornado) analysis
Probe every simulatable driver with a symmetric ±X% step and rank by end-of-horizon impact — which knobs matter most, in one call:
curl -X POST "$API_BASE/v1/goals/$GOAL_ID/scenarios/sensitivity" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Workspace-Id: $WORKSPACE_ID" \
-H "Content-Type: application/json" \
-d '{ "magnitude_pct": 10 }'from predictai import PredictAI
client = PredictAI(token="pa_live_…", workspace_id="ws_…")
data = client.goals.post_goals_by_goal_id_scenarios_sensitivity(GOAL_ID, json={"magnitude_pct": 10})
import { PredictAI } from "@predictai/sdk";
const client = new PredictAI({ token: "pa_live_…", workspaceId: "ws_…" });
const data = await client.goals.postGoalsByGoalIdScenariosSensitivity(GOAL_ID, { json: { magnitude_pct: 10 } });
{
"data": {
"sensitivity_id": "a90c…",
"magnitude_pct": 10.0,
"baseline_end": 14210.55,
"results": [
{ "key": "ad_spend", "lifecycle": "validated", "lift": 0.18, "span_bins": 14, "up_delta": 271.4, "down_delta": -244.9, "unit": "$" },
{ "key": "shipping_rates", "lifecycle": "weakening", "lift": 0.09, "span_bins": 14, "up_delta": -118.2, "down_delta": 105.7, "unit": "$" }
],
"warnings": [],
"caveats": ["model response under intervention — …"],
"snapshot": { "champion_pipeline_id": "9f2c41d8-…", "horizon_bins": 14, "…": "…" },
"credits_charged": 12,
"created_at": "2026-07-15T13:02:00+00:00"
}
}| Field | Meaning |
|---|---|
keys optional | Restrict the probe to specific drivers. Default: the goal's evidence-backed drivers, strongest first. |
magnitude_pct optional | The symmetric step size. Defaults to 10, clamped to 0.5–50. |
It's the priciest scenario variant — up to two champion inferences per driver — and the result is persisted, so re-viewing is free:
curl "$API_BASE/v1/goals/$GOAL_ID/scenarios/sensitivity" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Workspace-Id: $WORKSPACE_ID"from predictai import PredictAI
client = PredictAI(token="pa_live_…", workspace_id="ws_…")
data = client.goals.get_goals_by_goal_id_scenarios_sensitivity(GOAL_ID)
import { PredictAI } from "@predictai/sdk";
const client = new PredictAI({ token: "pa_live_…", workspaceId: "ws_…" });
const data = await client.goals.getGoalsByGoalIdScenariosSensitivity(GOAL_ID);
{ "data": { "sensitivity": { "sensitivity_id": "a90c…", "results": ["…"] } } }sensitivity is null if none has run yet.
Event timing sweep
If I can pick when an event happens, which timing moves the goal most? The sweep slides one injected event across every step of the horizon and reports the impact per candidate timing. The champion runs once (the baseline); each offset is then applied analytically from the stream's measured response curve — so the whole sweep is charged like a single scenario:
curl -X POST "$API_BASE/v1/goals/$GOAL_ID/scenarios/timing" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Workspace-Id: $WORKSPACE_ID" \
-H "Content-Type: application/json" \
-d '{ "stream": "promotions", "magnitude": 1, "direction": 1 }'from predictai import PredictAI
client = PredictAI(token="pa_live_…", workspace_id="ws_…")
data = client.goals.post_goals_by_goal_id_scenarios_timing(GOAL_ID, json={"stream": "promotions", "magnitude": 1, "direction": 1})
import { PredictAI } from "@predictai/sdk";
const client = new PredictAI({ token: "pa_live_…", workspaceId: "ws_…" });
const data = await client.goals.postGoalsByGoalIdScenariosTiming(GOAL_ID, {
json: { stream: "promotions", magnitude: 1, direction: 1 },
});
{
"data": {
"goal_id": "b7e9c2d4-…",
"stream": "promotions",
"magnitude": 1.0,
"direction": 1,
"horizon_bins": 14,
"interval_seconds": 86400,
"baseline_end": 14210.55,
"offsets": [
{ "offset_bins": 0, "at_ms": 1789459200000, "end_delta": 12.4, "peak_delta": 261.0, "total_delta": 903.1 },
{ "offset_bins": 1, "at_ms": 1789545600000, "end_delta": 19.8, "peak_delta": 261.0, "total_delta": 890.7 },
{ "…": "…" }
],
"best_end_offset": 11,
"best_peak_offset": 0,
"best_total_offset": 0,
"curve": { "edge_id": "7d3f…", "half_life_s": 172800, "curve_bins": 6, "scale": 261.0 },
"caveats": ["…", "Timing sweep applies the measured response curve analytically at each candidate offset — the champion ran once, on the baseline."],
"credits_charged": 5
}
}| Field | Meaning |
|---|---|
stream required | The event stream to sweep. Must hold a validated edge to this goal — the response curve is the measured edge. |
magnitude optional | Size versus a typical event, 0.1–5. Defaults to 1. |
direction optional | 1 or -1. |
Per offset: end_delta (impact on the final step), peak_delta (largest
single-step move), total_delta (area under the response).
best_end_offset / best_peak_offset / best_total_offset index the
offsets that maximize each — pick the one matching what you're optimizing.
A stream with no validated response curve returns 400
("Event response for '<stream>' could not be applied: …") and the charge
is refunded.
List & delete
curl "$API_BASE/v1/goals/$GOAL_ID/scenarios" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Workspace-Id: $WORKSPACE_ID"from predictai import PredictAI
client = PredictAI(token="pa_live_…", workspace_id="ws_…")
data = client.goals.get_goals_by_goal_id_scenarios(GOAL_ID)
import { PredictAI } from "@predictai/sdk";
const client = new PredictAI({ token: "pa_live_…", workspaceId: "ws_…" });
const data = await client.goals.getGoalsByGoalIdScenarios(GOAL_ID);
{ "data": { "scenarios": [{ "scenario_id": "d2f7…", "name": "Ad spend +10%", "…": "…" }] } }Saved scenarios are full documents (newest first) — rerunnable and comparable. Delete one by its ID (note the goal-less path):
curl -X DELETE "$API_BASE/v1/goals/scenarios/$SCENARIO_ID" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Workspace-Id: $WORKSPACE_ID"from predictai import PredictAI
client = PredictAI(token="pa_live_…", workspace_id="ws_…")
data = client.goals.delete_goals_scenarios_by_scenario_id(SCENARIO_ID)
import { PredictAI } from "@predictai/sdk";
const client = new PredictAI({ token: "pa_live_…", workspaceId: "ws_…" });
const data = await client.goals.deleteGoalsScenariosByScenarioId(SCENARIO_ID);
{ "data": { "deleted": true } }Returns 404 ("Scenario not found") for unknown IDs.

