What-if scenarios
Move a driver, replay an event, and watch the forecast respond — counterfactuals served by the live champion.
A forecast tells you what's coming. A scenario tells you what you can do about it: what happens to sales if we raise ad spend 10%? The intervention is applied to the serving data, derived features are recomputed, and the goal's champion re-infers — so the answer is the model's real response, not a rule of thumb.
You'll need: a goal with a live champion — the one from A goal on autopilot is perfect.
1. Find what you're allowed to move
Only drivers with validated relationships are simulatable — check drivers-now first:
from predictai import PredictAI
client = PredictAI(token="pa_live_...", workspace_id="ws_...")
drivers = client.goals.get_goals_by_goal_id_drivers_now(GOAL_ID)["data"]
levers = [d["key"] for d in drivers["drivers"] if d["simulatable"]]
print("you can move:", levers)
print("try:", drivers["scenario_examples"]["examples"])import { PredictAI } from "@predictai/sdk";
const client = new PredictAI({ token: "pa_live_...", workspaceId: "ws_..." });
const drivers = await client.goals.getGoalsByGoalIdDriversNow(GOAL_ID);
const levers = drivers.data.drivers.filter((d: any) => d.simulatable).map((d: any) => d.key);
console.log("you can move:", levers);
console.log("try:", drivers.data.scenario_examples.examples);you can move: ['foot_traffic', 'promo_active']
try: ['What if foot traffic rises 10%?']2. Run the counterfactual
Move a driver by percent (pct), absolute amount (abs), or standard
deviations (sigma), as a step (immediate) or a ramp (gradual). The
run is charged upfront and refunded if it fails; results are persisted,
so re-reading a saved scenario is free:
scenario = client.goals.post_goals_by_goal_id_scenarios(GOAL_ID, json={
"name": "Foot traffic +10%",
"overrides": [
{"key": "foot_traffic", "mode": "pct", "amount": 10, "shape": "step"},
],
})["data"]
for step, (base, delta) in enumerate(zip(scenario["baseline"]["point"], scenario["delta"])):
print(f'day {step + 1:2}: {base:9.1f} {delta:+8.1f}')
print("caveats:", scenario["caveats"][0])const { data: scenario } = await client.goals.postGoalsByGoalIdScenarios(GOAL_ID, {
json: {
name: "Foot traffic +10%",
overrides: [{ key: "foot_traffic", mode: "pct", amount: 10, shape: "step" }],
},
});
scenario.baseline.point.forEach((base: number, i: number) => {
console.log(`day ${i + 1}: ${base.toFixed(1)} ${scenario.delta[i] >= 0 ? "+" : ""}${scenario.delta[i].toFixed(1)}`);
});
console.log("caveats:", scenario.caveats[0]);day 1: 13102.4 +196.3
day 2: 13350.9 +266.6
day 3: 13571.2 +301.4
...
caveats: model response under intervention — a validated predictive relationship, not a causal guaranteeThe response carries the full provenance: baseline (the untouched
forecast), fan (the counterfactual), per-step delta, attribution
(which override drove the change), multi-hop chains when an upstream
move propagated, and honest caveats.
3. Inject an event instead
If the goal is connected to event streams, you can replay one — "what if a supply disruption hit tomorrow?" — using the stream's measured response curve:
shock = client.goals.post_goals_by_goal_id_scenarios(GOAL_ID, json={
"name": "Supply disruption, typical size",
"overrides": [],
"events": [
{"stream": "supply_disruptions", "magnitude": 1, "direction": -1},
],
})["data"]
print("worst step:", round(min(shock["delta"]), 1))const { data: shock } = await client.goals.postGoalsByGoalIdScenarios(GOAL_ID, {
json: {
name: "Supply disruption, typical size",
overrides: [],
events: [{ stream: "supply_disruptions", magnitude: 1, direction: -1 }],
},
});
console.log("worst step:", Math.min(...shock.delta).toFixed(1));4. Which lever matters most?
Instead of guessing one scenario at a time, sensitivity analysis probes every simulatable driver with a symmetric ±10% step and ranks them by end-of-horizon impact — the tornado chart, in one call:
sensitivity = client.goals.post_goals_by_goal_id_scenarios_sensitivity(
GOAL_ID, json={"magnitude_pct": 10},
)["data"]
print("baseline end of horizon:", sensitivity["baseline_end"])
for row in sensitivity["results"]:
print(f'{row["key"]:14} +10% → {row["up_delta"]:+8.1f} -10% → {row["down_delta"]:+8.1f}')const { data: sensitivity } = await client.goals.postGoalsByGoalIdScenariosSensitivity(GOAL_ID, {
json: { magnitude_pct: 10 },
});
console.log("baseline end of horizon:", sensitivity.baseline_end);
for (const row of sensitivity.results) {
console.log(`${row.key}: +10% → ${row.up_delta.toFixed(1)}, -10% → ${row.down_delta.toFixed(1)}`);
}baseline end of horizon: 14210.55
foot_traffic +10% → +271.4 -10% → -244.9
promo_active +10% → +118.2 -10% → -105.7The result is persisted — re-viewing via
GET /v1/goals/{goal_id}/scenarios/sensitivity is free.
Read the numbers honestly
Scenario output is the champion's response under intervention — a
validated predictive relationship, not a causal guarantee. The
caveats array in every response says exactly this, and attribution +
chains show you which relationships carried the effect so you can judge
them yourself. Saved scenarios stay listable at
GET /v1/goals/{goal_id}/scenarios — free to re-read, and each one can be
explained in plain language.

