
Sales Operations
- 268 installs
- 451 repo stars
- Updated July 21, 2026
- borghei/claude-skills
Support sales operations: pipeline hygiene, forecasting, quote-to-cash workflows, CRM hygiene, and revenue reporting for growing commercial teams.
About
sales-operations helps Claude run commercial back-office during growth: clean CRM pipelines, standardize quotes and approvals, produce forecast-ready reports, and tighten handoffs between marketing, sales, and finance for scalable revenue operations.
- Pipeline and forecast hygiene
- Quote-to-cash workflow support
- CRM data quality routines
- Revenue and funnel reporting
- Sales process standardization
Sales Operations by the numbers
- 268 all-time installs (skills.sh)
- Ranked #255 of 853 Sales & Marketing skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/borghei/claude-skills --skill sales-operationsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 268 |
|---|---|
| repo stars | ★ 451 |
| Last updated | July 21, 2026 |
| Repository | borghei/claude-skills ↗ |
What it does
Support sales operations: pipeline hygiene, forecasting, quote-to-cash workflows, CRM hygiene, and revenue reporting for growing commercial teams.
Files
Sales Operations
The agent operates as an expert sales operations professional, delivering revenue infrastructure through analytics, territory design, quota modeling, compensation architecture, and process optimization.
Workflow
1. Assess current state -- Audit CRM data quality, pipeline coverage, and rep performance baselines. Validate that required fields are populated and stage dates are current. 2. Analyze pipeline health -- Calculate coverage ratios, stage conversion rates, velocity metrics, and deal aging. Flag bottlenecks where conversion drops below historical norms. 3. Design or refine territories -- Balance territories by opportunity potential, workload, and geographic/industry alignment. Score accounts to inform assignment. 4. Model quotas -- Run top-down (revenue target / capacity) and bottom-up (account potential analysis) models. Reconcile and risk-adjust. 5. Architect compensation -- Structure OTE splits, commission tiers, accelerators, and SPIFs aligned to company stage and selling motion. 6. Build forecast -- Categorize deals by confidence tier, apply probability weights, and surface the gap-to-quota with required win rates. 7. Validate and iterate -- Cross-check outputs against historical actuals. Confirm territory balance, quota fairness, and forecast accuracy before publishing.
Sales Metrics Framework
Activity Metrics:
| Metric | Formula | Target |
|---|---|---|
| Calls/Day | Total calls / Days | 50+ |
| Meetings/Week | Total meetings / Weeks | 15+ |
| Proposals/Month | Total proposals / Months | 8+ |
Pipeline Metrics:
| Metric | Formula | Target |
|---|---|---|
| Pipeline Coverage | Pipeline / Quota | 3x+ |
| Pipeline Velocity | Won Deals / Avg Cycle Time | -- |
| Stage Conversion | Stage N+1 / Stage N | Varies |
Outcome Metrics:
| Metric | Formula | Target |
|---|---|---|
| Win Rate | Won / (Won + Lost) | 25%+ |
| Average Deal Size | Revenue / Deals | Context-dependent |
| Sales Cycle | Avg days to close | <60 |
| Quota Attainment | Actual / Quota | 100%+ |
Account Scoring
def score_account(account):
"""Score accounts for territory assignment and prioritization."""
score = 0
# Company size (0-30 points)
if account['employees'] > 5000:
score += 30
elif account['employees'] > 1000:
score += 20
elif account['employees'] > 200:
score += 10
# Industry fit (0-25 points)
if account['industry'] in ['Technology', 'Finance']:
score += 25
elif account['industry'] in ['Healthcare', 'Manufacturing']:
score += 15
# Engagement (0-25 points)
if account['website_visits'] > 10:
score += 15
if account['content_downloads'] > 0:
score += 10
# Intent signals (0-20 points)
if account['intent_score'] > 80:
score += 20
elif account['intent_score'] > 50:
score += 10
return score # Max 100; 70+ = Tier 1, 40-69 = Tier 2, <40 = Tier 3Territory Design
The agent balances territories across three dimensions:
- Balance -- Similar opportunity potential, comparable workload, fair distribution across reps.
- Coverage -- Geographic proximity, industry alignment, existing account relationships.
- Growth -- Room for expansion, career progression paths, untapped market potential.
Example: Territory Allocation Table
| Territory | Rep | Accounts | ARR Potential | Quota | Coverage |
|---|---|---|---|---|---|
| West Enterprise | Rep A | 45 | $3.0M | $2.7M | 111% |
| East Mid-Market | Rep B | 62 | $2.8M | $2.4M | 117% |
| Central (Ramping) | Rep C | 38 | $2.5M | $1.2M | 208% |
Quota Setting
Top-Down Model
Company Revenue Target: $50M
Growth Rate: 30%
Team Capacity: 20 reps
Average Quota: $2.5M
Adjustments: +/-20% based on territory potentialBottom-Up Model
Account Potential Analysis:
Existing accounts: $30M
Pipeline value: $15M
New logo potential: $10M
Total: $55M
Risk adjustment: -10%
Final: $49.5MThe agent reconciles both models and flags divergence exceeding 10%.
Compensation Architecture
TOTAL ON-TARGET EARNINGS (OTE)
Base Salary: 50-60%
Variable: 40-50%
Commission: 80% of variable
New Business: 60%
Expansion: 40%
Bonus: 20% of variable
Quarterly accelerators
SPIFs
COMMISSION RATE TIERS
0-50% quota: 0.5x rate
50-100% quota: 1.0x rate
100-150% quota: 1.5x rate
150%+ quota: 2.0x rateForecasting
Forecast Categories
| Category | Definition | Weighting |
|---|---|---|
| Closed | Signed contract | 100% |
| Commit | Verbal commit, high confidence | 90% |
| Best Case | Strong opportunity, likely to close | 50% |
| Pipeline | Active opportunity | 20% |
| Upside | Early stage | 5% |
Example: Weighted Forecast Output
Q4 Forecast - Week 8
Quota: $10M
Category Deals Amount Weighted
Closed 12 $2.4M $2.4M
Commit 8 $1.8M $1.6M
Best Case 15 $3.2M $1.6M
Pipeline 22 $4.5M $0.9M
Forecast (Closed + Commit): $4.0M
Upside (with Best Case): $5.6M
Gap to Quota: $6.0M
Required Win Rate on Pipeline: 35%CRM Data Quality Checklist
The agent validates these fields during every pipeline review:
- [ ] Required fields populated on all open opportunities
- [ ] Stage dates updated within the last 7 days
- [ ] Close dates set to realistic future dates (no past-due)
- [ ] Deal amounts reflect current pricing discussions
- [ ] Contact roles assigned with at least one economic buyer
- [ ] Next steps documented with specific actions and dates
Process Optimization
Sales Process Audit Framework
STAGE ANALYSIS
Average time in stage -> identify stalls
Conversion rate per stage -> find drop-off points
Drop-off reasons -> categorize and address
ACTIVITY ANALYSIS
Activities per stage -> benchmark against top performers
Activity-to-outcome ratio -> measure efficiency
Time allocation -> optimize selling vs. admin time
TOOL UTILIZATION
CRM adoption rate -> target 95%+ daily login
Feature usage -> identify underused capabilities
Data quality score -> track completeness over time
Automation opportunities -> reduce manual entryScripts
# Pipeline analyzer
python scripts/pipeline_analyzer.py --data opportunities.csv
# Territory optimizer
python scripts/territory_optimizer.py --accounts accounts.csv --reps 10
# Quota calculator
python scripts/quota_calculator.py --target 50000000 --reps team.csv
# Forecast reporter
python scripts/forecast_report.py --quarter Q4 --output report.htmlTroubleshooting
| Problem | Root Cause | Resolution |
|---|---|---|
| Forecast accuracy below 70% | Inconsistent stage definitions; reps over-committing; lack of weighted methodology | Enforce strict stage entry/exit criteria. Apply probability weights by category (Commit 90%, Best Case 50%, Pipeline 20%). Review commit deals individually in weekly forecast calls. Compare rolling 4-quarter actuals to calibrate weights. |
| Territory imbalance causing rep attrition | Uneven account distribution; potential-to-quota mismatch exceeding 20% | Re-score accounts quarterly using the scoring model. Target less than 15% variance in potential-to-quota ratio across territories. Review territory balance monthly in high-growth periods. |
| CRM data quality below 80% completeness | Insufficient enforcement; no automated validation; rep adoption gaps | Implement required field validation at stage transitions. Run weekly data quality reports. Tie CRM hygiene to variable compensation (5-10% of bonus). Target 95%+ daily login rate. |
| Quota attainment below 60% team-wide | Quotas set too aggressively; insufficient pipeline; ramp time underestimated | Reconcile top-down and bottom-up models. Flag divergence exceeding 10%. Risk-adjust for ramp (ramping reps at 50-75% quota). Ensure 3-4x pipeline coverage at quarter start. |
| Comp plan driving wrong behaviors | Misaligned incentives; rewarding volume over quality; no accelerators | Audit comp plans against strategic objectives. Ensure accelerators kick in at 100% attainment. Weight new business vs. expansion per GTM strategy. Add SPIFs for strategic priorities. |
| Pipeline coverage drops mid-quarter | Insufficient lead flow; deals pushed or lost faster than replaced | Alert AEs when individual coverage drops below 2.5x. Coordinate with Marketing on lead generation campaigns. Implement minimum weekly prospecting activity requirements. |
| Stage conversion rates declining | Process bottleneck; missing enablement; competitive pressure | Identify the specific stage with the highest drop-off. Compare top performer conversion rates to team average. Deploy targeted training on the bottleneck stage. Review competitive win/loss data for that stage. |
Success Criteria
| Metric | Target | Measurement Method |
|---|---|---|
| Forecast accuracy | Within 10% of actual quarterly | Abs(Weighted Forecast - Actual) / Actual |
| Pipeline coverage ratio | 3-4x quota at quarter start | Total pipeline value / Team quota |
| CRM data completeness | 95%+ required fields populated | Weekly automated data quality audit |
| Territory balance | Less than 15% variance in potential-to-quota | Standard deviation of potential-to-quota ratio across territories |
| Quota attainment distribution | 60%+ of reps at or above quota | Reps at 100%+ / Total ramped reps |
| Stage conversion rates | Improving or stable QoQ | Stage N+1 entries / Stage N entries per period |
| Sales cycle length | Trending downward or stable | Average days from opportunity creation to close |
| Ramp time to productivity | Under 6 months for new hires | Months until new rep reaches 75% of quota run rate |
| Process adoption | 90%+ compliance with defined process | Audit score from monthly process compliance review |
Scope & Limitations
In Scope:
- CRM administration, data quality management, and process enforcement
- Pipeline analytics: coverage ratios, stage conversion, velocity metrics, deal aging
- Territory design, account scoring, and balanced assignment optimization
- Quota modeling: top-down, bottom-up, and reconciliation approaches
- Compensation architecture: OTE splits, commission tiers, accelerators, SPIFs
- Forecast methodology: weighted pipeline, category-based, rolling forecasts
- Sales process audit: stage analysis, activity benchmarking, tool utilization
- Reporting infrastructure and dashboard design
Out of Scope:
- Individual deal strategy, qualification, and closing (see account-executive)
- Technical demos, RFP responses, and POC management (see sales-engineer)
- Post-sale customer management and retention (see customer-success-manager)
- Enterprise solution architecture and integration design (see solutions-architect)
- Marketing attribution modeling and campaign ROI (see marketing/campaign-analytics)
- Financial modeling beyond sales compensation (see finance)
Limitations:
- Territory optimization uses heuristic scoring, not mathematical optimization solvers; results are directional, not globally optimal
- Quota models require accurate historical data; garbage in, garbage out
- Forecast accuracy benchmarks assume consistent CRM hygiene; accuracy degrades with poor data quality
- Scripts process CSV/JSON exports only; no direct CRM API connectivity
- Compensation modeling does not account for tax implications or local labor law constraints
Integration Points
| Integration | Direction | Purpose | Handoff Artifact |
|---|---|---|---|
| Account Executive | Ops -> AE | Territory assignments, quota targets, pipeline reports, forecast templates | Territory map, quota letter, pipeline dashboard, forecast submission form |
| Sales Engineer | Ops -> SE | Activity tracking, demo conversion metrics, technical win/loss data | SE activity reports, technical evaluation pipeline |
| Customer Success Manager | Ops -> CSM | Renewal pipeline tracking, expansion revenue attribution, churn reporting | Renewal forecast rollup, NRR reports, churn analysis |
| Marketing | Bidirectional | Lead attribution, MQL-to-SQL conversion, campaign ROI, pipeline sourcing | Attribution reports, lead routing rules, campaign pipeline reports |
| Finance | Ops -> Finance | Revenue forecasting, commission calculations, quota-to-capacity planning | Forecast submissions, commission statements, headcount models |
| Revenue Operations | Bidirectional | Cross-functional GTM metrics, funnel analytics, ARR reporting | Unified revenue dashboard, GTM efficiency metrics |
| HR | Ops -> HR | Headcount planning, ramp modeling, performance data for reviews | Ramp timelines, quota attainment reports, territory capacity models |
Workflow Handoff Protocol: 1. Sales Ops publishes territory assignments and quota letters at least 2 weeks before quarter start 2. Sales Ops delivers weekly pipeline report to sales leadership every Monday by 10 AM 3. Sales Ops collects forecast submissions from AEs every Friday and publishes rolled-up forecast by Monday 4. Sales Ops runs monthly territory health review and flags imbalances exceeding 15% variance
Reference Materials
references/analytics.md-- Sales analytics guidereferences/territory.md-- Territory planningreferences/compensation.md-- Comp design principlesreferences/forecasting.md-- Forecasting methodology
#!/usr/bin/env python3
"""Analyze sales forecast accuracy across quarters and categories.
Compares forecasted values against actual outcomes to measure accuracy,
identify bias patterns, and recommend calibration adjustments.
Usage:
python forecast_analyzer.py --data forecast_history.csv
python forecast_analyzer.py --data forecast.json --json
python forecast_analyzer.py --data forecast.csv --quarters 4
"""
import argparse
import csv
import json
import math
import os
import sys
from collections import defaultdict
from datetime import datetime
FORECAST_CATEGORIES = ["closed", "commit", "best_case", "pipeline", "upside"]
CATEGORY_WEIGHTS = {
"closed": 1.00,
"commit": 0.90,
"best_case": 0.50,
"pipeline": 0.20,
"upside": 0.05,
}
ACCURACY_THRESHOLDS = {
(0, 5): ("Excellent", "Forecast is highly reliable. Maintain current methodology."),
(5, 10): ("Good", "Within acceptable range. Minor calibration may improve accuracy."),
(10, 20): ("Fair", "Noticeable deviation. Review stage definitions and commit criteria."),
(20, 40): ("Poor", "Significant accuracy issues. Overhaul forecast methodology."),
(40, 200): ("Unreliable", "Forecast not useful for planning. Fundamental process change needed."),
}
def load_data(filepath):
"""Load forecast data from CSV or JSON file."""
ext = os.path.splitext(filepath)[1].lower()
if ext == ".json":
with open(filepath, "r") as f:
data = json.load(f)
return data if isinstance(data, list) else [data]
elif ext == ".csv":
with open(filepath, "r") as f:
return list(csv.DictReader(f))
else:
print(f"Error: Unsupported file format '{ext}'. Use .csv or .json.", file=sys.stderr)
sys.exit(1)
def safe_float(value, default=0.0):
"""Parse float safely."""
try:
return float(str(value).replace("$", "").replace(",", "").replace("%", "").strip())
except (ValueError, TypeError):
return default
def analyze_forecast(records, num_quarters=None):
"""Analyze forecast accuracy from historical data."""
quarterly_data = defaultdict(lambda: {
"forecast": 0, "actual": 0, "quota": 0,
"categories": defaultdict(lambda: {"forecast": 0, "actual": 0}),
"rep_data": defaultdict(lambda: {"forecast": 0, "actual": 0}),
})
for record in records:
quarter = record.get("quarter", record.get("period", "Unknown"))
forecast = safe_float(record.get("forecast", record.get("forecast_amount", 0)))
actual = safe_float(record.get("actual", record.get("actual_amount", 0)))
quota = safe_float(record.get("quota", 0))
category = record.get("category", record.get("forecast_category", "")).lower().strip().replace(" ", "_")
rep = record.get("rep", record.get("rep_name", record.get("owner", "")))
quarterly_data[quarter]["forecast"] += forecast
quarterly_data[quarter]["actual"] += actual
if quota > 0:
quarterly_data[quarter]["quota"] = max(quarterly_data[quarter]["quota"], quota)
if category:
quarterly_data[quarter]["categories"][category]["forecast"] += forecast
quarterly_data[quarter]["categories"][category]["actual"] += actual
if rep:
quarterly_data[quarter]["rep_data"][rep]["forecast"] += forecast
quarterly_data[quarter]["rep_data"][rep]["actual"] += actual
# Limit to recent quarters
quarters = sorted(quarterly_data.keys(), reverse=True)
if num_quarters:
quarters = quarters[:num_quarters]
# Quarterly analysis
quarter_results = []
accuracy_values = []
bias_values = []
for q in quarters:
data = quarterly_data[q]
fc = data["forecast"]
act = data["actual"]
quota = data["quota"]
if act > 0:
accuracy_pct = abs(fc - act) / act * 100
bias_pct = (fc - act) / act * 100 # Positive = over-forecast
elif fc > 0:
accuracy_pct = 100
bias_pct = 100
else:
accuracy_pct = 0
bias_pct = 0
accuracy_values.append(accuracy_pct)
bias_values.append(bias_pct)
attainment = round(act / quota * 100, 1) if quota > 0 else 0
# Category breakdown
cat_results = {}
for cat in FORECAST_CATEGORIES:
cd = data["categories"].get(cat, {"forecast": 0, "actual": 0})
if cd["actual"] > 0:
cat_acc = abs(cd["forecast"] - cd["actual"]) / cd["actual"] * 100
else:
cat_acc = 0 if cd["forecast"] == 0 else 100
cat_results[cat] = {
"forecast": round(cd["forecast"], 2),
"actual": round(cd["actual"], 2),
"accuracy_error": round(cat_acc, 1),
}
# Rep analysis
rep_results = {}
for rep, rd in data["rep_data"].items():
if rd["actual"] > 0:
rep_acc = abs(rd["forecast"] - rd["actual"]) / rd["actual"] * 100
rep_bias = (rd["forecast"] - rd["actual"]) / rd["actual"] * 100
else:
rep_acc = 100 if rd["forecast"] > 0 else 0
rep_bias = 100 if rd["forecast"] > 0 else 0
rep_results[rep] = {
"forecast": round(rd["forecast"], 2),
"actual": round(rd["actual"], 2),
"accuracy_error": round(rep_acc, 1),
"bias": round(rep_bias, 1),
}
quarter_results.append({
"quarter": q,
"forecast": round(fc, 2),
"actual": round(act, 2),
"quota": round(quota, 2),
"accuracy_error_pct": round(accuracy_pct, 1),
"bias_pct": round(bias_pct, 1),
"quota_attainment_pct": attainment,
"category_breakdown": cat_results,
"rep_accuracy": dict(sorted(rep_results.items(), key=lambda x: x[1]["accuracy_error"], reverse=True)),
})
# Overall metrics
avg_accuracy = sum(accuracy_values) / len(accuracy_values) if accuracy_values else 0
avg_bias = sum(bias_values) / len(bias_values) if bias_values else 0
label = "Unknown"
advice = ""
for (lo, hi), (lbl, adv) in ACCURACY_THRESHOLDS.items():
if lo <= avg_accuracy < hi:
label = lbl
advice = adv
break
bias_direction = "over-forecasting" if avg_bias > 0 else "under-forecasting" if avg_bias < 0 else "neutral"
calibration_factor = round(1 - (avg_bias / 100), 3) if avg_bias != 0 else 1.0
return {
"summary": {
"quarters_analyzed": len(quarters),
"avg_accuracy_error_pct": round(avg_accuracy, 1),
"avg_bias_pct": round(avg_bias, 1),
"accuracy_rating": label,
"accuracy_advice": advice,
"bias_direction": bias_direction,
"recommended_calibration_factor": calibration_factor,
"improving": accuracy_values[-1] < accuracy_values[0] if len(accuracy_values) >= 2 else None,
},
"quarterly_results": quarter_results,
}
def format_human(results):
"""Format results for human-readable output."""
lines = []
lines.append("=" * 70)
lines.append("FORECAST ACCURACY ANALYSIS")
lines.append(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M')}")
lines.append("=" * 70)
s = results["summary"]
lines.append(f"\n Quarters Analyzed: {s['quarters_analyzed']}")
lines.append(f" Avg Accuracy Error: {s['avg_accuracy_error_pct']}%")
lines.append(f" Accuracy Rating: {s['accuracy_rating']}")
lines.append(f" Avg Bias: {s['avg_bias_pct']}% ({s['bias_direction']})")
lines.append(f" Calibration Factor: {s['recommended_calibration_factor']}")
lines.append(f" Recommendation: {s['accuracy_advice']}")
if s["improving"] is not None:
trend = "IMPROVING" if s["improving"] else "DECLINING"
lines.append(f" Trend: {trend}")
for qr in results["quarterly_results"]:
lines.append(f"\n{'=' * 50}")
lines.append(f" Quarter: {qr['quarter']}")
lines.append(f" Forecast: ${qr['forecast']:,.2f} | Actual: ${qr['actual']:,.2f}")
lines.append(f" Accuracy Error: {qr['accuracy_error_pct']}% | Bias: {qr['bias_pct']}%")
lines.append(f" Quota: ${qr['quota']:,.2f} | Attainment: {qr['quota_attainment_pct']}%")
if qr["category_breakdown"]:
lines.append(f"\n Category Breakdown:")
lines.append(f" {'Category':<14} {'Forecast':>12} {'Actual':>12} {'Error':>8}")
lines.append(" " + "-" * 48)
for cat, cd in qr["category_breakdown"].items():
if cd["forecast"] > 0 or cd["actual"] > 0:
lines.append(
f" {cat:<14} ${cd['forecast']:>10,.2f} ${cd['actual']:>10,.2f} "
f"{cd['accuracy_error']:>7.1f}%"
)
if qr["rep_accuracy"]:
lines.append(f"\n Rep Accuracy (worst to best):")
lines.append(f" {'Rep':<20} {'Forecast':>12} {'Actual':>12} {'Error':>8} {'Bias':>8}")
lines.append(" " + "-" * 62)
for rep, rd in list(qr["rep_accuracy"].items())[:10]:
bias_flag = " OVR" if rd["bias"] > 20 else " UND" if rd["bias"] < -20 else ""
lines.append(
f" {rep:<20} ${rd['forecast']:>10,.2f} ${rd['actual']:>10,.2f} "
f"{rd['accuracy_error']:>7.1f}% {rd['bias']:>7.1f}%{bias_flag}"
)
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(
description="Analyze sales forecast accuracy and identify improvement areas."
)
parser.add_argument("--data", required=True, help="Path to forecast history CSV or JSON file")
parser.add_argument(
"--quarters", type=int, default=None, help="Number of recent quarters to analyze"
)
parser.add_argument("--json", action="store_true", help="Output results as JSON")
args = parser.parse_args()
if not os.path.exists(args.data):
print(f"Error: File not found: {args.data}", file=sys.stderr)
sys.exit(1)
records = load_data(args.data)
if not records:
print("Error: No forecast records found in input file.", file=sys.stderr)
sys.exit(1)
results = analyze_forecast(records, args.quarters)
if args.json:
print(json.dumps(results, indent=2))
else:
print(format_human(results))
accuracy = results["summary"]["avg_accuracy_error_pct"]
sys.exit(0 if accuracy <= 15 else 1)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Track quota attainment with pacing indicators and gap analysis.
Reads rep performance data and quota assignments to calculate attainment,
pacing against plan, and projected finish with required run rates.
Usage:
python quota_calculator.py --data performance.csv --quarter Q1-2026
python quota_calculator.py --data performance.json --json
python quota_calculator.py --data performance.csv --weeks-elapsed 8 --total-weeks 13
"""
import argparse
import csv
import json
import os
import sys
from datetime import datetime
ATTAINMENT_LABELS = {
(0, 50): ("Far Behind", "Critical intervention needed. Review pipeline and activity."),
(50, 75): ("Behind Plan", "Accelerate pipeline. Focus on commit and best-case deals."),
(75, 90): ("Tracking", "On pace with room to improve. Push best-case deals to commit."),
(90, 100): ("Near Target", "Close to plan. Focus on closing committed deals."),
(100, 150): ("At or Above", "Hitting quota. Pursue accelerators and overachievement."),
(150, 500): ("Crushing It", "Exceptional performance. Maximize accelerator earnings."),
}
COMMISSION_TIERS = [
(0, 50, 0.5),
(50, 100, 1.0),
(100, 150, 1.5),
(150, float("inf"), 2.0),
]
def load_data(filepath):
"""Load performance data from CSV or JSON file."""
ext = os.path.splitext(filepath)[1].lower()
if ext == ".json":
with open(filepath, "r") as f:
data = json.load(f)
return data if isinstance(data, list) else [data]
elif ext == ".csv":
with open(filepath, "r") as f:
return list(csv.DictReader(f))
else:
print(f"Error: Unsupported file format '{ext}'. Use .csv or .json.", file=sys.stderr)
sys.exit(1)
def safe_float(value, default=0.0):
"""Parse float safely."""
try:
return float(str(value).replace("$", "").replace(",", "").replace("%", "").strip())
except (ValueError, TypeError):
return default
def calculate_commission_multiplier(attainment_pct):
"""Calculate blended commission multiplier based on attainment tiers."""
for lo, hi, rate in COMMISSION_TIERS:
if attainment_pct <= hi:
return rate
return COMMISSION_TIERS[-1][2]
def calculate_attainment(rep_data, weeks_elapsed, total_weeks):
"""Calculate quota attainment and projections for a single rep."""
name = rep_data.get("name", rep_data.get("rep_name", rep_data.get("rep", "Unknown")))
quota = safe_float(rep_data.get("quota", 0))
closed = safe_float(rep_data.get("closed", rep_data.get("closed_won", rep_data.get("bookings", 0))))
commit = safe_float(rep_data.get("commit", rep_data.get("commit_pipeline", 0)))
best_case = safe_float(rep_data.get("best_case", rep_data.get("best_case_pipeline", 0)))
pipeline = safe_float(rep_data.get("pipeline", rep_data.get("open_pipeline", 0)))
team = rep_data.get("team", rep_data.get("segment", ""))
ramping = str(rep_data.get("ramping", "false")).lower() in ("true", "1", "yes")
if quota <= 0:
return None
# Core attainment
attainment_pct = round(closed / quota * 100, 1)
# Pacing
elapsed_pct = round(weeks_elapsed / total_weeks * 100, 1) if total_weeks > 0 else 0
expected_at_pace = round(quota * (weeks_elapsed / total_weeks), 2) if total_weeks > 0 else 0
pace_delta = round(closed - expected_at_pace, 2)
pace_status = "ahead" if pace_delta > 0 else "behind" if pace_delta < 0 else "on_pace"
# Projections
weeks_remaining = max(total_weeks - weeks_elapsed, 0)
if weeks_elapsed > 0:
weekly_run_rate = closed / weeks_elapsed
projected_finish = round(closed + (weekly_run_rate * weeks_remaining), 2)
projected_attainment = round(projected_finish / quota * 100, 1)
else:
weekly_run_rate = 0
projected_finish = closed
projected_attainment = attainment_pct
# Gap analysis
gap = max(quota - closed, 0)
required_weekly_rate = round(gap / weeks_remaining, 2) if weeks_remaining > 0 else gap
required_win_amount = gap
# Weighted pipeline check
weighted_pipeline = (commit * 0.90) + (best_case * 0.50) + (pipeline * 0.20)
coverage_of_gap = round(weighted_pipeline / gap, 2) if gap > 0 else float("inf")
# Commission tier
commission_multiplier = calculate_commission_multiplier(attainment_pct)
# Label
label = "Unknown"
advice = ""
for (lo, hi), (lbl, adv) in ATTAINMENT_LABELS.items():
if lo <= attainment_pct < hi:
label = lbl
advice = adv
break
return {
"rep_name": name,
"team": team,
"ramping": ramping,
"quota": round(quota, 2),
"closed": round(closed, 2),
"attainment_pct": attainment_pct,
"attainment_label": label,
"attainment_advice": advice,
"pacing": {
"weeks_elapsed": weeks_elapsed,
"total_weeks": total_weeks,
"elapsed_pct": elapsed_pct,
"expected_at_pace": expected_at_pace,
"pace_delta": pace_delta,
"pace_status": pace_status,
},
"projection": {
"weekly_run_rate": round(weekly_run_rate, 2),
"projected_finish": projected_finish,
"projected_attainment_pct": projected_attainment,
},
"gap_analysis": {
"gap_to_quota": round(gap, 2),
"required_weekly_rate": required_weekly_rate,
"weeks_remaining": weeks_remaining,
"commit_pipeline": round(commit, 2),
"best_case_pipeline": round(best_case, 2),
"open_pipeline": round(pipeline, 2),
"weighted_pipeline": round(weighted_pipeline, 2),
"coverage_of_gap": coverage_of_gap if coverage_of_gap != float("inf") else "N/A (no gap)",
},
"commission_multiplier": commission_multiplier,
}
def format_human(results, team_summary):
"""Format results for human-readable output."""
lines = []
lines.append("=" * 70)
lines.append("QUOTA ATTAINMENT REPORT")
lines.append(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M')}")
lines.append("=" * 70)
ts = team_summary
lines.append(f"\n Team Summary")
lines.append(f" Total Reps: {ts['total_reps']}")
lines.append(f" Team Quota: ${ts['team_quota']:,.2f}")
lines.append(f" Team Closed: ${ts['team_closed']:,.2f}")
lines.append(f" Team Attainment: {ts['team_attainment_pct']}%")
lines.append(f" Reps at 100%+: {ts['reps_at_quota']} ({ts['pct_at_quota']}%)")
lines.append(f" Reps Below 50%: {ts['reps_below_50']}")
lines.append(f" Team Gap: ${ts['team_gap']:,.2f}")
lines.append(f"\n{'REP ATTAINMENT':^70}")
lines.append("-" * 70)
lines.append(
f" {'Rep':<18} {'Quota':>10} {'Closed':>10} {'Attn%':>7} {'Pace':>8} "
f"{'Proj%':>7} {'Gap':>10}"
)
lines.append(" " + "-" * 62)
for r in sorted(results, key=lambda x: x["attainment_pct"], reverse=True):
pace = r["pacing"]["pace_status"]
pace_flag = "+" if pace == "ahead" else "-" if pace == "behind" else "="
ramp = " (R)" if r["ramping"] else ""
lines.append(
f" {r['rep_name']:<18} ${r['quota']:>8,.0f} ${r['closed']:>8,.0f} "
f"{r['attainment_pct']:>6.1f}% {pace_flag:>7} "
f"{r['projection']['projected_attainment_pct']:>6.1f}% "
f"${r['gap_analysis']['gap_to_quota']:>8,.0f}{ramp}"
)
lines.append(f"\n{'DETAILED ANALYSIS':^70}")
lines.append("-" * 70)
for r in sorted(results, key=lambda x: x["attainment_pct"]):
lines.append(f"\n {r['rep_name']} [{r['attainment_label']}]")
lines.append(f" {r['attainment_advice']}")
lines.append(f" Closed: ${r['closed']:,.2f} / ${r['quota']:,.2f} ({r['attainment_pct']}%)")
lines.append(
f" Pacing: ${r['pacing']['pace_delta']:+,.2f} vs plan "
f"(expected ${r['pacing']['expected_at_pace']:,.2f} at week {r['pacing']['weeks_elapsed']})"
)
lines.append(
f" Projection: ${r['projection']['projected_finish']:,.2f} "
f"({r['projection']['projected_attainment_pct']}%) at current rate"
)
ga = r["gap_analysis"]
lines.append(
f" Gap: ${ga['gap_to_quota']:,.2f} | "
f"Need ${ga['required_weekly_rate']:,.2f}/wk over {ga['weeks_remaining']} weeks"
)
lines.append(
f" Pipeline: Commit ${ga['commit_pipeline']:,.2f} | "
f"Best Case ${ga['best_case_pipeline']:,.2f} | "
f"Open ${ga['open_pipeline']:,.2f}"
)
cov = ga["coverage_of_gap"]
cov_str = f"{cov}x" if isinstance(cov, (int, float)) else cov
lines.append(f" Weighted Pipeline vs Gap: {cov_str}")
lines.append(f" Commission Tier: {r['commission_multiplier']}x")
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(
description="Track quota attainment with pacing and gap analysis."
)
parser.add_argument("--data", required=True, help="Path to rep performance CSV or JSON file")
parser.add_argument("--quarter", default=None, help="Quarter label (e.g., Q1-2026)")
parser.add_argument(
"--weeks-elapsed", type=int, default=6, help="Weeks elapsed in quarter (default: 6)"
)
parser.add_argument(
"--total-weeks", type=int, default=13, help="Total weeks in quarter (default: 13)"
)
parser.add_argument("--json", action="store_true", help="Output results as JSON")
args = parser.parse_args()
if not os.path.exists(args.data):
print(f"Error: File not found: {args.data}", file=sys.stderr)
sys.exit(1)
reps = load_data(args.data)
if not reps:
print("Error: No rep data found in input file.", file=sys.stderr)
sys.exit(1)
results = []
for rep in reps:
result = calculate_attainment(rep, args.weeks_elapsed, args.total_weeks)
if result:
results.append(result)
if not results:
print("Error: No valid quota data found.", file=sys.stderr)
sys.exit(1)
# Team summary
team_quota = sum(r["quota"] for r in results)
team_closed = sum(r["closed"] for r in results)
reps_at_quota = sum(1 for r in results if r["attainment_pct"] >= 100)
reps_below_50 = sum(1 for r in results if r["attainment_pct"] < 50)
team_summary = {
"quarter": args.quarter or "Current",
"total_reps": len(results),
"team_quota": round(team_quota, 2),
"team_closed": round(team_closed, 2),
"team_attainment_pct": round(team_closed / team_quota * 100, 1) if team_quota > 0 else 0,
"team_gap": round(max(team_quota - team_closed, 0), 2),
"reps_at_quota": reps_at_quota,
"pct_at_quota": round(reps_at_quota / len(results) * 100, 1),
"reps_below_50": reps_below_50,
}
if args.json:
output = {"team_summary": team_summary, "rep_details": results}
print(json.dumps(output, indent=2))
else:
print(format_human(results, team_summary))
sys.exit(0 if team_summary["team_attainment_pct"] >= 80 else 1)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Optimize territory assignment balancing potential, workload, and coverage.
Reads account data and rep roster to produce balanced territory assignments
with variance analysis, coverage metrics, and fairness scoring.
Usage:
python territory_planner.py --accounts accounts.csv --reps 8
python territory_planner.py --accounts accounts.json --reps reps.csv --json
python territory_planner.py --accounts accounts.csv --reps 10 --strategy balanced
"""
import argparse
import csv
import json
import math
import os
import sys
from collections import defaultdict
from datetime import datetime
TIER_THRESHOLDS = {
"tier_1": 70,
"tier_2": 40,
"tier_3": 0,
}
INDUSTRY_SCORES = {
"technology": 25,
"finance": 25,
"financial_services": 25,
"healthcare": 20,
"manufacturing": 15,
"retail": 15,
"education": 10,
"government": 10,
}
def load_data(filepath):
"""Load data from CSV or JSON file."""
ext = os.path.splitext(filepath)[1].lower()
if ext == ".json":
with open(filepath, "r") as f:
data = json.load(f)
return data if isinstance(data, list) else [data]
elif ext == ".csv":
with open(filepath, "r") as f:
return list(csv.DictReader(f))
else:
print(f"Error: Unsupported file format '{ext}'. Use .csv or .json.", file=sys.stderr)
sys.exit(1)
def safe_float(value, default=0.0):
"""Parse float safely."""
try:
return float(str(value).replace("$", "").replace(",", "").strip())
except (ValueError, TypeError):
return default
def safe_int(value, default=0):
"""Parse int safely."""
try:
return int(float(str(value).strip()))
except (ValueError, TypeError):
return default
def score_account(account):
"""Score an account for territory assignment (0-100)."""
score = 0
# Company size (0-30 points)
employees = safe_int(account.get("employees", account.get("employee_count", 0)))
if employees > 5000:
score += 30
elif employees > 1000:
score += 20
elif employees > 200:
score += 10
elif employees > 50:
score += 5
# Revenue potential (0-25 points)
revenue = safe_float(account.get("potential", account.get("arr_potential", account.get("revenue", 0))))
if revenue > 500000:
score += 25
elif revenue > 200000:
score += 20
elif revenue > 100000:
score += 15
elif revenue > 50000:
score += 10
elif revenue > 0:
score += 5
# Industry fit (0-25 points)
industry = str(account.get("industry", "")).lower().strip().replace(" ", "_")
score += INDUSTRY_SCORES.get(industry, 8)
# Engagement signals (0-20 points)
engagement = safe_float(account.get("engagement_score", account.get("intent_score", 0)))
if engagement > 80:
score += 20
elif engagement > 50:
score += 12
elif engagement > 20:
score += 5
return min(score, 100)
def assign_tier(score):
"""Assign account tier based on score."""
if score >= TIER_THRESHOLDS["tier_1"]:
return "Tier 1"
elif score >= TIER_THRESHOLDS["tier_2"]:
return "Tier 2"
else:
return "Tier 3"
def plan_territories(accounts, num_reps, rep_data=None, strategy="balanced"):
"""Create balanced territory assignments."""
# Score all accounts
scored_accounts = []
for acct in accounts:
account_score = score_account(acct)
name = acct.get("name", acct.get("account_name", acct.get("company", "Unknown")))
potential = safe_float(acct.get("potential", acct.get("arr_potential", acct.get("revenue", 0))))
region = acct.get("region", acct.get("territory", acct.get("geo", "Unassigned")))
industry = acct.get("industry", "Unknown")
current_rep = acct.get("rep", acct.get("owner", acct.get("assigned_to", "")))
scored_accounts.append({
"name": name,
"score": account_score,
"tier": assign_tier(account_score),
"potential": potential,
"region": region,
"industry": industry,
"current_rep": current_rep,
"employees": safe_int(acct.get("employees", 0)),
})
# Sort by score descending for assignment
scored_accounts.sort(key=lambda x: x["score"], reverse=True)
# Build rep list
reps = []
if rep_data:
for r in rep_data:
reps.append({
"name": r.get("name", r.get("rep_name", f"Rep {len(reps)+1}")),
"region": r.get("region", r.get("territory", "")),
"ramping": str(r.get("ramping", r.get("is_ramping", "false"))).lower() in ("true", "1", "yes"),
"capacity": safe_float(r.get("capacity", 1.0)) if safe_float(r.get("capacity", 1.0)) > 0 else 1.0,
})
else:
for i in range(num_reps):
reps.append({
"name": f"Rep {i+1}",
"region": "",
"ramping": False,
"capacity": 1.0,
})
# Initialize territory buckets
territories = {rep["name"]: {
"rep": rep["name"],
"accounts": [],
"total_potential": 0,
"account_count": 0,
"avg_score": 0,
"tier_distribution": {"Tier 1": 0, "Tier 2": 0, "Tier 3": 0},
"ramping": rep["ramping"],
"capacity": rep["capacity"],
} for rep in reps}
# Assignment strategy: round-robin by score tiers for balance
if strategy == "balanced":
# Distribute accounts trying to balance potential
for acct in scored_accounts:
# Find rep with lowest weighted potential
best_rep = None
best_weighted = float("inf")
for rep_name, terr in territories.items():
capacity = terr["capacity"]
weighted_potential = terr["total_potential"] / capacity if capacity > 0 else float("inf")
# Prefer region match
rep_obj = next((r for r in reps if r["name"] == rep_name), None)
if rep_obj and rep_obj["region"] and acct["region"]:
if rep_obj["region"].lower() == acct["region"].lower():
weighted_potential *= 0.8 # Prefer region match
if weighted_potential < best_weighted:
best_weighted = weighted_potential
best_rep = rep_name
if best_rep:
territories[best_rep]["accounts"].append(acct)
territories[best_rep]["total_potential"] += acct["potential"]
territories[best_rep]["account_count"] += 1
territories[best_rep]["tier_distribution"][acct["tier"]] += 1
elif strategy == "geographic":
# Group by region first, then balance within regions
region_accounts = defaultdict(list)
for acct in scored_accounts:
region_accounts[acct["region"]].append(acct)
rep_index = 0
rep_names = list(territories.keys())
for region, accts in region_accounts.items():
for acct in accts:
rep_name = rep_names[rep_index % len(rep_names)]
territories[rep_name]["accounts"].append(acct)
territories[rep_name]["total_potential"] += acct["potential"]
territories[rep_name]["account_count"] += 1
territories[rep_name]["tier_distribution"][acct["tier"]] += 1
rep_index += 1
# Calculate averages and metrics
potentials = []
for terr in territories.values():
if terr["accounts"]:
terr["avg_score"] = round(
sum(a["score"] for a in terr["accounts"]) / len(terr["accounts"]), 1
)
terr["total_potential"] = round(terr["total_potential"], 2)
potentials.append(terr["total_potential"])
# Balance metrics
avg_potential = sum(potentials) / len(potentials) if potentials else 0
if avg_potential > 0 and len(potentials) > 1:
variance = sum((p - avg_potential) ** 2 for p in potentials) / len(potentials)
std_dev = math.sqrt(variance)
cv = round(std_dev / avg_potential * 100, 1) # Coefficient of variation
else:
cv = 0
max_potential = max(potentials) if potentials else 0
min_potential = min(potentials) if potentials else 0
spread = round((max_potential - min_potential) / avg_potential * 100, 1) if avg_potential > 0 else 0
if cv < 10:
balance_rating = "Excellent"
balance_advice = "Territories are well-balanced. Minor adjustments only if needed."
elif cv < 20:
balance_rating = "Good"
balance_advice = "Acceptable balance. Review outlier territories for adjustment."
elif cv < 30:
balance_rating = "Fair"
balance_advice = "Notable imbalance. Redistribute accounts from heaviest to lightest territories."
else:
balance_rating = "Poor"
balance_advice = "Significant imbalance. Re-run with adjusted strategy or manual overrides."
return {
"summary": {
"total_accounts": len(scored_accounts),
"total_reps": len(reps),
"total_potential": round(sum(potentials), 2),
"avg_potential_per_rep": round(avg_potential, 2),
"balance_cv_pct": cv,
"balance_rating": balance_rating,
"balance_advice": balance_advice,
"potential_spread_pct": spread,
"strategy_used": strategy,
},
"account_tier_distribution": {
"Tier 1": sum(1 for a in scored_accounts if a["tier"] == "Tier 1"),
"Tier 2": sum(1 for a in scored_accounts if a["tier"] == "Tier 2"),
"Tier 3": sum(1 for a in scored_accounts if a["tier"] == "Tier 3"),
},
"territories": {
name: {
"rep": terr["rep"],
"account_count": terr["account_count"],
"total_potential": terr["total_potential"],
"avg_account_score": terr["avg_score"],
"tier_distribution": terr["tier_distribution"],
"ramping": terr["ramping"],
"top_accounts": [
{"name": a["name"], "potential": a["potential"], "tier": a["tier"]}
for a in sorted(terr["accounts"], key=lambda x: x["potential"], reverse=True)[:5]
],
}
for name, terr in territories.items()
},
}
def format_human(results):
"""Format results for human-readable output."""
lines = []
lines.append("=" * 70)
lines.append("TERRITORY PLANNING REPORT")
lines.append(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M')}")
lines.append("=" * 70)
s = results["summary"]
lines.append(f"\n Total Accounts: {s['total_accounts']}")
lines.append(f" Total Reps: {s['total_reps']}")
lines.append(f" Total Potential: ${s['total_potential']:,.2f}")
lines.append(f" Avg Potential / Rep: ${s['avg_potential_per_rep']:,.2f}")
lines.append(f" Strategy: {s['strategy_used']}")
lines.append(f" Balance Rating: {s['balance_rating']} (CV: {s['balance_cv_pct']}%)")
lines.append(f" Balance Advice: {s['balance_advice']}")
td = results["account_tier_distribution"]
lines.append(f"\n Account Tiers: Tier 1: {td['Tier 1']} | Tier 2: {td['Tier 2']} | Tier 3: {td['Tier 3']}")
lines.append(f"\n{'TERRITORY ASSIGNMENTS':^70}")
lines.append("-" * 70)
lines.append(f" {'Rep':<20} {'Accts':>6} {'Potential':>14} {'Avg Score':>10} {'T1':>4} {'T2':>4} {'T3':>4}")
lines.append(" " + "-" * 64)
for name, terr in sorted(
results["territories"].items(),
key=lambda x: x[1]["total_potential"],
reverse=True,
):
ramp = " (R)" if terr["ramping"] else ""
td = terr["tier_distribution"]
lines.append(
f" {name:<20} {terr['account_count']:>6} "
f"${terr['total_potential']:>12,.2f} "
f"{terr['avg_account_score']:>9.1f} "
f"{td.get('Tier 1', 0):>4} {td.get('Tier 2', 0):>4} {td.get('Tier 3', 0):>4}{ramp}"
)
# Top accounts per territory
for name, terr in results["territories"].items():
if terr["top_accounts"]:
lines.append(f"\n {name} - Top Accounts:")
for acct in terr["top_accounts"]:
lines.append(f" {acct['name']:<35} ${acct['potential']:>10,.2f} [{acct['tier']}]")
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(
description="Optimize territory assignment balancing potential and workload."
)
parser.add_argument("--accounts", required=True, help="Path to accounts CSV or JSON file")
parser.add_argument(
"--reps",
required=True,
help="Number of reps (integer) or path to reps CSV/JSON file",
)
parser.add_argument(
"--strategy",
choices=["balanced", "geographic"],
default="balanced",
help="Assignment strategy (default: balanced)",
)
parser.add_argument("--json", action="store_true", help="Output results as JSON")
args = parser.parse_args()
if not os.path.exists(args.accounts):
print(f"Error: File not found: {args.accounts}", file=sys.stderr)
sys.exit(1)
accounts = load_data(args.accounts)
if not accounts:
print("Error: No accounts found in input file.", file=sys.stderr)
sys.exit(1)
# Parse reps argument
rep_data = None
try:
num_reps = int(args.reps)
except ValueError:
if os.path.exists(args.reps):
rep_data = load_data(args.reps)
num_reps = len(rep_data)
else:
print(f"Error: '{args.reps}' is not a valid number or file path.", file=sys.stderr)
sys.exit(1)
if num_reps < 1:
print("Error: Need at least 1 rep.", file=sys.stderr)
sys.exit(1)
results = plan_territories(accounts, num_reps, rep_data, args.strategy)
if args.json:
print(json.dumps(results, indent=2))
else:
print(format_human(results))
cv = results["summary"]["balance_cv_pct"]
sys.exit(0 if cv < 25 else 1)
if __name__ == "__main__":
main()