Finance Platform · Learn

Learn

Reference

Scenario overrides

A scenario is the plan's base inputs plus an ordered list of changes ("overrides"), such as "churn 20 % higher from July" or "hire a second engineer in March". Overrides never modify the stored inputs; the engine applies them to a copy for each run, so the base plan and every scenario can be compared side by side. This page gives the formulas for how an override changes a value, what each symbol means, when they hold and a worked example, followed by the catalogue of what can be overridden.

A scenario is the plan's base inputs plus an ordered list of changes ("overrides"), such as "churn 20 % higher from July" or "hire a second engineer in March". Overrides never modify the stored inputs; the engine applies them to a copy for each run, so the base plan and every scenario can be compared side by side. This page gives the formulas for how an override changes a value, what each symbol means, when they hold and a worked example, followed by the catalogue of what can be overridden.

How to read this page. Each formula is shown on its own line, followed by its symbols, the conditions under which it holds, what it says in plain words and a short example.

How an override changes a value

Set, multiply, add

\begin{gathered} v'(t) = \begin{cases} \text{value} & \text{set} \\ v(t) \cdot \text{value} & \text{multiply} \\ v(t) + \text{value} & \text{add} \end{cases} \\[8pt] \text{for } t \ge t_0, \\[8pt] v'(t) = v(t) \text{ for } t < t_0 \end{gathered}
Symbol Meaning Unit
v(t) The value in month t before this override (the base input, or the result of earlier overrides) the parameter's unit
v'(t) The value after this override the parameter's unit
value The override's value the parameter's unit, or a factor for multiply
t_0 Month index of effectiveFrom (0 when empty or before the model start) month

In plain words. An override replaces a value, scales it or adds to it, from a given month on. Earlier months keep the base value.

Valid when. The key is numeric and matches at least one entity (otherwise it is reported as ignored). Several overrides on the same field stack in list order: each one starts from the result of the previous one.

Example. Base churn 3 %. A multiply by 1.2 from month 6, then an add of 0.005 from month 9: churn is 3 % in months 0 to 5, 0.03 \times 1.2 = 3.6\,\% in months 6 to 8 and 3.6\,\% + 0.5\,\% = 4.1\,\% from month 9.

Overrides on amounts that grow

\text{amt}(t) = v'\,(1 + g)^{\,t - s}
Symbol Meaning Unit
v' The overridden base amount (amount of a cost driver, or leadsPerMonth) currency, or leads per month
g The item's monthly growth rate share per month
s The item's start month month

In plain words. For cost amounts and leads, the override changes the starting amount, and growth still applies on top. So set to 2,000 does not pin the amount at 2,000 when the item grows.

Example. A cost of €1,000 growing 2 % a month from month 0, overridden with set 2,000 from month 6: in month 6 it is 2{,}000 \times 1.02^6 = 2{,}252.32 euros, not 2,000.

Repricing existing customers

\text{ARPA}_{\text{cohort}}(t_0) \leftarrow \text{ARPA}_{\text{cohort}}(t_0 - 1) \cdot \frac{\text{netPrice}(t_0)}{\text{netPrice}(t_0 - 1)}

In plain words. Normally customers keep the price they signed up at, and a price change affects only new customers. With applyToExisting: true on price or discount_rate, every cohort of the matched plans moves by the same percentage as the price, in the month the change takes effect.

Valid when. t_0 > 0. An override effective in month 0 already sets the price of the starting customers, so there is nothing to reprice.

Example. The price goes from €100 to €110 in month 6 with applyToExisting: a cohort paying €90 after a discount pays 90 \times 1.1 = 99 euros from month 6.

Override shape

Field (JSON) Type Meaning
assumptionKey string Key from the catalogue below, or assumption.<name>
operation set | multiply | add Default set. set: v = value, multiply: v = base * value, add: v = base + value
value any JSON Number (or numeric string) for numeric keys; date/month string or object for structural keys
targetId string | null Restrict to one entity id; null = all matching entities
effectiveFrom date | null First month the change applies (month index, clamped to >= 0); null = from month 0
applyToExisting bool Price/discount only: also reprice existing customers (default false)
note string Free text, not used in calculation

Catalogue

Numeric (time-varying) keys. Rules are resolved per month by OverrideBook.value(kind, field, base, t, entityId, category, employmentType).

assumptionKey (aliases) Target Filter
price, price_plan.list_price_per_month price plan listPricePerMonth
discount_rate price plan discountRate
one_time_fee, usage_price_per_unit, usage_units_per_customer, new_customer_mix price plan field of same name
monthly_churn_rate, churn price plan monthlyChurnRate
monthly_expansion_rate price plan monthlyExpansionRate
conversion_rate, leads_per_month, monthly_lead_growth_rate revenue driver field
cost_driver.amount cost driver amount any category
api_cost_per_customer, ai_api_cost cost driver amount category ai_api
cloud_cost / marketing_spend / software_cost / contractor_cost cost driver amount cloud_hosting / marketing / software / contractors
salary_cost, headcount.monthly_cost headcount monthlyCost
contractor_fee headcount monthlyCost employmentType = contractor

