
Account Executive
- 179 installs
- 451 repo stars
- Updated July 21, 2026
- borghei/claude-skills
Draft account plans, discovery questions, proposals, follow-ups, and renewal outreach for B2B SaaS account executives managing pipeline, expansion, and customer relationships.
About
Account executive skill from borghei/claude-skills that helps B2B sellers craft discovery notes, proposals, follow-ups, and renewal plans, supporting pipeline management, deal progression, and customer expansion for SaaS and services teams.
- B2B discovery and proposal drafting
- Pipeline and renewal communication templates
- Expansion and upsell messaging support
- Customer relationship planning artifacts
Account Executive by the numbers
- 179 all-time installs (skills.sh)
- Ranked #341 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 account-executiveAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 179 |
|---|---|
| repo stars | ★ 451 |
| Last updated | July 21, 2026 |
| Repository | borghei/claude-skills ↗ |
What it does
Draft account plans, discovery questions, proposals, follow-ups, and renewal outreach for B2B SaaS account executives managing pipeline, expansion, and customer relationships.
Files
Account Executive
The agent operates as an expert account executive, driving revenue through disciplined pipeline management, structured discovery, value-based selling, strategic negotiation, and accurate forecasting.
Workflow
1. Qualify the opportunity -- Score the lead against ICP criteria and MEDDIC dimensions. Confirm budget, authority, need, and timeline before advancing. Validate: qualification score reaches 18+ out of 30. 2. Run discovery -- Execute MEDDIC framework to map Metrics, Economic Buyer, Decision Criteria, Decision Process, Identify Pain, and Champion. Document findings in the discovery template. Validate: all six MEDDIC fields populated. 3. Deliver demo / evaluation -- Present solution mapped to the prospect's specific pain points and use cases. Engage all stakeholders identified during discovery. Validate: technical fit confirmed and champion provides positive feedback. 4. Build and deliver proposal -- Construct pricing aligned to the prospect's budget and value expectations. Include ROI justification. Validate: proposal accepted or objections documented for negotiation. 5. Negotiate and close -- Apply trade-based negotiation (never give without getting). Handle objections using the response framework. Validate: contract signed and payment terms confirmed. 6. Hand off to Customer Success -- Transfer account context including success criteria, stakeholder map, and implementation expectations. Validate: CS acknowledges receipt and kickoff is scheduled. 7. Update forecast -- Categorize deal accurately by confidence tier. Maintain pipeline hygiene weekly. Validate: all open opportunities have current close dates and documented next steps.
Sales Stages
| Stage | Probability | Entry Criteria | Exit Criteria |
|---|---|---|---|
| Prospect | 10% | Lead meets ICP | Meeting scheduled |
| Discovery | 20% | Meeting held | MEDDIC qualified |
| Demo/Evaluation | 40% | Technical fit confirmed | Demo delivered, stakeholders engaged |
| Proposal | 60% | Budget approved | Proposal accepted |
| Negotiation | 80% | Terms discussed | Contract agreed |
| Closed Won | 100% | Signed | Payment terms confirmed, CS handoff |
MEDDIC Discovery Framework
The agent uses MEDDIC to qualify every opportunity:
- Metrics -- "What measurable outcomes does the customer want? How would they measure success?"
- Economic Buyer -- "Who ultimately approves this purchase and controls the budget?"
- Decision Criteria -- "What are the must-haves vs. nice-to-haves driving the decision?"
- Decision Process -- "What steps, stakeholders, and timeline define the evaluation?"
- Identify Pain -- "What is the cost of inaction? What happens if this problem persists?"
- Champion -- "Who internally advocates for this solution and shares the vision?"
Discovery Questions by Category
Situation: Current process, existing tools/systems, team structure. Problem: What is working, what is not, frequency and severity of pain. Impact: Cost of the problem, team and business effects, consequences of inaction. Need: Ideal solution characteristics, priorities, required timeline.
Qualification Scorecard
| Criteria | Score (1-5) | Notes |
|---|---|---|
| Budget | ||
| Authority | ||
| Need | ||
| Timeline | ||
| Champion | ||
| Competition | ||
| Total | /30 |
- 25-30: Strong opportunity -- prioritize and advance aggressively.
- 18-24: Viable -- develop weak areas before proposal stage.
- Below 18: Needs further qualification or deprioritize.
Pipeline Management
Weekly Pipeline Hygiene
- [ ] Update all opportunity stages to reflect current reality
- [ ] Verify close dates are realistic (move or close stale deals)
- [ ] Confirm documented next steps with specific dates and owners
- [ ] Remove deals inactive for 30+ days without engagement
- [ ] Add newly qualified opportunities
Coverage Targets
Pipeline Coverage = Total Pipeline Value / Quota
Early quarter: 4-5x coverage
Mid quarter: 3x coverage
Late quarter: 1.5-2x coverageForecast Categories
| Category | Definition | Probability |
|---|---|---|
| Commit | Will close this period | 90%+ |
| Best Case | Strong chance to close | 60-90% |
| Pipeline | In active evaluation | 20-60% |
| Upside | Early stage, possible | <20% |
Negotiation Framework
Principles: 1. Never negotiate against yourself -- wait for the counter, use silence. 2. Trade, don't give -- "If I do X, will you commit to Y?" 3. Understand their constraints -- budget limits, approval thresholds, timing pressures. 4. Create win-win -- find creative structures (multi-year, phased rollout, usage tiers).
Objection Handling
| Objection | Response Approach |
|---|---|
| "Too expensive" | Reframe to ROI: "Compared to the cost of [problem], this pays for itself in [timeframe]." |
| "Need to think about it" | Surface concerns: "What specific questions should we address to move forward?" |
| "Competitor is cheaper" | Shift to total value: "Let's compare total cost of ownership including [implementation, support, outcomes]." |
| "Bad timing" | Understand triggers: "What would need to change? Let's plan for when the timing is right." |
| "Need more features" | Map to goals: "Which capabilities map to your top priorities? Let's focus there." |
Discount Guidelines
Standard (0-10%): AE authority, no approval needed.
Moderate (10-20%): Manager approval, documented justification.
Deep (20-30%): Director approval, strategic justification, quid pro quo required.
Exception (30%+): VP approval, executive sponsor, documented business case.Account Plan Template
# Account Plan: [Account Name]
## Account Overview
- Industry: [Industry] | Revenue: $[Amount] | Employees: [Number]
- Current ARR: $[Amount] | Whitespace: $[Amount]
## Relationship Map
| Name | Title | Role | Influence |
|------|-------|------|-----------|
| [Name] | [Title] | Champion | High |
| [Name] | [Title] | Economic Buyer | High |
## Strategy
- 90-day goals: [Goal 1], [Goal 2]
- 12-month goals: [Goal 1], [Goal 2]
## Action Plan
| Action | Owner | Due Date | Status |
|--------|-------|----------|--------|
| [Action] | [Name] | [Date] | [Status] |
## Risks
- [Risk]: [Mitigation plan]Example: Deal Progression
Opportunity: Acme Corp - Enterprise Platform
Stage: Proposal (60%)
Amount: $180,000 ACV
Close Date: 2026-03-28
Champion: VP Engineering (confirmed)
Econ Buyer: CTO (met, aligned on budget)
Next Step: Legal review of MSA by 2026-03-15
Risk: Procurement cycle may extend 2 weeks
Action: Send ROI summary to CTO for internal justificationScripts
# Pipeline analyzer
python scripts/pipeline_analyzer.py --data opportunities.csv
# Forecast calculator
python scripts/forecast.py --pipeline pipeline.csv --quarter Q4
# Win/loss analyzer
python scripts/win_loss.py --deals closed_deals.csv
# Account planner
python scripts/account_plan.py --account "Account Name"Troubleshooting
| Problem | Root Cause | Resolution |
|---|---|---|
| Deals stalling at Discovery stage | Incomplete MEDDPICC qualification; missing Economic Buyer access | Re-qualify using the scorecard. If Economic Buyer is inaccessible, ask Champion for a warm introduction. Research shows early decision-maker involvement boosts win rates by 55%. |
| Forecast accuracy below 70% | Over-reliance on rep gut feel; inconsistent stage definitions | Enforce stage entry/exit criteria. Require documented next steps with dates. Switch to weighted pipeline forecasting and validate commit deals weekly. |
| Win rate declining quarter-over-quarter | Poor upfront qualification; 63% of losses happen before needs assessment | Raise minimum qualification score to 20/30 before advancing past Discovery. Implement mandatory MEDDPICC field updates at every stage gate. |
| Champion goes dark mid-cycle | Single-threaded relationship; Champion may have changed roles or priorities | Multi-thread every deal with 3+ contacts. Reach out to other mapped stakeholders within 48 hours. Refresh the relationship map monthly. |
| Discounting eroding margins | Negotiating on price before establishing value; skipping ROI justification | Always present ROI analysis before any pricing discussion. Use trade-based negotiation: never concede without a reciprocal commitment. |
| Pipeline coverage drops below 3x | Insufficient prospecting activity; over-reliance on inbound | Dedicate 20% of weekly time to outbound prospecting. Set minimum weekly meeting targets. Review pipeline coverage every Monday. |
| Deals lost to competitors | Weak competitive positioning; late discovery of competitive evaluation | Ask about competitive alternatives in first Discovery call. Prepare battle cards and landmine questions. Engage sales engineering early for technical differentiation. |
Success Criteria
| Metric | Target | Measurement Method |
|---|---|---|
| Quota attainment | 100%+ quarterly | CRM closed-won revenue vs. assigned quota |
| Win rate | 25%+ overall; 35%+ for qualified pipeline | Won / (Won + Lost) excluding disqualified |
| Average deal size | Trending upward QoQ | Mean ACV of closed-won deals |
| Sales cycle length | Under 60 days for mid-market; under 90 for enterprise | Average days from Discovery to Closed Won |
| Pipeline coverage | 3-4x quota at all times | Total weighted pipeline / remaining quota |
| Forecast accuracy | Within 10% of actual | Abs(Forecast - Actual) / Actual per quarter |
| MEDDPICC completion | 100% for deals past Discovery | Percentage of qualified deals with all 6+ fields populated |
| Activity-to-close ratio | Improving QoQ | Meetings booked / Deals closed |
Scope & Limitations
In Scope:
- Full-cycle deal management from qualification through close and CS handoff
- MEDDPICC and BANT qualification frameworks for B2B enterprise and mid-market
- Pipeline management, forecasting, and weekly hygiene
- Negotiation strategy, objection handling, and proposal construction
- Account planning for strategic and named accounts
- Multi-stakeholder selling with 3-10 decision participants
Out of Scope:
- Lead generation and top-of-funnel prospecting strategy (see marketing/demand-acquisition)
- Post-sale customer success execution (see customer-success-manager)
- CRM administration, territory design, and comp plan architecture (see sales-operations)
- Technical demo delivery and POC management (see sales-engineer)
- Complex enterprise integration architecture (see solutions-architect)
- Legal contract review and procurement negotiation beyond commercial terms
Limitations:
- Qualification frameworks assume B2B SaaS or technology selling motions; adapt scoring weights for hardware, services, or transactional sales
- Pipeline velocity benchmarks are calibrated to mid-market ($50K-$500K ACV); adjust thresholds for SMB or enterprise segments
- Discount guidelines require alignment with your organization's specific approval matrix
- Scripts process local CSV/JSON data only; no CRM API integration
Integration Points
| Integration | Direction | Purpose | Handoff Artifact |
|---|---|---|---|
| Sales Engineer | AE -> SE | Technical validation, demo delivery, POC support | Discovery notes, stakeholder map, demo requirements |
| Sales Operations | Bidirectional | Pipeline data, territory assignments, forecast rollups, quota tracking | CRM opportunity records, forecast submissions |
| Customer Success Manager | AE -> CSM | Post-close handoff with account context | Success criteria doc, stakeholder map, implementation expectations, signed contract |
| Marketing (Demand Gen) | Marketing -> AE | MQL-to-SQL conversion, lead routing, campaign attribution | Qualified lead with engagement history and ICP score |
| Solutions Architect | AE -> SA | Complex enterprise deals requiring architecture design | Technical requirements, integration constraints, compliance needs |
| Product Team | AE -> Product | Feature requests, competitive intel, market feedback | Win/loss reports, feature gap analysis, competitive battle cards |
| Finance | Bidirectional | Deal desk approval, revenue recognition, payment terms | Signed MSA, order form, discount justification |
Workflow Handoff Protocol: 1. AE completes MEDDPICC qualification before requesting SE or SA engagement 2. AE submits forecast to Sales Ops weekly by end-of-day Friday 3. AE initiates CS handoff within 24 hours of contract signature using the handoff template 4. AE logs competitive intel in battle card repository after every competitive deal
Reference Materials
references/discovery.md-- Discovery frameworkreferences/negotiation.md-- Negotiation tacticsreferences/objections.md-- Objection handlingreferences/forecasting.md-- Forecasting best practices
#!/usr/bin/env python3
"""Score deal health using MEDDPICC qualification framework.
Reads deal data from CSV or JSON and produces a qualification score
for each opportunity based on MEDDPICC dimensions plus BANT criteria.
Usage:
python deal_scorer.py --data deals.csv
python deal_scorer.py --data deals.json --json
python deal_scorer.py --data deals.csv --threshold 60
"""
import argparse
import csv
import json
import os
import sys
from datetime import datetime
MEDDPICC_DIMENSIONS = [
"metrics",
"economic_buyer",
"decision_criteria",
"decision_process",
"paper_process",
"identify_pain",
"champion",
"competition",
]
BANT_DIMENSIONS = ["budget", "authority", "need", "timeline"]
SCORE_LABELS = {
(0, 30): ("Critical", "Needs immediate qualification or disqualification"),
(30, 50): ("Weak", "Significant gaps; address before advancing"),
(50, 70): ("Developing", "Viable but requires work on weak dimensions"),
(70, 85): ("Strong", "Well-qualified; advance with confidence"),
(85, 101): ("Exceptional", "Top-tier opportunity; prioritize and close"),
}
def load_data(filepath):
"""Load deal 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:
reader = csv.DictReader(f)
return list(reader)
else:
print(f"Error: Unsupported file format '{ext}'. Use .csv or .json.", file=sys.stderr)
sys.exit(1)
def parse_score(value, max_val=5):
"""Parse a score value, clamping to 0-max_val range."""
try:
score = float(value)
return max(0, min(score, max_val))
except (ValueError, TypeError):
return 0
def score_deal(deal, mode="meddpicc"):
"""Score a single deal using MEDDPICC or BANT framework.
Each dimension is scored 0-5. Total is normalized to 0-100.
"""
if mode == "meddpicc":
dimensions = MEDDPICC_DIMENSIONS
else:
dimensions = BANT_DIMENSIONS
scores = {}
for dim in dimensions:
raw = deal.get(dim, deal.get(dim.replace("_", " "), 0))
scores[dim] = parse_score(raw, 5)
max_possible = len(dimensions) * 5
raw_total = sum(scores.values())
normalized = (raw_total / max_possible) * 100 if max_possible > 0 else 0
label = "Unknown"
advice = ""
for (lo, hi), (lbl, adv) in SCORE_LABELS.items():
if lo <= normalized < hi:
label = lbl
advice = adv
break
weak_dimensions = [dim for dim, score in scores.items() if score < 3]
strong_dimensions = [dim for dim, score in scores.items() if score >= 4]
return {
"deal_name": deal.get("deal_name", deal.get("name", deal.get("opportunity", "Unknown"))),
"stage": deal.get("stage", "Unknown"),
"amount": deal.get("amount", deal.get("acv", "N/A")),
"framework": mode.upper(),
"dimension_scores": scores,
"raw_total": round(raw_total, 1),
"max_possible": max_possible,
"normalized_score": round(normalized, 1),
"label": label,
"advice": advice,
"weak_dimensions": weak_dimensions,
"strong_dimensions": strong_dimensions,
}
def format_human(results, threshold):
"""Format results for human-readable output."""
lines = []
lines.append("=" * 70)
lines.append("DEAL QUALIFICATION SCORECARD")
lines.append(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M')}")
lines.append(f"Threshold: {threshold}/100")
lines.append("=" * 70)
above = [r for r in results if r["normalized_score"] >= threshold]
below = [r for r in results if r["normalized_score"] < threshold]
for result in sorted(results, key=lambda x: x["normalized_score"], reverse=True):
lines.append("")
lines.append(f" Deal: {result['deal_name']}")
lines.append(f" Stage: {result['stage']} | Amount: {result['amount']}")
lines.append(f" Framework: {result['framework']}")
lines.append(f" Score: {result['normalized_score']}/100 ({result['raw_total']}/{result['max_possible']})")
lines.append(f" Rating: {result['label']}")
lines.append(f" Assessment: {result['advice']}")
lines.append("")
lines.append(" Dimension Scores:")
for dim, score in result["dimension_scores"].items():
bar = "#" * int(score) + "." * (5 - int(score))
flag = " << WEAK" if score < 3 else ""
lines.append(f" {dim:20s} [{bar}] {score}/5{flag}")
if result["weak_dimensions"]:
lines.append(f"\n Action Required: Strengthen {', '.join(result['weak_dimensions'])}")
if result["strong_dimensions"]:
lines.append(f" Strengths: {', '.join(result['strong_dimensions'])}")
status = "ABOVE" if result["normalized_score"] >= threshold else "BELOW"
lines.append(f" Threshold Status: {status}")
lines.append("-" * 70)
lines.append("")
lines.append("SUMMARY")
lines.append(f" Total deals scored: {len(results)}")
lines.append(f" Above threshold ({threshold}): {len(above)}")
lines.append(f" Below threshold ({threshold}): {len(below)}")
if results:
avg = sum(r["normalized_score"] for r in results) / len(results)
lines.append(f" Average score: {avg:.1f}/100")
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(
description="Score deal health using MEDDPICC or BANT qualification framework."
)
parser.add_argument("--data", required=True, help="Path to deals CSV or JSON file")
parser.add_argument(
"--mode",
choices=["meddpicc", "bant"],
default="meddpicc",
help="Qualification framework (default: meddpicc)",
)
parser.add_argument(
"--threshold",
type=float,
default=60,
help="Minimum qualification score 0-100 (default: 60)",
)
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)
deals = load_data(args.data)
if not deals:
print("Error: No deals found in input file.", file=sys.stderr)
sys.exit(1)
results = [score_deal(deal, args.mode) for deal in deals]
if args.json:
print(json.dumps(results, indent=2))
else:
print(format_human(results, args.threshold))
below = [r for r in results if r["normalized_score"] < args.threshold]
sys.exit(1 if below else 0)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Analyze sales pipeline for forecast accuracy, stage velocity, and health.
Reads opportunity data from CSV or JSON and produces pipeline coverage,
stage conversion rates, deal aging, velocity metrics, and forecast projections.
Usage:
python pipeline_analyzer.py --data opportunities.csv --quota 2500000
python pipeline_analyzer.py --data opportunities.json --quota 5000000 --json
"""
import argparse
import csv
import json
import os
import sys
from collections import defaultdict
from datetime import datetime, timedelta
STAGE_ORDER = {
"prospect": 0,
"discovery": 1,
"demo": 2,
"evaluation": 3,
"proposal": 4,
"negotiation": 5,
"closed_won": 6,
"closed_lost": 7,
}
STAGE_PROBABILITIES = {
"prospect": 0.10,
"discovery": 0.20,
"demo": 0.40,
"evaluation": 0.40,
"proposal": 0.60,
"negotiation": 0.80,
"closed_won": 1.00,
"closed_lost": 0.00,
}
AGING_THRESHOLDS = {
"prospect": 14,
"discovery": 21,
"demo": 14,
"evaluation": 21,
"proposal": 14,
"negotiation": 21,
}
def load_data(filepath):
"""Load opportunity 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 parse_amount(value):
"""Parse monetary amount from string."""
if not value:
return 0.0
cleaned = str(value).replace("$", "").replace(",", "").strip()
try:
return float(cleaned)
except ValueError:
return 0.0
def parse_date(value):
"""Parse date from common formats."""
if not value:
return None
for fmt in ["%Y-%m-%d", "%m/%d/%Y", "%d/%m/%Y", "%Y-%m-%dT%H:%M:%S"]:
try:
return datetime.strptime(str(value).strip(), fmt)
except ValueError:
continue
return None
def normalize_stage(stage):
"""Normalize stage name to standard key."""
if not stage:
return "unknown"
s = stage.lower().strip().replace(" ", "_").replace("/", "_")
for key in STAGE_ORDER:
if key in s:
return key
return s
def analyze_pipeline(opportunities, quota):
"""Run full pipeline analysis."""
today = datetime.now()
results = {
"total_opportunities": 0,
"total_pipeline_value": 0,
"weighted_pipeline_value": 0,
"quota": quota,
"coverage_ratio": 0,
"weighted_coverage": 0,
"stage_summary": {},
"aging_alerts": [],
"velocity_metrics": {},
"forecast": {},
}
stage_deals = defaultdict(list)
stage_values = defaultdict(float)
stage_counts = defaultdict(int)
cycle_times = []
won_values = []
lost_values = []
for opp in opportunities:
stage = normalize_stage(opp.get("stage", ""))
amount = parse_amount(opp.get("amount", opp.get("acv", opp.get("value", 0))))
created_date = parse_date(opp.get("created_date", opp.get("create_date", "")))
close_date = parse_date(opp.get("close_date", opp.get("expected_close", "")))
stage_date = parse_date(opp.get("stage_date", opp.get("last_stage_change", "")))
if stage in ("closed_won", "closed_lost"):
if stage == "closed_won":
won_values.append(amount)
if created_date and close_date:
days = (close_date - created_date).days
if days > 0:
cycle_times.append(days)
else:
lost_values.append(amount)
continue
results["total_opportunities"] += 1
prob = STAGE_PROBABILITIES.get(stage, 0.20)
weighted = amount * prob
results["total_pipeline_value"] += amount
results["weighted_pipeline_value"] += weighted
stage_deals[stage].append({
"name": opp.get("name", opp.get("deal_name", opp.get("opportunity", "Unknown"))),
"amount": amount,
"weighted": round(weighted, 2),
"close_date": close_date.strftime("%Y-%m-%d") if close_date else "Not set",
})
stage_values[stage] += amount
stage_counts[stage] += 1
# Check aging
if stage_date:
days_in_stage = (today - stage_date).days
threshold = AGING_THRESHOLDS.get(stage, 21)
if days_in_stage > threshold:
results["aging_alerts"].append({
"deal": opp.get("name", opp.get("deal_name", "Unknown")),
"stage": stage,
"days_in_stage": days_in_stage,
"threshold": threshold,
"amount": amount,
})
# Coverage ratios
if quota > 0:
results["coverage_ratio"] = round(results["total_pipeline_value"] / quota, 2)
results["weighted_coverage"] = round(results["weighted_pipeline_value"] / quota, 2)
# Stage summary
for stage in sorted(stage_counts.keys(), key=lambda s: STAGE_ORDER.get(s, 99)):
results["stage_summary"][stage] = {
"count": stage_counts[stage],
"total_value": round(stage_values[stage], 2),
"avg_deal_size": round(stage_values[stage] / stage_counts[stage], 2) if stage_counts[stage] > 0 else 0,
"probability": STAGE_PROBABILITIES.get(stage, 0.20),
"weighted_value": round(stage_values[stage] * STAGE_PROBABILITIES.get(stage, 0.20), 2),
}
# Velocity metrics
total_won = len(won_values)
total_lost = len(lost_values)
total_decided = total_won + total_lost
results["velocity_metrics"] = {
"avg_cycle_time_days": round(sum(cycle_times) / len(cycle_times), 1) if cycle_times else 0,
"median_cycle_time_days": sorted(cycle_times)[len(cycle_times) // 2] if cycle_times else 0,
"win_rate": round(total_won / total_decided * 100, 1) if total_decided > 0 else 0,
"avg_won_deal_size": round(sum(won_values) / total_won, 2) if won_values else 0,
"avg_lost_deal_size": round(sum(lost_values) / total_lost, 2) if lost_values else 0,
"total_won": total_won,
"total_lost": total_lost,
}
# Forecast
gap = quota - results["weighted_pipeline_value"]
open_pipeline = results["total_pipeline_value"]
win_rate = results["velocity_metrics"]["win_rate"]
required_win_rate = (gap / open_pipeline * 100) if open_pipeline > 0 and gap > 0 else 0
results["forecast"] = {
"weighted_forecast": round(results["weighted_pipeline_value"], 2),
"gap_to_quota": round(max(gap, 0), 2),
"gap_percentage": round(max(gap, 0) / quota * 100, 1) if quota > 0 else 0,
"required_win_rate": round(required_win_rate, 1),
"current_win_rate": win_rate,
"on_track": gap <= 0,
}
results["total_pipeline_value"] = round(results["total_pipeline_value"], 2)
results["weighted_pipeline_value"] = round(results["weighted_pipeline_value"], 2)
return results
def format_human(results):
"""Format results for human-readable output."""
lines = []
lines.append("=" * 70)
lines.append("PIPELINE ANALYSIS REPORT")
lines.append(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M')}")
lines.append("=" * 70)
lines.append(f"\n Open Opportunities: {results['total_opportunities']}")
lines.append(f" Total Pipeline Value: ${results['total_pipeline_value']:,.2f}")
lines.append(f" Weighted Pipeline: ${results['weighted_pipeline_value']:,.2f}")
lines.append(f" Quota: ${results['quota']:,.2f}")
lines.append(f" Coverage Ratio: {results['coverage_ratio']}x")
lines.append(f" Weighted Coverage: {results['weighted_coverage']}x")
cov = results["coverage_ratio"]
if cov >= 4:
lines.append(" Coverage Status: HEALTHY (4x+)")
elif cov >= 3:
lines.append(" Coverage Status: ADEQUATE (3x)")
elif cov >= 2:
lines.append(" Coverage Status: WARNING (below 3x)")
else:
lines.append(" Coverage Status: CRITICAL (below 2x)")
lines.append(f"\n{'STAGE BREAKDOWN':^70}")
lines.append("-" * 70)
lines.append(f" {'Stage':<16} {'Count':>6} {'Value':>14} {'Prob':>6} {'Weighted':>14}")
lines.append(" " + "-" * 58)
for stage, data in results["stage_summary"].items():
lines.append(
f" {stage:<16} {data['count']:>6} "
f"${data['total_value']:>12,.2f} "
f"{data['probability']:>5.0%} "
f"${data['weighted_value']:>12,.2f}"
)
vm = results["velocity_metrics"]
lines.append(f"\n{'VELOCITY METRICS':^70}")
lines.append("-" * 70)
lines.append(f" Avg Sales Cycle: {vm['avg_cycle_time_days']} days")
lines.append(f" Median Sales Cycle: {vm['median_cycle_time_days']} days")
lines.append(f" Win Rate: {vm['win_rate']}%")
lines.append(f" Avg Won Deal Size: ${vm['avg_won_deal_size']:,.2f}")
lines.append(f" Won Deals: {vm['total_won']}")
lines.append(f" Lost Deals: {vm['total_lost']}")
fc = results["forecast"]
lines.append(f"\n{'FORECAST':^70}")
lines.append("-" * 70)
lines.append(f" Weighted Forecast: ${fc['weighted_forecast']:,.2f}")
lines.append(f" Gap to Quota: ${fc['gap_to_quota']:,.2f} ({fc['gap_percentage']}%)")
lines.append(f" Required Win Rate: {fc['required_win_rate']}%")
lines.append(f" Current Win Rate: {fc['current_win_rate']}%")
status = "ON TRACK" if fc["on_track"] else "AT RISK"
lines.append(f" Status: {status}")
if results["aging_alerts"]:
lines.append(f"\n{'AGING ALERTS':^70}")
lines.append("-" * 70)
for alert in sorted(results["aging_alerts"], key=lambda a: a["days_in_stage"], reverse=True):
lines.append(
f" {alert['deal']:<30} {alert['stage']:<14} "
f"{alert['days_in_stage']}d (threshold: {alert['threshold']}d) "
f"${alert['amount']:,.2f}"
)
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(
description="Analyze sales pipeline for coverage, velocity, and forecast accuracy."
)
parser.add_argument("--data", required=True, help="Path to opportunities CSV or JSON file")
parser.add_argument(
"--quota", type=float, default=2500000, help="Quarterly quota target (default: 2500000)"
)
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)
opportunities = load_data(args.data)
if not opportunities:
print("Error: No opportunities found in input file.", file=sys.stderr)
sys.exit(1)
results = analyze_pipeline(opportunities, args.quota)
if args.json:
print(json.dumps(results, indent=2))
else:
print(format_human(results))
sys.exit(0 if results["forecast"]["on_track"] else 1)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Analyze win/loss patterns from closed deal data.
Reads closed deal data from CSV or JSON and identifies patterns in wins vs.
losses across dimensions: deal size, sales cycle, competitor, industry,
lead source, and qualification score.
Usage:
python win_loss_analyzer.py --data closed_deals.csv
python win_loss_analyzer.py --data deals.json --json
python win_loss_analyzer.py --data deals.csv --min-deals 5
"""
import argparse
import csv
import json
import math
import os
import sys
from collections import defaultdict
from datetime import datetime
def load_data(filepath):
"""Load closed deal 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 parse_amount(value):
"""Parse monetary amount from string."""
if not value:
return 0.0
cleaned = str(value).replace("$", "").replace(",", "").strip()
try:
return float(cleaned)
except ValueError:
return 0.0
def parse_days(value):
"""Parse numeric days value."""
try:
return int(float(str(value).strip()))
except (ValueError, TypeError):
return 0
def is_won(deal):
"""Determine if a deal was won."""
outcome = str(deal.get("outcome", deal.get("stage", deal.get("status", "")))).lower().strip()
return outcome in ("won", "closed_won", "closed won", "win", "1", "true")
def bucket_amount(amount):
"""Categorize deal amount into size buckets."""
if amount < 25000:
return "SMB (<$25K)"
elif amount < 100000:
return "Mid-Market ($25K-$100K)"
elif amount < 500000:
return "Enterprise ($100K-$500K)"
else:
return "Strategic ($500K+)"
def bucket_cycle(days):
"""Categorize sales cycle length into buckets."""
if days <= 0:
return "Unknown"
elif days <= 30:
return "Fast (0-30d)"
elif days <= 60:
return "Normal (31-60d)"
elif days <= 90:
return "Extended (61-90d)"
else:
return "Long (90d+)"
def analyze_dimension(deals, get_key, min_deals=3):
"""Analyze win/loss rates by a given dimension."""
groups = defaultdict(lambda: {"won": 0, "lost": 0, "won_value": 0, "lost_value": 0})
for deal in deals:
key = get_key(deal)
if not key or key == "Unknown" or key == "":
key = "Unspecified"
amount = parse_amount(deal.get("amount", deal.get("acv", deal.get("value", 0))))
if is_won(deal):
groups[key]["won"] += 1
groups[key]["won_value"] += amount
else:
groups[key]["lost"] += 1
groups[key]["lost_value"] += amount
results = {}
for key, data in groups.items():
total = data["won"] + data["lost"]
if total >= min_deals:
results[key] = {
"won": data["won"],
"lost": data["lost"],
"total": total,
"win_rate": round(data["won"] / total * 100, 1),
"won_value": round(data["won_value"], 2),
"lost_value": round(data["lost_value"], 2),
"avg_won_value": round(data["won_value"] / data["won"], 2) if data["won"] > 0 else 0,
}
return dict(sorted(results.items(), key=lambda x: x[1]["win_rate"], reverse=True))
def find_patterns(analysis_results):
"""Identify key patterns and actionable insights."""
patterns = []
for dimension, data in analysis_results.items():
if not data:
continue
rates = [(k, v["win_rate"], v["total"]) for k, v in data.items()]
if len(rates) < 2:
continue
best = max(rates, key=lambda x: x[1])
worst = min(rates, key=lambda x: x[1])
if best[1] - worst[1] > 15:
patterns.append({
"dimension": dimension,
"finding": f"Win rate varies significantly: {best[0]} ({best[1]}%) vs {worst[0]} ({worst[1]}%)",
"spread": round(best[1] - worst[1], 1),
"recommendation": f"Investigate what drives success in '{best[0]}' and apply lessons to '{worst[0]}'",
"priority": "high" if best[1] - worst[1] > 30 else "medium",
})
return sorted(patterns, key=lambda p: p["spread"], reverse=True)
def analyze_loss_reasons(deals):
"""Analyze primary loss reasons if provided."""
reasons = defaultdict(lambda: {"count": 0, "total_value": 0})
for deal in deals:
if is_won(deal):
continue
reason = deal.get("loss_reason", deal.get("close_reason", deal.get("reason", "")))
if reason:
reason = str(reason).strip()
amount = parse_amount(deal.get("amount", deal.get("acv", 0)))
reasons[reason]["count"] += 1
reasons[reason]["total_value"] += amount
return dict(sorted(reasons.items(), key=lambda x: x[1]["count"], reverse=True))
def format_human(results, patterns, loss_reasons):
"""Format results for human-readable output."""
lines = []
lines.append("=" * 70)
lines.append("WIN/LOSS ANALYSIS REPORT")
lines.append(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M')}")
lines.append("=" * 70)
summary = results.get("summary", {})
lines.append(f"\n Total Deals Analyzed: {summary.get('total_deals', 0)}")
lines.append(f" Won: {summary.get('total_won', 0)} | Lost: {summary.get('total_lost', 0)}")
lines.append(f" Overall Win Rate: {summary.get('overall_win_rate', 0)}%")
lines.append(f" Total Won Revenue: ${summary.get('total_won_value', 0):,.2f}")
lines.append(f" Total Lost Revenue: ${summary.get('total_lost_value', 0):,.2f}")
for dimension, data in results.items():
if dimension == "summary":
continue
if not data:
continue
title = dimension.replace("_", " ").upper()
lines.append(f"\n{'WIN RATE BY ' + title:^70}")
lines.append("-" * 70)
lines.append(f" {'Category':<30} {'Won':>5} {'Lost':>5} {'Total':>6} {'Win Rate':>9}")
lines.append(" " + "-" * 57)
for category, stats in data.items():
wr = stats["win_rate"]
indicator = " ***" if wr >= 40 else " !" if wr < 15 else ""
lines.append(
f" {category:<30} {stats['won']:>5} {stats['lost']:>5} "
f"{stats['total']:>6} {wr:>8.1f}%{indicator}"
)
if loss_reasons:
lines.append(f"\n{'LOSS REASONS':^70}")
lines.append("-" * 70)
lines.append(f" {'Reason':<40} {'Count':>6} {'Lost Value':>14}")
lines.append(" " + "-" * 62)
for reason, data in loss_reasons.items():
lines.append(f" {reason:<40} {data['count']:>6} ${data['total_value']:>12,.2f}")
if patterns:
lines.append(f"\n{'KEY PATTERNS & RECOMMENDATIONS':^70}")
lines.append("-" * 70)
for i, p in enumerate(patterns, 1):
lines.append(f"\n {i}. [{p['priority'].upper()}] {p['dimension'].replace('_', ' ').title()}")
lines.append(f" Finding: {p['finding']}")
lines.append(f" Action: {p['recommendation']}")
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(
description="Analyze win/loss patterns from closed deal data."
)
parser.add_argument("--data", required=True, help="Path to closed deals CSV or JSON file")
parser.add_argument(
"--min-deals",
type=int,
default=3,
help="Minimum deals per category to include in analysis (default: 3)",
)
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)
deals = load_data(args.data)
if not deals:
print("Error: No deals found in input file.", file=sys.stderr)
sys.exit(1)
total_won = sum(1 for d in deals if is_won(d))
total_lost = len(deals) - total_won
won_value = sum(parse_amount(d.get("amount", d.get("acv", 0))) for d in deals if is_won(d))
lost_value = sum(parse_amount(d.get("amount", d.get("acv", 0))) for d in deals if not is_won(d))
results = {
"summary": {
"total_deals": len(deals),
"total_won": total_won,
"total_lost": total_lost,
"overall_win_rate": round(total_won / len(deals) * 100, 1) if deals else 0,
"total_won_value": round(won_value, 2),
"total_lost_value": round(lost_value, 2),
},
"deal_size": analyze_dimension(
deals,
lambda d: bucket_amount(parse_amount(d.get("amount", d.get("acv", 0)))),
args.min_deals,
),
"sales_cycle": analyze_dimension(
deals,
lambda d: bucket_cycle(parse_days(d.get("cycle_days", d.get("sales_cycle", 0)))),
args.min_deals,
),
"competitor": analyze_dimension(
deals,
lambda d: d.get("competitor", d.get("primary_competitor", "")),
args.min_deals,
),
"industry": analyze_dimension(
deals, lambda d: d.get("industry", ""), args.min_deals
),
"lead_source": analyze_dimension(
deals, lambda d: d.get("lead_source", d.get("source", "")), args.min_deals
),
}
all_dimension_data = {k: v for k, v in results.items() if k != "summary"}
patterns = find_patterns(all_dimension_data)
loss_reasons = analyze_loss_reasons(deals)
if args.json:
output = {
**results,
"patterns": patterns,
"loss_reasons": loss_reasons,
}
print(json.dumps(output, indent=2))
else:
print(format_human(results, patterns, loss_reasons))
sys.exit(0)
if __name__ == "__main__":
main()