Blog

Automating Lead Degradation Scoring for Dead Prospects

Ankit Dhiman

Min Read

Lead scores go up but never down. Here's the time-decay algorithm that automatically degrades stale leads, unassigns reps, and recycles them into nurture.


Your "Hot" Lead Attended a Webinar in October. It's April.

The lead score says 74. The tag says "Marketing Qualified." The rep has sent six follow-up emails since November with no reply. The system still thinks this is a priority prospect because nobody built a mechanism for scores to go down.

This is the silent revenue drain that almost no scoring implementation accounts for: lead scores are treated as permanent achievements rather than real-time signals. A prospect racks up points for attending a webinar, downloading a whitepaper, and clicking an email sequence — all in Q4. It's now Q2. They haven't opened an email in 14 weeks. The score still reads 74 because nothing subtracted from it.

The rep is still following up because the system says to. The prospect has mentally moved on, changed priorities, or already bought from a competitor. And the nurture automation isn't touching them because they're classified as a sales-owned lead — even though sales has effectively abandoned them after six ignored touchpoints.

The result: a pipeline of phantom "hot" leads that waste rep time, clog the CRM, and prevent the marketing team from re-engaging prospects who might convert later under different circumstances.

The Asymmetry Problem in Lead Scoring

Every lead scoring system is architecturally asymmetric. Points are added constantly — email opens, page visits, form submissions, event registrations. Points are almost never subtracted systematically. The few systems that have negative scoring apply static penalties: unsubscribes, competitor domain detection, bounce events. These are one-time flags, not ongoing decay.

The result is a directional ratchet: scores only move meaningfully upward. A lead that was highly active six months ago looks identical in the CRM to one that became active yesterday. Your sales motion treats them the same way because the system can't distinguish between recent intent and historical activity.

Time-decay scoring fixes this asymmetry by treating lead scores as depreciating assets rather than accumulated totals. Every day without engagement reduces the score by a calculated amount. The decay rate varies by signal type — a pricing page visit decays faster than a product trial signup — because different signals have different intent half-lives. A rep assigned to a lead with a decayed score of 22 that was 74 six months ago sees a fundamentally different picture than one looking at a fresh 74. That difference changes behavior, prioritization, and forecast accuracy.

The Time-Decay Algorithm: How It Works

The mathematical foundation is a standard exponential decay function applied to each scoring component independently:

Scurrent=Sinitial×e−λtS_{\text{current}} = S_{\text{initial}} \times e^{-\lambda t}Scurrent​=Sinitial​×e−λt

