DEV Community

Cover image for Building a Deal Health Score with CRM Data: A RevOps Engineering Walkthrough
SpurIQ Engineering
SpurIQ Engineering

Posted on

Building a Deal Health Score with CRM Data: A RevOps Engineering Walkthrough

There's a question every sales leader asks at least once a week, usually on a Friday afternoon right before they have to present the forecast: "Which of these deals are actually real?"

The honest answer is: nobody knows. Not really. Because most CRMs are full of deals that look active, the stage says Negotiation, the probability says 70%, the close date says end of month, but nobody's talked to the buyer in eleven days, the only stakeholder mapped is the person who took the original meeting and the last logged activity was a rep updating the stage field.

That's not a pipeline. That's a collection of optimistic assumptions.

A deal health score is what you build when you're tired of discovering that on a Thursday.

Why Stage + Probability Isn't Enough

The default CRM setup gives you two levers: stage and probability. Stage tells you where a deal sits in your process. Probability is either rep-assigned (wildly subjective) or stage-assigned (wildly generic). Neither one tells you whether the deal is actually moving.

What you actually need to know is: is this deal progressing toward a close, or is it just sitting in a field?

To answer that, you need to look at behavioral signals, things that happen (or don't happen) around the deal. Idle time. Activity volume. Stakeholder coverage. These are the inputs that tell you whether a deal has momentum or whether it's decaying quietly while nobody's watching.

That's exactly what a deal health score does. It's a weighted formula that turns multiple weak signals into one actionable number.

The Four Inputs That Actually Matter

Before you write a single line of code, you need to decide what you're measuring. Here's the framework that works consistently across different sales motions.

1. Pipeline Stage Weight

Not all stages carry equal weight. A deal in Discovery is early and uncertain. A deal in Contract Review is close and committed. Your score should reflect that.
Assign a stage multiplier between 0 and 1 based on your actual pipeline stages and win-rate data:

STAGE_WEIGHTS = {
    "Discovery": 0.2,
    "Qualified": 0.35,
    "Demo Completed": 0.5,
    "Proposal Sent": 0.65,
    "Negotiation": 0.8,
    "Contract Review": 0.9
}
Enter fullscreen mode Exit fullscreen mode

Calibrate these against your historical conversion rates by stage. If deals in "Proposal Sent" only close at 30% in your org, don't assign 0.65. Match the weight to the reality.

2. Idle Days Penalty

This is the most important signal most CRMs completely ignore. A deal that hasn't had buyer-side engagement in 14 days is a different deal than one where something happened yesterday.

def idle_days_score(last_buyer_activity_date):
    from datetime import date
    idle = (date.today() - last_buyer_activity_date).days

    if idle <= 3:
        return 1.0
    elif idle <= 7:
        return 0.75
    elif idle <= 14:
        return 0.5
    elif idle <= 21:
        return 0.25
    else:
        return 0.0  # deal is stalling hard
Enter fullscreen mode Exit fullscreen mode

One important note: you want buyer-side activity, not rep activity. A rep sending a follow-up email doesn't count. A buyer replying, joining a call, or forwarding to a colleague, that counts. Most CRMs track this loosely; you may need to parse email metadata or call logs to get this right.

3. Activity Volume Score

Volume signals effort and engagement. But raw count is misleading, 12 activities all from the rep side is not the same as 6 mutual exchanges. Cap the score to avoid inflating deals with a lot of one-sided noise.

def activity_score(total_activities, mutual_activities):
    # Weight mutual interactions more heavily
    raw = (total_activities * 0.3) + (mutual_activities * 0.7)
    # Cap at 1.0  high volume shouldn't mask other weak signals
    return min(raw / 10, 1.0)
Enter fullscreen mode Exit fullscreen mode

4. Stakeholder Coverage

Enterprise deals die when the champion disappears and you have no other relationships. Coverage score measures whether you've mapped the key people involved in the buying decision.

def stakeholder_score(contacts_mapped, decision_maker_identified, budget_holder_identified):
    score = 0
    score += 0.4 if decision_maker_identified else 0
    score += 0.4 if budget_holder_identified else 0
    score += min(contacts_mapped * 0.05, 0.2)  # bonus for breadth, capped
    return score
Enter fullscreen mode Exit fullscreen mode

The Weighted Formula

Now combine the four signals. The weights here are a starting point, tune them based on which signals are most predictive of close in your specific motion.

def calculate_deal_health_score(deal):
    stage_w = STAGE_WEIGHTS.get(deal['stage'], 0.1)
    idle_w = idle_days_score(deal['last_buyer_activity'])
    activity_w = activity_score(deal['total_activities'], deal['mutual_activities'])
    stakeholder_w = stakeholder_score(
        deal['contacts_mapped'],
        deal['decision_maker_identified'],
        deal['budget_holder_identified']
    )

    # Weighted combination - adjust these weights for your motion
    score = (
        stage_w * 0.25 +
        idle_w * 0.35 +        # Idle time matters most
        activity_w * 0.20 +
        stakeholder_w * 0.20
    )

    return round(score * 100, 1)  # Return as 0-100
Enter fullscreen mode Exit fullscreen mode

The output is a number between 0 and 100. Anything below 40 is a deal that needs immediate attention or should be moved to stalled. 40–65 is active but fragile. 65+ is genuinely healthy with momentum.

Running This Against Your CRM

The cleanest way to run this at scale is a daily batch job that pulls open deals via your CRM API, calculates scores and writes them back to a custom field.

import requests

def update_deal_scores(crm_deals):
    scored_deals = []

    for deal in crm_deals:
        health = calculate_deal_health_score(deal)

        scored_deals.append({
            "deal_id": deal['id'],
            "health_score": health,
            "risk_flag": "HIGH" if health < 40 else "MEDIUM" if health < 65 else "LOW"
        })

        # Write back to CRM
        update_crm_field(deal['id'], "deal_health_score", health)

    return scored_deals
Enter fullscreen mode Exit fullscreen mode

Once the scores live in your CRM, you can build a pipeline review dashboard that surfaces the 5 deals most at risk before your Monday morning meeting, instead of discovering them during it.

What This Unlocks

A deal health score isn't just a number on a dashboard. It's the foundation for:

  • Automated alerts when a deal crosses below a threshold, routed to the rep and manager
  • Forecast filtering that strips out low-health deals before they inflate the number
  • Coaching conversations grounded in actual deal behavior rather than rep memory
  • Handoff quality, when a rep leaves or goes on vacation, the incoming person can see exactly where momentum stands

This is exactly the kind of CRM data science that separates teams that forecast with confidence from teams that spend Friday afternoon reconciling CRM with reality.

Where This Connects to Execution

The score identifies the problem. The harder part is making sure something actually happens about it, that a stalled deal at score 28 triggers a follow-up, surfaces to the right manager and gets a specific next step attached to it before it dies.

That's the gap between visibility and execution. And closing it is what SpurIQ's DealIQ was built to do, turning signals like these into executed actions automatically, so pipeline health isn't just measured, it's managed.

Top comments (0)