Structural keys (applied once to a deep copy of the input before the run):

assumptionKey Effect targetId
headcount.add Appends a HeadcountPlan built from value (object) optional, used as the new id
hiring_start_date, headcount.start_date Sets startDate to value required
headcount.end_date Sets endDate to value optional (null = all hires)
headcount.remove With effectiveFrom: endDate = effectiveFrom - 1 day; without: entry removed required
funding_event.add Appends a FundingEvent built from value optional, used as the new id
debt.add Appends a DebtInstrument (a loan with interest and repayment) built from value optional, used as the new id
funding_event.amount amount = apply(operation, amount, value) optional (null = all events)
funding_event.date Sets date to value optional (null = all events)
funding_event.remove Sets includeInForecast = false optional (null = all events)

Assumption keys: payment_delay_days, payables_delay_days, refund_rate, employer_overhead_rate, corporate_tax_rate, minimum_cash_buffer, plus any assumption.<name> (camelCase is converted to snake_case, e.g. assumption.annualSalaryIncreaseRate).

Semantics

Validation results

apply_overrides returns one entry per override: {override, status, message, matchedIds} with status applied or ignored.

Message Cause
unknown assumptionKey '<key>' Key not in catalogue and not assumption.*
unknown assumption '<field>' assumption.<name> that is not an Assumptions field
no matching entity for override Numeric key matched no entity
invalid override value: <error> ValueError/TypeError/KeyError, e.g. non-numeric value, no matching headcount entry, no matching funding event, targetId is required for this headcount override, or a model validation error from headcount.add/funding_event.add
structural change applied, <field> -> <value>, <n> entities affected Applied

POST /v1/forecasts still runs when some overrides are ignored; they are listed in appliedOverrides. POST /v1/overrides/validate returns valid = all applied.

Control plane (PlanningService.normalizeOverrides, validateOverrides): assumptionKey is required and operation must be set|multiply|add (400 otherwise); blank targetId becomes null; applyToExisting defaults to false. On scenario create/update the overrides are validated by the engine against the scenario's input, and any ignored result rejects the request with 422 "Some scenario overrides cannot be applied". Preview runs and runScenario do not re-validate.

Scenario lifecycle (control plane)

PlanningService:

Run snapshot and hashes

PlanningService.executeRun sends {input, scenario: {id, name, kind}, overrides} to POST /v1/forecasts and stores:

Table.column Content
forecast_runs.scenario_id, scenario_version, scenario_name, scenario_kind Scenario at run time (null id for previews)
assumption_set_id Pinned set, or null for live inputs
run_kind scenario or preview
model_version result.modelVersion (= meta.modelVersion)
engine_version result.engineVersion
input_snapshot The full engine request (input + scenario ref + overrides)
input_hash result.inputHash, computed by the engine
summary result.summary plus warnings and appliedOverrides
calculated_at Engine timestamp (UTC)
source, created_by, created_at Actor and insert time
forecast_results.result The complete ForecastResult JSON (series, lines, summary, effective input)

A forecast.run audit event records scenario id/name, model and engine version, input hash, overrides, runwayMonths and minimumCash. Runs are not updated afterwards except for the plan-baseline columns (is_plan_baseline, plan_approved_by, plan_approved_at) set by markPlanBaseline, which only accepts runs of approved scenarios.

Two different hashes exist:

Hash Computed by Over Excludes
forecast_runs.input_hash engine forecast.input_hash Pydantic-normalised request (defaults filled, decimals as strings, camelCase), json.dumps(sort_keys=True, separators=(",", ":")), SHA-256 nothing in the request; calculatedAt is not part of the request
assumption_sets.snapshot_hash and working-status hash control plane ModelInputBuilder.contentHash Snapshot JSON with sorted keys (core/Json.kt canonical), SHA-256 meta.modelVersion, meta.asOfMonth

Consequences: the run hash changes with the scenario name/kind and with asOfMonth, which moves with the calendar, so re-running an unchanged pinned scenario in a later month gives a different input_hash. The two hashes are not comparable with each other.

For developers

Sources: apps/finance-engine/src/finance_engine/calc/overrides.py (CATALOGUE, resolve_target, apply_overrides, OverrideBook, _apply_structural), models/inputs.py (ScenarioOverride), and in the control plane model/Planning.kt (PlanningService) and model/ModelInputBuilder.kt. Scenario and run persistence lives in model/Planning.kt. Keep this page in sync with any change there.

Markdown · version 215ed0340802