GTM Planning Engine — a config-driven sales-capacity optimizer.
The interview artifact for the GTM Ops role I'm currently in
I can take a fuzzy GTM problem, formalise it into a constrained optimization, and ship the math, the API, and the UI as one defensible system.
Open live app ↗System.
Interactive snapshot of how this project actually works.
Depth selector.
Brief · what it is. Approach · how it works. Deep · why the calls.
Takes an annual revenue target, a seasonality curve, a hiring plan, and a set of unit-economics assumptions. Returns an allocation — how many SAOs to chase per channel × product × segment × month — that the field can execute against. Re-runs in seconds when any input changes. Mid-cycle misses get redistributed analytically, not by hand-waving.
Tier 2
The brief was “build the thing you’d want to be handed on day one.”
I read that as: most GTM planning lives in a brittle spreadsheet where formulas, assumptions, and scenario knobs are tangled into the same cells. Re-planning mid-cycle means re-deriving last quarter’s logic from scratch.
So I built the inverse — a YAML config holds every assumption (share floor and ceiling, decay rates, ramp duration, mentoring overhead, stretch threshold), a Python pipeline runs the same eight stages every time, and every plan run is a versioned artifact with the exact config snapshot that produced it.
The non-obvious call was treating decay as two independent curves: ASP gets exponential decay past one SAO threshold, win rate gets linear decay past a different threshold, each with its own floor multiplier.
Most allocation models conflate them into a single “efficiency” curve — which hides the lever you actually want to pull when a segment is saturating one factor but not the other.
Tier 3
Architecture · 13 modules, 4 layers
| Layer | Modules |
|---|---|
| Foundation | Config Manager · Data Loader · Version Store |
| Modelling | Target Generator · Marginal Economics · AE Capacity |
| Reconciliation | Allocation Optimizer · Validation Engine · Recovery & Rebalancing |
| Analysis | What-If Engine · Version Comparator · Lever Analysis Engine · Ad-Hoc Adjustment |
Eight modules form the pipeline run_plan.py walks every run. The other five are support — versioning, comparison, scenario perturbation, mid-cycle re-plan, gap attribution.
The Allocation Optimizer runs in two modes:
- Greedy — sorts segments by effective ROI, assigns share up to the ceiling. Fast, explainable in a meeting.
- SLSQP solver —
scipy.optimize.minimizewith the greedy result as warm start. Precise, handles non-linear decay interactions. Default when cash-cycle weighting is enabled.
When cash-cycle weighting is on, late-month allocation should steer toward the shorter-cycle products (CM, then Payroll, then EOR) — and only the solver captures that interaction analytically.
Math anchor · AE capacity
Where the system stops being a spreadsheet replacement and starts being a decision tool.
C_eff = max(0, HC_tenured × (1 − shrinkage − M_tax) × P_ae)
+ Σ(new_hire × ramp_factor × (1 − shrinkage) × P_ae)
The mentoring tax M_tax is the load-bearing term:
- Each ramping AE consumes
A% × (1 − days_in_ramp/Y)of a tenured AE’s time - Summed across all currently-ramping AEs
- Capped at
max_mentees_per_ae × tenured_hc - Clamped to
[0, 1]
That capped-and-clamped shape is what prevents an aggressive front-loaded hiring tranche from mathematically inverting tenured capacity into negative territory — which it absolutely would otherwise, because two big hiring tranches in months 1 and 2 produce a mentoring load that can briefly exceed total tenured time.
The Recovery & Rebalancing module asks the inverse question: if quarter N misses, redistribute the shortfall across remaining quarters weighted by their relative capacity, and flag any quarter where the adjusted target exceeds the stretch threshold (default 1.20× the original).
“Q3 can absorb 12% of the miss. Q4 can absorb 18%. The rest is real.”
Two backend variants — deliberate split
gtm_engine/ is the development surface — thirteen Python files, one per module, individually importable and testable, with run_plan.py orchestrating them.
gtm_monolith/ is a single 100KB+ engine.py that flattens the same logic into one importable surface, with a thin Flask wrapper (app_monolith.py) for Railway deployment.
The split exists because the container deploy surfaced too many cross-module import edges to be worth debugging in production. Collapsing to a monolith for the deploy artifact while keeping the modular package as the dev source was cheaper than fighting Python’s module resolution inside ephemeral Railway containers.
The frontend is a Vite / React / shadcn / Tailwind Lovable app fronting the Flask API with three tabs:
- Config — the YAML rendered as a form
- Results — a Plotly dashboard over the allocation output
- Docs — the architecture spec embedded
Click Launch engine on the public page and you’re inside the same surface I’d hand to a sales-ops lead.
What didn’t ship — and why deliberately not
The probabilistic forecasting layer.
The current system is deterministic — feed it targets, seasonality, decay parameters, a hiring plan; get an allocation. The honest next layer is three forecasters sitting upstream:
- Demand — Prophet-based SAO time series with strategic-bet overlays
- Economics — mix-shift models for ASP and win-rate evolution
- Capacity — stochastic hiring and attrition under empirical ramp distributions
These would produce P10 / P50 / P90 distributions instead of point estimates, feeding scenario-based planning with confidence intervals.
I scoped it, wrote the architecture for it, and deliberately didn’t build it.
The reason is the one most planning systems get wrong: probabilistic outputs without empirical decay-parameter calibration are theatre. The right sequence is to let the deterministic engine accumulate enough real plan-vs-actual cycles to calibrate the decay curves, the productivity-per-AE constant, and the confidence thresholds from actual data — then put a forecasting layer on top.
Shipping the forecaster first would have been the more impressive interview demo and the wrong system.