Where λ\lambdaλ is the decay constant (determined by the signal's intent half-life) and ttt is days since the signal fired. In practice, you don't need to implement continuous exponential decay — a nightly discrete approximation is operationally equivalent and far easier to maintain.

Signal Half-Lives by Category


Signal Type

Intent Half-Life

Decay Rate Per Day

Pricing page visit

7 days

−4.2 pts/day

Demo request (no-show)

14 days

−2.1 pts/day

Webinar attendance

30 days

−1.0 pt/day

Whitepaper download

45 days

−0.67 pts/day

Email open (pre-MPP)

10 days

−3.0 pts/day

Product trial activity

21 days

−1.4 pts/day

The pricing page visit decays fastest because it's the most time-sensitive signal. A prospect researching pricing is in an active evaluation window — if they haven't converted or re-engaged within two weeks, the window has likely passed. A whitepaper download has a much longer half-life because educational content consumption happens earlier in the buying cycle and represents slower-moving research behavior.

Building the Decay Workflow in n8n

The decay engine runs as a nightly scheduled workflow — 1 AM, before leadership pulls morning pipeline reports and before reps start their day.

Step 1: Pull All Active Scored Leads

Query Salesforce via HTTP Request node for every Lead and Contact with a score above your minimum threshold and a IsConverted = false flag:


sqlSELECT Id, Lead_Score__c, Score_Components__c,
       Last_Engagement_Date__c, OwnerId, RecordType.Name
FROM Lead
WHERE Lead_Score__c > 10
  AND IsConverted = false
  AND Last_Engagement_Date__c != null
ORDER BY Last_Engagement_Date__c ASC
LIMIT 500
sqlSELECT Id, Lead_Score__c, Score_Components__c,
       Last_Engagement_Date__c, OwnerId, RecordType.Name
FROM Lead
WHERE Lead_Score__c > 10
  AND IsConverted = false
  AND Last_Engagement_Date__c != null
ORDER BY Last_Engagement_Date__c ASC
LIMIT 500
sqlSELECT Id, Lead_Score__c, Score_Components__c,
       Last_Engagement_Date__c, OwnerId, RecordType.Name
FROM Lead
WHERE Lead_Score__c > 10
  AND IsConverted = false
  AND Last_Engagement_Date__c != null
ORDER BY Last_Engagement_Date__c ASC
LIMIT 500

The Score_Components__c field should be a JSON blob storing each scoring event with its timestamp — not just the aggregate score. If you're only storing the total, you can't apply differential decay rates. You need the signal history. This is a field schema change worth making before you build the decay engine, not after.

Step 2: Calculate Decayed Score Per Lead

The Function node iterates through each lead's score components and applies the decay formula:


javascriptconst decayRates = {
  pricing_page_visit: 4.2,
  demo_no_show: 2.1,
  webinar_attendance: 1.0,
  content_download: 0.67,
  email_open: 3.0,
  product_trial: 1.4,
  form_submission: 1.5
};

function calculateDecayedScore(components) {
  let totalScore = 0;

  for (const event of components) {
    const daysSince = Math.floor(
      (Date.now() - new Date(event.timestamp)) / 86400000
    );
    const rate = decayRates[event.type] || 1.0;
    const decayedPoints = event.original_points * Math.exp(-rate * daysSince / 100);
    totalScore += Math.max(0, decayedPoints); // floor at 0
  }

  return Math.round(totalScore);
}

return items.map(item => {
  const components = JSON.parse(item.json.Score_Components__c || "[]");
  const decayedScore = calculateDecayedScore(components);
  const previousScore = item.json.Lead_Score__c;
  const scoreDelta = previousScore - decayedScore;

  return {
    json: {
      ...item.json,
      decayed_score: decayedScore,
      score_delta: scoreDelta,
      previous_score: previousScore
    }
  };
});
javascriptconst decayRates = {
  pricing_page_visit: 4.2,
  demo_no_show: 2.1,
  webinar_attendance: 1.0,
  content_download: 0.67,
  email_open: 3.0,
  product_trial: 1.4,
  form_submission: 1.5
};

function calculateDecayedScore(components) {
  let totalScore = 0;

  for (const event of components) {
    const daysSince = Math.floor(
      (Date.now() - new Date(event.timestamp)) / 86400000
    );
    const rate = decayRates[event.type] || 1.0;
    const decayedPoints = event.original_points * Math.exp(-rate * daysSince / 100);
    totalScore += Math.max(0, decayedPoints); // floor at 0
  }

  return Math.round(totalScore);
}

return items.map(item => {
  const components = JSON.parse(item.json.Score_Components__c || "[]");
  const decayedScore = calculateDecayedScore(components);
  const previousScore = item.json.Lead_Score__c;
  const scoreDelta = previousScore - decayedScore;

  return {
    json: {
      ...item.json,
      decayed_score: decayedScore,
      score_delta: scoreDelta,
      previous_score: previousScore
    }
  };
});
javascriptconst decayRates = {
  pricing_page_visit: 4.2,
  demo_no_show: 2.1,
  webinar_attendance: 1.0,
  content_download: 0.67,
  email_open: 3.0,
  product_trial: 1.4,
  form_submission: 1.5
};

function calculateDecayedScore(components) {
  let totalScore = 0;

  for (const event of components) {
    const daysSince = Math.floor(
      (Date.now() - new Date(event.timestamp)) / 86400000
    );
    const rate = decayRates[event.type] || 1.0;
    const decayedPoints = event.original_points * Math.exp(-rate * daysSince / 100);
    totalScore += Math.max(0, decayedPoints); // floor at 0
  }

  return Math.round(totalScore);
}

return items.map(item => {
  const components = JSON.parse(item.json.Score_Components__c || "[]");
  const decayedScore = calculateDecayedScore(components);
  const previousScore = item.json.Lead_Score__c;
  const scoreDelta = previousScore - decayedScore;

  return {
    json: {
      ...item.json,
      decayed_score: decayedScore,
      score_delta: scoreDelta,
      previous_score: previousScore
    }
  };
});

The score_delta field is what drives the downstream routing decisions. A delta of 5 over the past week is normal drift. A delta of 40 over the past 30 days means this lead has gone almost completely cold and needs to be handled differently.

Step 3: Routing Based on Post-Decay Score Tier

After calculating the decayed score, a Switch node routes each lead into one of four operational buckets:

Bucket A — Score ≥ 65 (Remains Hot):
No action. Score is written back to Salesforce. Rep assignment unchanged.

Bucket B — Score 40–64 (Degraded to Warm):

  • Update Lead_Score__c and Score_Tier__c = "Warm" in Salesforce

  • Send internal Slack alert to assigned rep: "Lead score for [Name] at [Company] has degraded from [previous] to [current]. Last engagement: [X] days ago. Consider refreshing your outreach angle."

  • No unassignment yet — rep retains ownership with visibility into the decay

Bucket C — Score 15–39 (Degraded to Cold — Rep Unassignment):

  • Update score and tier in Salesforce

  • Unassign the rep — set OwnerId to the Marketing Queue owner ID

  • Create a Salesforce Task for the rep's manager: "[Lead name] degraded to Cold score ([current]). Rep unassigned. Entering marketing nurture."

  • Enroll in marketing re-engagement nurture sequence via your marketing automation platform (HubSpot workflow trigger, Marketo webhook, or direct Apollo sequence enrollment)

  • Write Nurture_Re_Entry_Date__c = today to the lead record

Bucket D — Score < 15 (Suppress):

  • Archive the lead: Is_Suppressed__c = true

  • Remove from all active sequences

  • Do not delete — the historical record has value for future re-engagement triggers (job change, funding event, new intent signal)

  • Log to a PostgreSQL "cold storage" table for monthly re-evaluation

The unassignment in Bucket C is the operationally significant action most teams resist building. Reps feel they're losing a prospect. What's actually happening is the opposite: a rep hanging onto a 30-point lead that was 74 six months ago is spending time on probability that doesn't exist while neglecting higher-intent leads in their queue. The unassignment is a prioritization gift, not a punishment.

The Re-Activation Trigger: When Decayed Leads Come Back

The decay engine needs a symmetric re-activation mechanism. When a suppressed or cold-stored lead generates a new high-intent signal — visits pricing again, starts a new trial, fires an intent event in 6sense — the workflow should:

  1. Recalculate the score from scratch using only signals from the last 90 days (historical decay events are archived, not carried forward)

  2. Re-assign to sales if the new score clears your MQL threshold

  3. Flag the record with Re_Activated__c = true and Re_Activation_Source__c = [signal type] for pipeline reporting

Re-activated leads that originally went cold convert at a different rate than first-time MQLs — understanding that rate for your specific pipeline is valuable intelligence for forecasting and for determining how aggressively to work re-activation signals.

What Changes When Decay Is Automated

The operational impact isn't just cleaner data. It's a fundamental change in how reps experience the lead queue:

  • Every lead in the "Hot" tier has shown intent recently, not historically

  • Reps stop wasting follow-up cycles on prospects the system falsely reports as warm

  • Marketing re-engagement runs on leads that have genuine re-activation potential rather than sitting in a rep's assigned queue indefinitely

  • Forecast accuracy improves because scored pipeline reflects current intent distribution, not cumulative activity history

The most valuable outcome is behavioral: when reps learn the system degrades scores for inactivity, they start engaging leads earlier and more meaningfully to keep them warm — because they can see the score moving in real time, not just accumulating.

That's the difference between a scoring model that generates reports and one that actually changes how your revenue team operates.

Your Lead Lifecycle Logic Needs a Direction Change

If your current scoring model only moves in one direction, you're not measuring intent — you're measuring history. And history gets stale faster than anyone on your team is accounting for.

Chronexa redesigns lead lifecycle logic for revenue teams whose scoring models have drifted from signal to noise. The engagement starts with a lead score audit — we pull your current scoring rules, map the last 90 days of score movements, and show you exactly how many "hot" leads in your active pipeline have had zero meaningful engagement in the past 30, 45, and 60 days.

If that number is higher than 20%, your reps are working fiction.

Book the lead lifecycle audit here

— we'll design the decay model that makes your scoring reflect reality, and build the n8n workflow that runs it automatically every night.

About author

Ankit is the brains behind bold business roadmaps. He loves turning “half-baked” ideas into fully baked success stories (preferably with extra sprinkles). When he’s not sketching growth plans, you’ll find him trying out quirky coffee shops or quoting lines from 90s sitcoms.

Ankit Dhiman

Head of Strategy

Subscribe to our newsletter

Sign up to get the most recent blog articles in your email every week.

Sometimes the hardest part is reaching out, but once you do, we’ll make the rest easy.

Opening Hours

Mon to Sat: 9.00am - 8.30pm

Sun: Closed

2:51:26 PM

Chronexa

Sometimes the hardest part is reaching out, but once you do, we’ll make the rest easy.

Opening Hours

Mon to Sat: 9.00am - 8.30pm

Sun: Closed

2:51:26 PM

Chronexa

Sometimes the hardest part is reaching out, but once you do, we’ll make the rest easy.

Opening Hours

Mon to Sat: 9.00am - 8.30pm

Sun: Closed

2:51:26 PM

Chronexa