Solo operator (design, build, run)

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
At a glance

System.

Interactive snapshot of how this project actually works.

Effective monthly capacity
Ceff = HCtenured × (1 − shrinkage − Mtax) × Pae + Σ(new_hire × ramp_factor × (1 − shrinkage) × Pae)
Mtax is capped and clamped — prevents front-loaded hiring from inverting tenured capacity into negative territory.
01
Foundation
Config Manager Data Loader Version Store
02
Modelling
Target Generator Marginal Economics AE Capacity
03
Reconciliation
Allocation Optimizer Validation Engine Recovery & Rebalancing
04
Analysis
What-If Engine Version Comparator Lever Analysis
Greedy
Sort by ROI → assign to ceiling. Fast, explainable.
SLSQP solver
scipy.optimize.minimize with greedy as warm-start. Default when cash-cycle weighting is on.
Read

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

LayerModules
FoundationConfig Manager · Data Loader · Version Store
ModellingTarget Generator · Marginal Economics · AE Capacity
ReconciliationAllocation Optimizer · Validation Engine · Recovery & Rebalancing
AnalysisWhat-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 solverscipy.optimize.minimize with 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.