cmo-advisor
by Alireza Rezvani · MIT
Marketing leadership for scaling companies. Brand positioning, growth model design, marketing budget allocation, and marketing org design. Use when designing brand strategy, selecting growth models (PLG vs sales-led vs community-led), allocating marketing budgets, building marketing teams, or when user mentions CMO, brand strategy, growth model, CAC, LTV, channel mix, or marketing ROI.
Labels from its files: runs commands. 6 files, read Oct 23, 2026. Nothing was run.
npx skills add https://github.com/alirezarezvani/claude-skills --skill cmo-advisorWhat it lets an agent do
Each label, and the line in the skill's files that set it.
- Runs commands
Allows a shell or ships scripts the agent runs.
- It ships 2 scripts the agent can run.
Findings
Matches of fixed checks, with the text they matched.
No check matched anything in this skill's files.
Not a safety rating, and no finding is not a safety guarantee. These are fixed checks of the files as read on Oct 23, 2026. Nothing was run. How skills are checked.
SKILL.md
As the publisher wrote it. Shown as text, never rendered.
--- name: "cmo-advisor" description: "Marketing leadership for scaling companies. Brand positioning, growth model design, marketing budget allocation, and marketing org design. Use when designing brand strategy, selecting growth models (PLG vs sales-led vs community-led), allocating marketing budgets, building marketing teams, or when user mentions CMO, brand strategy, growth model, CAC, LTV, channel mix, or marketing ROI." license: MIT metadata: version: 1.0.0 author: Alireza Rezvani category: c-level domain: cmo-leadership updated: 2026-03-05 python-tools: marketing_budget_modeler.py, growth_model_simulator.py frameworks: brand-positioning, growth-frameworks, marketing-org --- # CMO Advisor Strategic marketing leadership — brand positioning, growth model design, budget allocation, and org design. Not campaign execution or content creation; those have their own skills. This is the engine. ## Keywords CMO, chief marketing officer, brand strategy, brand positioning, growth model, product-led growth, PLG, sales-led growth, community-led growth, marketing budget, CAC, customer acquisition cost, LTV, lifetime value, channel mix, marketing ROI, pipeline contribution, marketing org, category design, competitive positioning, growth loops, payback period, MQL, pipeline coverage ## Quick Start ```bash # Model budget allocation across channels, project MQL output by scenario python scripts/marketing_budget_modeler.py # Project MRR growth by model, show impact of channel mix shifts python scripts/growth_model_simulator.py ``` **Reference docs (load when needed):** - `references/brand_positioning.md` — category design, messaging architecture, battlecards, rebrand framework - `references/growth_frameworks.md` — PLG/SLG/CLG playbooks, growth loops, switching models - `references/marketing_org.md` — team structure by stage, hiring sequence, agency vs. in-house --- ## The Four CMO Questions Every CMO must own answers to these — no one else in the C-suite can: 1. **Who are we for?** — ICP, positioning, category 2. **Why do they choose us?** — Differentiation, messaging, brand 3. **How do they find us?** — Growth model, channel mix, demand gen 4. **Is it working?** — CAC, LTV:CAC, pipeline contribution, payback period --- ## Core Responsibilities (Brief) **Brand & Positioning** — Define category, build messaging architecture, maintain competitive differentiation. Details → `references/brand_positioning.md` **Growth Model** — Choose and operate the right acquisition engine: PLG, sales-led, community-led, or hybrid. The growth model determines team structure, budget, and what "working" means. Details → `references/growth_frameworks.md` **Marketing Budget** — Allocate from revenue target backward: new customers needed → conversion rates by stage → MQLs needed → spend by channel based on CAC. Run `marketing_budget_modeler.py` for scenarios. **Marketing Org** — Structure follows growth model. Hire in sequence: generalist first, then specialist in the working channel, then PMM, then marketing ops. Details → `references/marketing_org.md` **Channel Mix** — Audit quarterly: MQLs, cost, CAC, payback, trend. Scale what's improving. Cut what's worsening. Don't optimize a channel that isn't in the strategy. **Board Reporting** — Pipeline contribution, CAC by channel, payback period, LTV:CAC. Not impressions. Not MQLs in isolation. --- ## Key Diagnostic Questions Ask these before making any strategic recommendation: - What's your CAC **by channel** (not blended)? - What's the payback period on your largest channel? - What's your LTV:CAC ratio? - What % of pipeline is marketing-sourced vs. sales-sourced? - Where do your **best customers** (highest LTV, lowest churn) come from? - What's your MQL → Opportunity conversion rate? (proxy for lead quality) - Is this brand work or performance marketing? (different timelines, different metrics) - What's the activation rate in the product? (PLG signal) - If a prospect doesn't buy, why not? (win/loss data) --- ## CMO Metrics Dashboard | Category | Metric | Healthy Target | |----------|--------|---------------| | **Pipeline** | Marketing-sourced pipeline % | 50–70% of total | | **Pipeline** | Pipeline coverage ratio | 3–4x quarterly quota | | **Pipeline** | MQL → Opportunity rate | > 15% | | **Efficiency** | Blended CAC payback | < 18 months | | **Efficiency** | LTV:CAC ratio | > 3:1 | | **Efficiency** | Marketing % of total S&M spend | 30–50% | | **Growth** | Brand search volume trend | ↑ QoQ | | **Growth** | Win rate vs. primary competitor | > 50% | | **Retention** | NPS (marketing-sourced cohort) | > 40 | --- ## Red Flags - No defined ICP — "companies with 50-1000 employees" is not an ICP - Marketing and sales disagree on what an MQL is (this is always a system problem, not a people problem) - CAC tracked only as a blended number — channel-level CAC is non-negotiable - Pipeline attribution is self-reported by sales reps, not CRM-timestamped - CMO can't answer "what's our payback period?" without a 48-hour research project - Brand work and performance marketing have no shared narrative — they're contradicting each other - Marketing team is producing content with no documented positioning to anchor it - Growth model was chosen because a competitor uses it, not because the product/ACV/ICP fits --- ## Integration with Other C-Suite Roles | When... | CMO works with... | To... | |---------|-------------------|-------| | Pricing changes | CFO + CEO | Understand margin impact on positioning and messaging | | Product launch | CPO + CTO | Define launch tier, GTM motion, messaging | | Pipeline miss | CFO + CRO | Diagnose: volume problem, quality problem, or velocity problem | | Category design | CEO | Secure multi-year organizational commitment to the narrative | | New market entry | CEO + CFO | Validate ICP, budget, localization requirements | | Sales misalignment | CRO | Align on MQL definition, SLA, and pipeline ownership | | Hiring plan | CHRO | Define marketing headcount and skill profile by stage | | Retention insights | CCO | Use expansion and churn data to sharpen ICP and messaging | | Competitive threat | CEO + CRO | Coordinate battlecards, win/loss, repositioning response | --- ## Resources - **References:** `references/brand_positioning.md`, `references/growth_frameworks.md`, `references/marketing_org.md` - **Scripts:** `scripts/marketing_budget_modeler.py`, `scripts/growth_model_simulator.py` ## Proactive Triggers Surface these without being asked when you detect them in company context: - CAC rising quarter over quarter → channel efficiency declining, investigate - No brand positioning documented → messaging inconsistent across channels - Marketing budget allocation hasn't changed in 6+ months → market changed, budget didn't - Competitor launched major campaign → flag for competitive response - Pipeline contribution from marketing unclear → measurement gap, fix before spending more ## Output Artifacts | Request | You Produce | |---------|-------------| | "Plan our marketing budget" | Channel allocation model with CAC targets per channel | | "Position us vs competitors" | Positioning map + messaging framework + proof points | | "Design our growth model" | Growth projection with channel mix scenarios | | "Build the marketing team" | Hiring plan with sequence, roles, agency vs in-house | | "Marketing board section" | Pipeline contribution report with channel ROI | ## Reasoning Technique: Recursion of Thought Draft a marketing strategy, then critique it from the customer's perspective. Refine based on the critique. Repeat until the strategy survives scrutiny. ## Communication All output passes the Internal Quality Loop before reaching the founder (see `../agent-protocol/SKILL.md`). - Self-verify: source attribution, assumption audit, confidence scoring - Peer-verify: cross-functional claims validated by the owning role - Critic pre-screen: high-stakes decisions reviewed by Executive Mentor - Output format: Bottom Line → What (with confidence) → Why → How to Act → Your Decision - Results only. Every finding tagged: 🟢 verified, 🟡 medium, 🔴 assumed. ## Context Integration - **Always** read `company-context.md` before responding (if it exists) - **During board meetings:** Use only your own analysis in Phase 2 (no cross-pollination) - **Invocation:** You can request input from other roles: `[INVOKE:role|question]`
Scripts (2)
Read as text. anlyon.dev never runs them.
scripts/growth_model_simulator.pyShowHide
#!/usr/bin/env python3
"""
Growth Model Simulator
----------------------
Projects MRR growth across different growth models (PLG, sales-led, community-led,
hybrid) and shows the impact of channel mix changes on growth trajectory.
Usage:
python growth_model_simulator.py
Inputs (edit INPUTS section):
- Starting MRR and churn rate
- Current channel mix (% of new MRR from each source)
- Conversion rates per model
- Growth rate assumptions per channel
Outputs:
- 12-month MRR projection by growth model
- Channel mix impact analysis (what happens if you shift mix)
- Break-even months for each model
- Side-by-side comparison table
"""
from __future__ import annotations
import math
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Tuple
# ---------------------------------------------------------------------------
# Data models
# ---------------------------------------------------------------------------
@dataclass
class ChannelSource:
name: str
pct_of_new_mrr: float # Current share of new MRR (0.0–1.0)
monthly_growth_rate: float # How fast this channel grows month-over-month
cac: float # CAC in dollars
payback_months: float # Months to recover CAC
@dataclass
class GrowthModel:
name: str
description: str
channel_mix: Dict[str, float] # channel name → % of new MRR
new_mrr_monthly_base: float # Starting new MRR/month from this model
monthly_acceleration: float # Acceleration factor (compounding)
avg_ltv_cac: float # Expected LTV:CAC at scale
months_to_steady_state: int # Months before model hits its natural growth rate
notes: List[str] = field(default_factory=list)
@dataclass
class MonthSnapshot:
month: int
mrr: float
new_mrr: float
churned_mrr: float
expansion_mrr: float
net_new_mrr: float
cumulative_cac_spend: float
@dataclass
class ModelProjection:
model: GrowthModel
snapshots: List[MonthSnapshot]
break_even_month: Optional[int] # Month when cumulative revenue > cumulative CAC
# ---------------------------------------------------------------------------
# INPUTS — edit these
# ---------------------------------------------------------------------------
STARTING_MRR = 85_000 # Current MRR ($)
MONTHLY_CHURN_RATE = 0.012 # Monthly churn rate (1.2% = ~14% annual)
EXPANSION_RATE = 0.008 # Monthly expansion MRR as % of existing MRR
GROSS_MARGIN = 0.75
SIMULATION_MONTHS = 18
# Channel sources (used to model mix shift scenarios)
CHANNELS: List[ChannelSource] = [
ChannelSource("Organic/SEO", pct_of_new_mrr=0.28, monthly_growth_rate=0.04, cac=1_800, payback_months=9),
ChannelSource("PLG Self-Serve", pct_of_new_mrr=0.15, monthly_growth_rate=0.08, cac=900, payback_months=5),
ChannelSource("Outbound SDR", pct_of_new_mrr=0.25, monthly_growth_rate=0.02, cac=5_100, payback_months=21),
ChannelSource("Paid Search", pct_of_new_mrr=0.15, monthly_growth_rate=0.01, cac=6_200, payback_months=26),
ChannelSource("Events/Field", pct_of_new_mrr=0.08, monthly_growth_rate=0.01, cac=9_800, payback_months=41),
ChannelSource("Partner/Channel", pct_of_new_mrr=0.09, monthly_growth_rate=0.05, cac=3_400, payback_months=14),
]
# Growth models to simulate
GROWTH_MODELS: List[GrowthModel] = [
GrowthModel(
name="Current Mix",
description="Baseline — maintain current channel allocation",
channel_mix={"Organic/SEO": 0.28, "PLG Self-Serve": 0.15, "Outbound SDR": 0.25,
"Paid Search": 0.15, "Events/Field": 0.08, "Partner/Channel": 0.09},
new_mrr_monthly_base=12_000,
monthly_acceleration=0.025,
avg_ltv_cac=3.2,
months_to_steady_state=3,
notes=["Baseline. No changes to channel mix."],
),
GrowthModel(
name="PLG-First",
description="Shift budget toward PLG self-serve and organic; reduce paid and outbound",
channel_mix={"Organic/SEO": 0.35, "PLG Self-Serve": 0.35, "Outbound SDR": 0.10,
"Paid Search": 0.08, "Events/Field": 0.04, "Partner/Channel": 0.08},
new_mrr_monthly_base=9_500, # Slower start — PLG takes time to activate
monthly_acceleration=0.048, # But compounds faster
avg_ltv_cac=5.8,
months_to_steady_state=6, # PLG loops take time to build
notes=[
"Lower new MRR in months 1-6 while PLG loops activate.",
"Acceleration compounds strongly after month 6.",
"Requires product investment in activation/onboarding.",
"Best fit if time-to-value < 30 min and viral coefficient > 0.3.",
],
),
GrowthModel(
name="Sales-Led Scale",
description="Double down on outbound SDR and field; optimize for enterprise ACV",
channel_mix={"Organic/SEO": 0.20, "PLG Self-Serve": 0.05, "Outbound SDR": 0.40,
"Paid Search": 0.15, "Events/Field": 0.15, "Partner/Channel": 0.05},
new_mrr_monthly_base=15_000, # Higher new MRR from enterprise ACV
monthly_acceleration=0.018, # Linear growth — headcount-constrained
avg_ltv_cac=2.8,
months_to_steady_state=2,
notes=[
"Fastest short-term new MRR if ACV > $30K.",
"Growth is linear — adds headcount to add pipeline.",
"CAC and payback worsen as SDR market tightens.",
"Requires sales capacity increase to sustain.",
],
),
GrowthModel(
name="Community-Led",
description="Invest in community and content; reduce paid; long-term brand play",
channel_mix={"Organic/SEO": 0.45, "PLG Self-Serve": 0.15, "Outbound SDR": 0.15,
"Paid Search": 0.05, "Events/Field": 0.10, "Partner/Channel": 0.10},
new_mrr_monthly_base=7_000, # Slowest start
monthly_acceleration=0.038,
avg_ltv_cac=4.5,
months_to_steady_state=9, # Community takes longest to activate
notes=[
"Lowest new MRR in months 1-9.",
"Community trust drives lower CAC and higher retention at scale.",
"Best for categories where buyers seek peer validation.",
"Requires dedicated community manager from day one.",
],
),
GrowthModel(
name="Hybrid PLS",
description="PLG self-serve for SMB + sales-assisted for enterprise (Product-Led Sales)",
channel_mix={"Organic/SEO": 0.30, "PLG Self-Serve": 0.28, "Outbound SDR": 0.22,
"Paid Search": 0.08, "Events/Field": 0.06, "Partner/Channel": 0.06},
new_mrr_monthly_base=11_000,
monthly_acceleration=0.035,
avg_ltv_cac=4.1,
months_to_steady_state=4,
notes=[
"PLG handles SMB; sales closes enterprise with PQL signals.",
"Requires clear PQL definition and SDR/PLG handoff process.",
"Best if you have a product with both bottom-up and top-down adoption.",
],
),
]
# ---------------------------------------------------------------------------
# Simulation engine
# ---------------------------------------------------------------------------
def simulate_model(model: GrowthModel, months: int) -> ModelProjection:
snapshots: List[MonthSnapshot] = []
mrr = STARTING_MRR
cumulative_cac = 0.0
cumulative_revenue = 0.0
break_even_month = None
for m in range(1, months + 1):
# Ramp up — new_mrr accelerates each month
if m <= model.months_to_steady_state:
# Ramp phase: linear ramp from 60% to 100% of base
ramp_factor = 0.6 + 0.4 * (m / model.months_to_steady_state)
else:
# Steady state: compound acceleration
months_past_ramp = m - model.months_to_steady_state
ramp_factor = 1.0 + model.monthly_acceleration * months_past_ramp
new_mrr = model.new_mrr_monthly_base * ramp_factor
churned_mrr = mrr * MONTHLY_CHURN_RATE
expansion_mrr = mrr * EXPANSION_RATE
net_new_mrr = new_mrr - churned_mrr + expansion_mrr
mrr = mrr + net_new_mrr
# CAC spend approximation: new_mrr / (avg_deal_mrr) * blended_cac
# Use weighted CAC from channel mix
weighted_cac = _weighted_cac(model.channel_mix)
avg_deal_mrr = 1_500 # Assumption: $1,500 average deal MRR
deals_this_month = new_mrr / avg_deal_mrr
cac_spend = deals_this_month * weighted_cac
cumulative_cac += cac_spend
cumulative_revenue += mrr * GROSS_MARGIN
if break_even_month is None and cumulative_revenue >= cumulative_cac:
break_even_month = m
snapshots.append(MonthSnapshot(
month=m,
mrr=mrr,
new_mrr=new_mrr,
churned_mrr=churned_mrr,
expansion_mrr=expansion_mrr,
net_new_mrr=net_new_mrr,
cumulative_cac_spend=cumulative_cac,
))
return ModelProjection(
model=model,
snapshots=snapshots,
break_even_month=break_even_month,
)
def _weighted_cac(channel_mix: Dict[str, float]) -> float:
channel_cac = {ch.name: ch.cac for ch in CHANNELS}
total = sum(
channel_mix.get(name, 0) * cac
for name, cac in channel_cac.items()
)
weight_sum = sum(channel_mix.values())
return total / weight_sum if weight_sum > 0 else 5_000
# ---------------------------------------------------------------------------
# Reporting
# ---------------------------------------------------------------------------
def fmt_mrr(n: float) -> str:
if n >= 1_000_000:
return f"${n/1_000_000:.3f}M"
return f"${n/1_000:.1f}K"
def fmt_currency(n: float) -> str:
if n >= 1_000_000:
return f"${n/1_000_000:.2f}M"
if n >= 1_000:
return f"${n/1_000:.1f}K"
return f"${n:.0f}"
def print_header(title: str) -> None:
width = 78
print("\n" + "=" * width)
print(f" {title}")
print("=" * width)
def print_channel_overview() -> None:
print_header("Current Channel Mix")
print(f" Starting MRR: {fmt_mrr(STARTING_MRR)} | Monthly churn: {MONTHLY_CHURN_RATE:.1%} | Expansion: {EXPANSION_RATE:.1%}/mo")
print()
print(f" {'Channel':<22} {'% MRR':>7} {'CAC':>8} {'Payback':>9} {'Growth/mo':>10}")
print(" " + "-" * 60)
for ch in sorted(CHANNELS, key=lambda c: c.pct_of_new_mrr, reverse=True):
print(
f" {ch.name:<22} {ch.pct_of_new_mrr:>6.0%} "
f"{fmt_currency(ch.cac):>8} {ch.payback_months:>7.0f}mo "
f"{ch.monthly_growth_rate:>9.1%}"
)
def print_model_detail(proj: ModelProjection) -> None:
model = proj.model
print_header(f"Model: {model.name}")
print(f" {model.description}")
if model.notes:
print()
for note in model.notes:
print(f" • {note}")
print()
# Print monthly snapshot (every 3 months + final)
milestones = set(range(3, SIMULATION_MONTHS + 1, 3)) | {SIMULATION_MONTHS}
print(f" {'Month':<7} {'MRR':>10} {'New MRR':>9} {'Churned':>9} {'Expand':>8} {'Net New':>9}")
print(" " + "-" * 56)
for snap in proj.snapshots:
if snap.month in milestones:
print(
f" {snap.month:<7} {fmt_mrr(snap.mrr):>10} "
f"{fmt_mrr(snap.new_mrr):>9} {fmt_mrr(snap.churned_mrr):>9} "
f"{fmt_mrr(snap.expansion_mrr):>8} {fmt_mrr(snap.net_new_mrr):>9}"
)
final = proj.snapshots[-1]
growth_x = final.mrr / STARTING_MRR
arr_final = final.mrr * 12
weighted_cac = _weighted_cac(model.channel_mix)
be = f"Month {proj.break_even_month}" if proj.break_even_month else f"> {SIMULATION_MONTHS}mo"
print()
print(f" Final MRR ({SIMULATION_MONTHS}mo): {fmt_mrr(final.mrr)}")
print(f" Final ARR: {fmt_currency(arr_final)}")
print(f" Growth multiple: {growth_x:.1f}x from starting MRR")
print(f" Weighted blended CAC: {fmt_currency(weighted_cac)}")
print(f" Expected LTV:CAC: {model.avg_ltv_cac:.1f}x")
print(f" Months to steady state:{model.months_to_steady_state}")
print(f" CAC break-even: {be}")
def print_comparison_table(projections: List[ModelProjection]) -> None:
print_header(f"Growth Model Comparison — Month {SIMULATION_MONTHS} Outcomes")
header = (
f" {'Model':<20} {'MRR (final)':>12} {'ARR (final)':>12} "
f"{'Growth':>7} {'LTV:CAC':>8} {'Break-even':>11}"
)
print(header)
print(" " + "-" * 74)
for proj in sorted(projections, key=lambda p: p.snapshots[-1].mrr, reverse=True):
final = proj.snapshots[-1]
growth_x = final.mrr / STARTING_MRR
arr_final = final.mrr * 12
be = f"Mo {proj.break_even_month}" if proj.break_even_month else f">{SIMULATION_MONTHS}mo"
print(
f" {proj.model.name:<20} {fmt_mrr(final.mrr):>12} "
f"{fmt_currency(arr_final):>12} {growth_x:>6.1f}x "
f"{proj.model.avg_ltv_cac:>7.1f}x {be:>11}"
)
def print_channel_mix_impact(projections: List[ModelProjection]) -> None:
print_header("Channel Mix Impact Analysis")
print(" How shifting channel mix changes growth trajectory:\n")
baseline = next((p for p in projections if p.model.name == "Current Mix"), None)
if not baseline:
return
baseline_final_mrr = baseline.snapshots[-1].mrr
for proj in projections:
if proj.model.name == "Current Mix":
continue
final_mrr = proj.snapshots[-1].mrr
delta = final_mrr - baseline_final_mrr
delta_pct = (delta / baseline_final_mrr) * 100
arrow = "↑" if delta > 0 else "↓"
m6_mrr = proj.snapshots[5].mrr if len(proj.snapshots) >= 6 else 0
m6_baseline = baseline.snapshots[5].mrr if len(baseline.snapshots) >= 6 else 0
m6_delta = m6_mrr - m6_baseline
m6_pct = (m6_delta / m6_baseline) * 100 if m6_baseline else 0
m6_arrow = "↑" if m6_delta > 0 else "↓"
print(f" {proj.model.name}:")
print(f" Month 6: {m6_arrow} {abs(m6_pct):.1f}% vs. current ({fmt_mrr(m6_delta)} {'more' if m6_delta > 0 else 'less'} MRR)")
print(f" Month {SIMULATION_MONTHS}: {arrow} {abs(delta_pct):.1f}% vs. current ({fmt_mrr(delta)} {'more' if delta > 0 else 'less'} MRR)")
if proj.model.months_to_steady_state > 4:
print(f" ⚠ Model takes {proj.model.months_to_steady_state} months to reach steady state — short-term dip expected.")
print()
def print_decision_guide(projections: List[ModelProjection]) -> None:
print_header("Decision Guide")
print(" Choose your growth model based on your constraints:\n")
guides = [
("ACV < $5K and fast time-to-value", "PLG-First"),
("ACV > $25K and complex buying process", "Sales-Led Scale"),
("Strong practitioner community exists", "Community-Led"),
("Both SMB self-serve and enterprise buyers", "Hybrid PLS"),
("Uncertain — keep optionality", "Current Mix"),
]
for condition, model_name in guides:
proj = next((p for p in projections if p.model.name == model_name), None)
if proj:
final_mrr = proj.snapshots[-1].mrr
print(f" If: {condition}")
print(f" → Use {model_name} → {fmt_mrr(final_mrr)} MRR at month {SIMULATION_MONTHS}")
print()
print(" Key question before switching models:")
print(" 'Do we have 12-18 months of runway to prove the new model")
print(" while the current model continues in parallel?'")
print(" If no → optimize current model. Don't switch.")
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main() -> None:
print_channel_overview()
projections = [simulate_model(model, SIMULATION_MONTHS) for model in GROWTH_MODELS]
for proj in projections:
print_model_detail(proj)
print_comparison_table(projections)
print_channel_mix_impact(projections)
print_decision_guide(projections)
print("\n" + "=" * 78)
print(" Notes:")
print(f" Starting MRR: {fmt_mrr(STARTING_MRR)}")
print(f" Simulation: {SIMULATION_MONTHS} months")
print(f" Churn: {MONTHLY_CHURN_RATE:.1%}/mo ({MONTHLY_CHURN_RATE*12:.0%} annualized)")
print(f" Expansion: {EXPANSION_RATE:.1%}/mo of existing MRR")
print(f" Gross margin: {GROSS_MARGIN:.0%}")
print(" Acceleration rates are estimates — validate against your actuals.")
print("=" * 78 + "\n")
if __name__ == "__main__":
main()
scripts/marketing_budget_modeler.pyShowHide
#!/usr/bin/env python3
"""
Marketing Budget Modeler
------------------------
Allocates marketing budget across channels based on CAC efficiency and
target MQL volume. Models conservative / moderate / aggressive scenarios.
Usage:
python marketing_budget_modeler.py
Inputs (edit INPUTS section below or extend with argparse):
- Annual revenue target (new ARR)
- Average selling price (ASP)
- Conversion rates by funnel stage
- Historical CAC per channel
- Channel capacity constraints (max MQLs the channel can realistically produce)
Outputs:
- Required MQL volume by channel
- Budget allocation per channel per scenario
- LTV:CAC and payback period per channel
- Summary table across scenarios
"""
from __future__ import annotations
import math
from dataclasses import dataclass, field
from typing import Dict, List, Tuple
# ---------------------------------------------------------------------------
# Data models
# ---------------------------------------------------------------------------
@dataclass
class Channel:
name: str
cac: float # Customer acquisition cost ($)
max_mqls_per_month: int # Realistic capacity ceiling (MQLs/month)
mql_to_close_rate: float # Combined MQL → closed-won rate (0.0–1.0)
payback_months: float # Based on ARPU × gross margin
ltv: float # Lifetime value ($)
trend: str = "stable" # "improving" | "stable" | "declining"
@dataclass
class FunnelRates:
mql_to_sal: float # MQL → Sales Accepted Lead
sal_to_sql: float # SAL → Sales Qualified Lead
sql_to_opp: float # SQL → Opportunity
opp_to_close: float # Opportunity → Closed-Won
@property
def mql_to_close(self) -> float:
return self.mql_to_sal * self.sal_to_sql * self.sql_to_opp * self.opp_to_close
@dataclass
class ScenarioResult:
name: str
total_budget: float
channel_budgets: Dict[str, float]
channel_mqls: Dict[str, int]
projected_customers: int
projected_arr: float
blended_cac: float
notes: List[str] = field(default_factory=list)
# ---------------------------------------------------------------------------
# INPUTS — edit these
# ---------------------------------------------------------------------------
TARGET_NEW_ARR = 3_000_000 # New ARR to generate this year ($)
ASP_ANNUAL = 18_000 # Average annual contract value ($)
GROSS_MARGIN = 0.75 # Product gross margin (%)
ARPU_MONTHLY = ASP_ANNUAL / 12 # Monthly revenue per account
FUNNEL = FunnelRates(
mql_to_sal=0.65,
sal_to_sql=0.45,
sql_to_opp=0.75,
opp_to_close=0.27,
)
# LTV = ARPU_monthly × gross_margin / monthly_churn_rate
MONTHLY_CHURN = 0.012 # ~14% annual churn
LTV = (ARPU_MONTHLY * GROSS_MARGIN) / MONTHLY_CHURN
CHANNELS: List[Channel] = [
Channel(
name="Organic SEO",
cac=1_800,
max_mqls_per_month=80,
mql_to_close_rate=FUNNEL.mql_to_close,
payback_months=(1_800 / (ARPU_MONTHLY * GROSS_MARGIN)),
ltv=LTV,
trend="improving",
),
Channel(
name="Paid Search",
cac=6_200,
max_mqls_per_month=60,
mql_to_close_rate=FUNNEL.mql_to_close,
payback_months=(6_200 / (ARPU_MONTHLY * GROSS_MARGIN)),
ltv=LTV,
trend="stable",
),
Channel(
name="Paid Social (LinkedIn)",
cac=8_500,
max_mqls_per_month=35,
mql_to_close_rate=FUNNEL.mql_to_close,
payback_months=(8_500 / (ARPU_MONTHLY * GROSS_MARGIN)),
ltv=LTV,
trend="declining",
),
Channel(
name="Outbound SDR",
cac=5_100,
max_mqls_per_month=50,
mql_to_close_rate=FUNNEL.mql_to_close,
payback_months=(5_100 / (ARPU_MONTHLY * GROSS_MARGIN)),
ltv=LTV,
trend="stable",
),
Channel(
name="Events / Field",
cac=9_800,
max_mqls_per_month=25,
mql_to_close_rate=FUNNEL.mql_to_close,
payback_months=(9_800 / (ARPU_MONTHLY * GROSS_MARGIN)),
ltv=LTV,
trend="stable",
),
Channel(
name="Partner / Channel",
cac=3_400,
max_mqls_per_month=30,
mql_to_close_rate=FUNNEL.mql_to_close,
payback_months=(3_400 / (ARPU_MONTHLY * GROSS_MARGIN)),
ltv=LTV,
trend="improving",
),
Channel(
name="Content / Inbound",
cac=2_600,
max_mqls_per_month=45,
mql_to_close_rate=FUNNEL.mql_to_close,
payback_months=(2_600 / (ARPU_MONTHLY * GROSS_MARGIN)),
ltv=LTV,
trend="improving",
),
]
# ---------------------------------------------------------------------------
# Core calculations
# ---------------------------------------------------------------------------
def customers_needed(target_arr: float, asp: float) -> int:
return math.ceil(target_arr / asp)
def mqls_needed_total(customers: int, mql_to_close: float) -> int:
return math.ceil(customers / mql_to_close)
def ltv_to_cac(ltv: float, cac: float) -> float:
return ltv / cac if cac > 0 else 0.0
def score_channel(ch: Channel) -> float:
"""
Score a channel for budget priority.
Higher = more efficient. Used to rank allocation order.
Factors: LTV:CAC ratio, trend multiplier, capacity.
"""
ratio = ltv_to_cac(ch.ltv, ch.cac)
trend_mult = {"improving": 1.2, "stable": 1.0, "declining": 0.7}.get(ch.trend, 1.0)
return ratio * trend_mult
def allocate_mqls(
channels: List[Channel],
total_mqls_needed: int,
budget_multiplier: float = 1.0,
) -> Tuple[Dict[str, int], Dict[str, float]]:
"""
Allocate MQL targets across channels in priority order (best LTV:CAC first).
budget_multiplier: 0.7 = conservative, 1.0 = moderate, 1.3 = aggressive.
Returns (channel → MQLs, channel → budget).
"""
ranked = sorted(channels, key=score_channel, reverse=True)
remaining = total_mqls_needed
channel_mqls: Dict[str, int] = {}
channel_budget: Dict[str, float] = {}
for ch in ranked:
if remaining <= 0:
channel_mqls[ch.name] = 0
channel_budget[ch.name] = 0.0
continue
# Apply capacity ceiling scaled by multiplier (aggressive = push capacity)
capacity = int(ch.max_mqls_per_month * 12 * budget_multiplier)
allocated = min(remaining, capacity)
channel_mqls[ch.name] = allocated
channel_budget[ch.name] = allocated * ch.cac
remaining -= allocated
return channel_mqls, channel_budget
def build_scenario(
name: str,
channels: List[Channel],
total_mqls: int,
multiplier: float,
notes: List[str],
) -> ScenarioResult:
channel_mqls, channel_budget = allocate_mqls(channels, total_mqls, multiplier)
total_budget = sum(channel_budget.values())
total_mqls_allocated = sum(channel_mqls.values())
projected_customers = math.floor(total_mqls_allocated * FUNNEL.mql_to_close)
projected_arr = projected_customers * ASP_ANNUAL
# Blended CAC = total budget / customers acquired
blended_cac = total_budget / projected_customers if projected_customers > 0 else 0.0
return ScenarioResult(
name=name,
total_budget=total_budget,
channel_budgets=channel_budget,
channel_mqls=channel_mqls,
projected_customers=projected_customers,
projected_arr=projected_arr,
blended_cac=blended_cac,
notes=notes,
)
# ---------------------------------------------------------------------------
# Reporting
# ---------------------------------------------------------------------------
def fmt_currency(n: float) -> str:
if n >= 1_000_000:
return f"${n/1_000_000:.2f}M"
if n >= 1_000:
return f"${n/1_000:.1f}K"
return f"${n:.0f}"
def fmt_ratio(n: float) -> str:
return f"{n:.1f}x"
def print_header(title: str) -> None:
width = 72
print("\n" + "=" * width)
print(f" {title}")
print("=" * width)
def print_channel_table(channels: List[Channel]) -> None:
print_header("Channel Analysis — Current State")
header = f"{'Channel':<25} {'CAC':>8} {'Payback':>9} {'LTV:CAC':>8} {'Cap/mo':>7} {'Trend':>10}"
print(header)
print("-" * 72)
for ch in sorted(channels, key=score_channel, reverse=True):
ratio = ltv_to_cac(ch.ltv, ch.cac)
flag = ""
if ratio < 1:
flag = " ⚠ LOSS"
elif ratio >= 6:
flag = " ★ STRONG"
elif ratio >= 3:
flag = " ✓"
print(
f"{ch.name:<25} {fmt_currency(ch.cac):>8} "
f"{ch.payback_months:>7.1f}mo {fmt_ratio(ratio):>8} "
f"{ch.max_mqls_per_month:>7} {ch.trend:>10}{flag}"
)
def print_funnel_summary(customers: int, mqls: int) -> None:
print_header("Funnel Requirements")
print(f" Target new ARR: {fmt_currency(TARGET_NEW_ARR)}")
print(f" Average selling price: {fmt_currency(ASP_ANNUAL)}")
print(f" New customers needed: {customers}")
print(f" Funnel MQL→Close rate: {FUNNEL.mql_to_close:.1%}")
print(f" Total MQLs needed: {mqls}")
print(f"\n Funnel stage rates:")
print(f" MQL → SAL: {FUNNEL.mql_to_sal:.0%}")
print(f" SAL → SQL: {FUNNEL.mql_to_sal * FUNNEL.sal_to_sql:.0%}")
print(f" SQL → Opportunity: {FUNNEL.mql_to_sal * FUNNEL.sal_to_sql * FUNNEL.sql_to_opp:.0%}")
print(f" Opportunity → Close: {FUNNEL.mql_to_close:.0%}")
print(f"\n LTV (estimated): {fmt_currency(LTV)}")
print(f" Monthly churn: {MONTHLY_CHURN:.1%} ({MONTHLY_CHURN*12:.0%} annualized)")
def print_scenario(result: ScenarioResult, channels: List[Channel]) -> None:
print_header(f"Scenario: {result.name}")
print(f" Total marketing budget: {fmt_currency(result.total_budget)}")
print(f" Projected customers: {result.projected_customers}")
print(f" Projected new ARR: {fmt_currency(result.projected_arr)}")
print(f" Blended CAC: {fmt_currency(result.blended_cac)}")
blended_ltv_cac = LTV / result.blended_cac if result.blended_cac > 0 else 0
blended_payback = result.blended_cac / (ARPU_MONTHLY * GROSS_MARGIN)
print(f" Blended LTV:CAC: {fmt_ratio(blended_ltv_cac)}", end="")
if blended_ltv_cac < 1:
print(" ⚠ BELOW BREAK-EVEN")
elif blended_ltv_cac < 3:
print(" △ MARGINAL")
elif blended_ltv_cac >= 3:
print(" ✓ HEALTHY")
else:
print()
print(f" Blended payback: {blended_payback:.1f} months")
if result.notes:
print(f"\n Notes:")
for note in result.notes:
print(f" • {note}")
print(f"\n {'Channel':<25} {'MQLs':>6} {'Budget':>10} {'% of Budget':>12} {'LTV:CAC':>8}")
print(" " + "-" * 65)
for ch in sorted(channels, key=score_channel, reverse=True):
mqls = result.channel_mqls.get(ch.name, 0)
budget = result.channel_budgets.get(ch.name, 0.0)
pct = (budget / result.total_budget * 100) if result.total_budget > 0 else 0
ratio = ltv_to_cac(ch.ltv, ch.cac)
print(
f" {ch.name:<25} {mqls:>6} {fmt_currency(budget):>10} "
f"{pct:>11.1f}% {fmt_ratio(ratio):>8}"
)
def print_scenario_comparison(scenarios: List[ScenarioResult]) -> None:
print_header("Scenario Comparison")
header = f"{'Scenario':<18} {'Budget':>10} {'Customers':>10} {'ARR':>10} {'Blended CAC':>12} {'LTV:CAC':>8} {'Payback':>9}"
print(header)
print("-" * 82)
for s in scenarios:
blended_ltv_cac = LTV / s.blended_cac if s.blended_cac > 0 else 0
blended_payback = s.blended_cac / (ARPU_MONTHLY * GROSS_MARGIN)
print(
f"{s.name:<18} {fmt_currency(s.total_budget):>10} "
f"{s.projected_customers:>10} {fmt_currency(s.projected_arr):>10} "
f"{fmt_currency(s.blended_cac):>12} {fmt_ratio(blended_ltv_cac):>8} "
f"{blended_payback:>7.1f}mo"
)
def print_recommendations(channels: List[Channel]) -> None:
print_header("Channel Recommendations")
scale = [ch for ch in channels if score_channel(ch) >= 1.5 and ch.trend in ("improving", "stable")]
hold = [ch for ch in channels if 0.8 <= score_channel(ch) < 1.5 or (ch.trend == "stable" and ltv_to_cac(ch.ltv, ch.cac) >= 3)]
cut = [ch for ch in channels if ltv_to_cac(ch.ltv, ch.cac) < 2 or ch.trend == "declining"]
# Deduplicate
hold = [ch for ch in hold if ch not in scale]
cut = [ch for ch in cut if ch not in scale and ch not in hold]
if scale:
print(" SCALE (strong LTV:CAC, improving or stable trend):")
for ch in scale:
print(f" + {ch.name} [LTV:CAC {fmt_ratio(ltv_to_cac(ch.ltv, ch.cac))}, payback {ch.payback_months:.0f}mo]")
if hold:
print(" HOLD (monitor — adequate but not outstanding):")
for ch in hold:
print(f" = {ch.name} [LTV:CAC {fmt_ratio(ltv_to_cac(ch.ltv, ch.cac))}, trend: {ch.trend}]")
if cut:
print(" CUT or REDUCE (poor LTV:CAC or declining):")
for ch in cut:
print(f" - {ch.name} [LTV:CAC {fmt_ratio(ltv_to_cac(ch.ltv, ch.cac))}, trend: {ch.trend}]")
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main() -> None:
customers = customers_needed(TARGET_NEW_ARR, ASP_ANNUAL)
total_mqls = mqls_needed_total(customers, FUNNEL.mql_to_close)
print_channel_table(CHANNELS)
print_funnel_summary(customers, total_mqls)
scenarios = [
build_scenario(
name="Conservative",
channels=CHANNELS,
total_mqls=total_mqls,
multiplier=0.7,
notes=[
"Prioritizes lowest CAC channels only.",
"May not reach MQL target — expect ~70% of goal.",
"Best for capital-constrained orgs or short runway.",
],
),
build_scenario(
name="Moderate",
channels=CHANNELS,
total_mqls=total_mqls,
multiplier=1.0,
notes=[
"Balanced allocation — efficiency-first but full MQL target.",
"Recommended baseline. Revisit Q2 based on actuals.",
],
),
build_scenario(
name="Aggressive",
channels=CHANNELS,
total_mqls=total_mqls,
multiplier=1.4,
notes=[
"Pushes all channels toward capacity ceiling.",
"Higher spend on lower-efficiency channels to hit volume.",
"Requires > 18-month runway to justify payback period.",
],
),
]
for scenario in scenarios:
print_scenario(scenario, CHANNELS)
print_scenario_comparison(scenarios)
print_recommendations(CHANNELS)
print("\n" + "=" * 72)
print(" Key questions before finalizing budget:")
print(" 1. What is the payback period the CFO/board will accept?")
print(" 2. Is CAC for declining-trend channels actually recoverable?")
print(" 3. Does the moderate scenario require sales headcount increase?")
print(" 4. Which channels have capacity to absorb 20% more spend?")
print("=" * 72 + "\n")
if __name__ == "__main__":
main()
