Effective lead scoring helps sales teams focus on the most promising prospects. This guide shows how to build intelligent lead scoring automation with n8n.
Lead Scoring Framework
Core Scoring Engine
// n8n Function Node - Lead Scoring Engine
const leadData = $input.first().json;
// Scoring configuration
const scoringModel = {
demographic: {
weight: 0.30,
factors: {
company_size: {
'enterprise': 100,
'mid_market': 80,
'small_business': 60,
'startup': 40,
'unknown': 20
},
industry: {
'technology': 100,
'finance': 95,
'healthcare': 90,
'manufacturing': 85,
'retail': 75,
'other': 50
},
job_title: {
patterns: [
{ regex: /^(c-level|ceo|cto|cfo|coo|cmo)/i, score: 100 },
{ regex: /^(vp|vice president|director)/i, score: 90 },
{ regex: /^(head|senior manager|manager)/i, score: 75 },
{ regex: /^(senior|lead|principal)/i, score: 60 },
{ regex: /.+/, score: 40 }
]
},
location: {
'tier1_markets': 100,
'tier2_markets': 80,
'tier3_markets': 60,
'other': 40
}
}
},
behavioral: {
weight: 0.40,
factors: {
page_views: {
'20+': 100,
'10-19': 80,
'5-9': 60,
'1-4': 40,
'0': 0
},
pricing_page_visits: {
'3+': 100,
'2': 80,
'1': 50,
'0': 0
},
content_downloads: {
'5+': 100,
'3-4': 80,
'1-2': 50,
'0': 0
},
demo_requests: {
'yes': 100,
'no': 0
},
email_engagement: {
'high': 100,
'medium': 60,
'low': 30,
'none': 0
}
}
},
engagement: {
weight: 0.20,
factors: {
email_opens: {
'10+': 100,
'5-9': 80,
'1-4': 50,
'0': 0
},
email_clicks: {
'5+': 100,
'2-4': 70,
'1': 40,
'0': 0
},
webinar_attendance: {
'attended_live': 100,
'watched_recording': 70,
'registered_no_show': 30,
'none': 0
},
social_engagement: {
'high': 100,
'medium': 60,
'low': 30,
'none': 0
}
}
},
intent: {
weight: 0.10,
factors: {
recency: {
'24_hours': 100,
'7_days': 80,
'30_days': 50,
'90_days': 20,
'older': 0
},
frequency: {
'daily': 100,
'weekly': 70,
'monthly': 40,
'rarely': 10
}
}
}
};
// Calculate demographic score
function calculateDemographicScore(lead) {
let score = 0;
const factors = scoringModel.demographic.factors;
// Company size
const sizeScore = factors.company_size[lead.company_size] || factors.company_size['unknown'];
score += sizeScore * 0.25;
// Industry
const industryScore = factors.industry[lead.industry] || factors.industry['other'];
score += industryScore * 0.25;
// Job title
const titlePatterns = factors.job_title.patterns;
let titleScore = 40; // default
for (const pattern of titlePatterns) {
if (pattern.regex.test(lead.job_title || '')) {
titleScore = pattern.score;
break;
}
}
score += titleScore * 0.30;
// Location
const locationTier = determineLocationTier(lead.country, lead.state);
const locationScore = factors.location[locationTier] || factors.location['other'];
score += locationScore * 0.20;
return score;
}
// Calculate behavioral score
function calculateBehavioralScore(lead) {
let score = 0;
const factors = scoringModel.behavioral.factors;
const behavior = lead.behavior || {};
// Page views
const pageViews = behavior.page_views || 0;
const pvScore = pageViews >= 20 ? 100 : pageViews >= 10 ? 80 : pageViews >= 5 ? 60 : pageViews >= 1 ? 40 : 0;
score += pvScore * 0.20;
// Pricing page visits
const pricingVisits = behavior.pricing_page_visits || 0;
const ppScore = pricingVisits >= 3 ? 100 : pricingVisits === 2 ? 80 : pricingVisits === 1 ? 50 : 0;
score += ppScore * 0.25;
// Content downloads
const downloads = behavior.content_downloads || 0;
const dlScore = downloads >= 5 ? 100 : downloads >= 3 ? 80 : downloads >= 1 ? 50 : 0;
score += dlScore * 0.20;
// Demo request
const demoScore = behavior.demo_requested ? 100 : 0;
score += demoScore * 0.25;
// Email engagement
const emailEngagement = behavior.email_engagement || 'none';
const emailScore = factors.email_engagement[emailEngagement] || 0;
score += emailScore * 0.10;
return score;
}
// Calculate engagement score
function calculateEngagementScore(lead) {
let score = 0;
const engagement = lead.engagement || {};
// Email opens
const opens = engagement.email_opens || 0;
const openScore = opens >= 10 ? 100 : opens >= 5 ? 80 : opens >= 1 ? 50 : 0;
score += openScore * 0.25;
// Email clicks
const clicks = engagement.email_clicks || 0;
const clickScore = clicks >= 5 ? 100 : clicks >= 2 ? 70 : clicks >= 1 ? 40 : 0;
score += clickScore * 0.30;
// Webinar attendance
const webinarStatus = engagement.webinar_status || 'none';
const webinarScore = scoringModel.engagement.factors.webinar_attendance[webinarStatus] || 0;
score += webinarScore * 0.25;
// Social engagement
const socialEngagement = engagement.social_engagement || 'none';
const socialScore = scoringModel.engagement.factors.social_engagement[socialEngagement] || 0;
score += socialScore * 0.20;
return score;
}
// Calculate intent score
function calculateIntentScore(lead) {
let score = 0;
const intent = lead.intent || {};
// Recency
const lastActivityDate = new Date(intent.last_activity || 0);
const daysSinceActivity = Math.floor((Date.now() - lastActivityDate) / (1000 * 60 * 60 * 24));
let recencyScore = 0;
if (daysSinceActivity <= 1) recencyScore = 100;
else if (daysSinceActivity <= 7) recencyScore = 80;
else if (daysSinceActivity <= 30) recencyScore = 50;
else if (daysSinceActivity <= 90) recencyScore = 20;
score += recencyScore * 0.50;
// Frequency
const visitFrequency = intent.visit_frequency || 'rarely';
const freqScore = scoringModel.intent.factors.frequency[visitFrequency] || 10;
score += freqScore * 0.50;
return score;
}
function determineLocationTier(country, state) {
const tier1 = ['US', 'GB', 'CA', 'AU', 'DE'];
const tier2 = ['FR', 'NL', 'SE', 'NO', 'DK', 'CH', 'JP'];
if (tier1.includes(country)) return 'tier1_markets';
if (tier2.includes(country)) return 'tier2_markets';
return 'tier3_markets';
}
// Calculate all scores
const demographicScore = calculateDemographicScore(leadData);
const behavioralScore = calculateBehavioralScore(leadData);
const engagementScore = calculateEngagementScore(leadData);
const intentScore = calculateIntentScore(leadData);
// Calculate weighted total
const totalScore = Math.round(
demographicScore * scoringModel.demographic.weight +
behavioralScore * scoringModel.behavioral.weight +
engagementScore * scoringModel.engagement.weight +
intentScore * scoringModel.intent.weight
);
// Determine lead grade
function determineGrade(score) {
if (score >= 80) return 'A';
if (score >= 60) return 'B';
if (score >= 40) return 'C';
if (score >= 20) return 'D';
return 'F';
}
// Determine MQL/SQL status
function determineQualificationStatus(score, grade, lead) {
// SQL criteria
if (score >= 75 && lead.behavior?.demo_requested) {
return 'SQL';
}
if (score >= 80) {
return 'SQL';
}
// MQL criteria
if (score >= 50 && grade <= 'B') {
return 'MQL';
}
if (score >= 40 && lead.behavior?.pricing_page_visits >= 2) {
return 'MQL';
}
return 'Lead';
}
const grade = determineGrade(totalScore);
const qualification = determineQualificationStatus(totalScore, grade, leadData);
// Build scoring result
const scoringResult = {
lead_id: leadData.id,
email: leadData.email,
scores: {
demographic: Math.round(demographicScore),
behavioral: Math.round(behavioralScore),
engagement: Math.round(engagementScore),
intent: Math.round(intentScore),
total: totalScore
},
grade: grade,
qualification_status: qualification,
score_breakdown: {
demographic_factors: {
company_size: leadData.company_size,
industry: leadData.industry,
job_title: leadData.job_title,
location: leadData.country
},
behavioral_factors: {
page_views: leadData.behavior?.page_views || 0,
pricing_visits: leadData.behavior?.pricing_page_visits || 0,
downloads: leadData.behavior?.content_downloads || 0,
demo_requested: leadData.behavior?.demo_requested || false
}
},
scored_at: new Date().toISOString(),
previous_score: leadData.previous_score || null,
score_change: leadData.previous_score ? totalScore - leadData.previous_score : null
};
return { json: scoringResult };Real-Time Score Updates
// n8n Function Node - Real-Time Score Updater
const event = $input.first().json;
const currentScores = $('Get Lead Scores').first()?.json || {};
// Event scoring adjustments
const eventScoreAdjustments = {
// Website events
'page_view': 1,
'pricing_page_view': 5,
'product_page_view': 3,
'case_study_view': 4,
'blog_post_view': 1,
// Content events
'ebook_download': 10,
'whitepaper_download': 12,
'checklist_download': 8,
'webinar_registration': 15,
'webinar_attendance': 20,
// Form submissions
'demo_request': 30,
'contact_form': 25,
'pricing_request': 28,
'trial_signup': 35,
// Email events
'email_open': 2,
'email_click': 5,
'email_reply': 15,
'unsubscribe': -20,
// Negative events
'bounce': -10,
'spam_complaint': -50,
'inactive_30_days': -15,
'inactive_60_days': -25,
'inactive_90_days': -40
};
// Calculate score adjustment
const eventType = event.event_type;
const adjustment = eventScoreAdjustments[eventType] || 0;
// Apply decay for old scores
function applyScoreDecay(currentScore, lastActivityDate) {
const daysSinceActivity = Math.floor(
(Date.now() - new Date(lastActivityDate).getTime()) / (1000 * 60 * 60 * 24)
);
// Apply 2% decay per inactive week
const weeksInactive = Math.floor(daysSinceActivity / 7);
const decayMultiplier = Math.pow(0.98, weeksInactive);
return Math.round(currentScore * decayMultiplier);
}
// Calculate new score
let newBehavioralScore = currentScores.behavioral || 0;
// Apply event adjustment
newBehavioralScore += adjustment;
// Apply decay if needed
if (event.event_type.includes('inactive')) {
newBehavioralScore = applyScoreDecay(
newBehavioralScore,
currentScores.last_activity_date
);
}
// Ensure score stays within bounds
newBehavioralScore = Math.max(0, Math.min(100, newBehavioralScore));
// Check for threshold crossings
const thresholds = {
mql: 50,
sql: 75,
hot_lead: 85
};
const previousTotal = currentScores.total || 0;
const newTotal = Math.round(
currentScores.demographic * 0.30 +
newBehavioralScore * 0.40 +
currentScores.engagement * 0.20 +
currentScores.intent * 0.10
);
const thresholdsCrossed = [];
for (const [name, threshold] of Object.entries(thresholds)) {
if (previousTotal < threshold && newTotal >= threshold) {
thresholdsCrossed.push({ threshold: name, direction: 'up' });
} else if (previousTotal >= threshold && newTotal < threshold) {
thresholdsCrossed.push({ threshold: name, direction: 'down' });
}
}
// Determine actions based on score change
const actions = [];
if (thresholdsCrossed.some(t => t.threshold === 'sql' && t.direction === 'up')) {
actions.push({
type: 'notify_sales',
priority: 'high',
message: `Lead ${event.lead_id} is now SQL (score: ${newTotal})`
});
actions.push({
type: 'create_task',
assignee: 'sales_round_robin',
task: `Follow up with SQL: ${event.lead_email}`
});
}
if (thresholdsCrossed.some(t => t.threshold === 'mql' && t.direction === 'up')) {
actions.push({
type: 'add_to_nurture',
campaign: 'mql_nurture',
message: 'Added to MQL nurture sequence'
});
}
if (thresholdsCrossed.some(t => t.threshold === 'hot_lead' && t.direction === 'up')) {
actions.push({
type: 'notify_sales',
priority: 'urgent',
message: `Hot lead alert! ${event.lead_id} score: ${newTotal}`
});
}
return {
json: {
lead_id: event.lead_id,
event: eventType,
score_adjustment: adjustment,
scores: {
previous: {
behavioral: currentScores.behavioral,
total: previousTotal
},
current: {
behavioral: newBehavioralScore,
total: newTotal
}
},
thresholds_crossed: thresholdsCrossed,
actions: actions,
updated_at: new Date().toISOString()
}
};Lead Routing
// n8n Function Node - Lead Router
const scoredLead = $input.first().json;
const salesTeam = $('Get Sales Team').all().map(i => i.json);
// Routing rules configuration
const routingRules = {
enterprise: {
condition: (lead) => lead.company_size === 'enterprise' || lead.scores.total >= 85,
team: 'enterprise_sales',
sla_minutes: 15
},
mid_market: {
condition: (lead) => lead.company_size === 'mid_market' || lead.scores.total >= 70,
team: 'mid_market_sales',
sla_minutes: 30
},
smb: {
condition: (lead) => true, // Default
team: 'smb_sales',
sla_minutes: 60
}
};
// Industry-specific routing
const industryRouting = {
'healthcare': 'healthcare_specialist',
'finance': 'financial_services_specialist',
'technology': 'tech_specialist'
};
// Geographic routing
const geoRouting = {
'US': { 'CA': 'west_coast_team', 'NY': 'east_coast_team' },
'GB': 'emea_team',
'DE': 'emea_team',
'JP': 'apac_team',
'AU': 'apac_team'
};
function determineRoute(lead) {
// Determine segment
let segment = 'smb';
for (const [seg, rule] of Object.entries(routingRules)) {
if (rule.condition(lead)) {
segment = seg;
break;
}
}
// Check for specialist routing
let specialist = null;
if (industryRouting[lead.industry]) {
specialist = industryRouting[lead.industry];
}
// Check geographic routing
let geoTeam = null;
if (geoRouting[lead.country]) {
if (typeof geoRouting[lead.country] === 'object') {
geoTeam = geoRouting[lead.country][lead.state] || null;
} else {
geoTeam = geoRouting[lead.country];
}
}
return {
segment,
team: routingRules[segment].team,
specialist,
geo_team: geoTeam,
sla_minutes: routingRules[segment].sla_minutes
};
}
function selectSalesRep(team, availableReps) {
// Filter by team and availability
const teamReps = availableReps.filter(rep =>
rep.team === team && rep.is_available && rep.current_leads < rep.max_leads
);
if (teamReps.length === 0) {
// Fall back to any available rep
const anyAvailable = availableReps.filter(rep =>
rep.is_available && rep.current_leads < rep.max_leads
);
if (anyAvailable.length === 0) return null;
return anyAvailable.sort((a, b) => a.current_leads - b.current_leads)[0];
}
// Round-robin with load balancing
return teamReps.sort((a, b) => {
// Prioritize by current load
const loadDiff = a.current_leads - b.current_leads;
if (loadDiff !== 0) return loadDiff;
// Then by last assignment time
return new Date(a.last_assigned) - new Date(b.last_assigned);
})[0];
}
const route = determineRoute(scoredLead);
const assignedRep = selectSalesRep(route.team, salesTeam);
const routingResult = {
lead_id: scoredLead.lead_id,
routing: {
segment: route.segment,
team: route.team,
specialist_required: route.specialist,
geographic_team: route.geo_team,
sla_minutes: route.sla_minutes,
sla_deadline: new Date(Date.now() + route.sla_minutes * 60 * 1000).toISOString()
},
assignment: assignedRep ? {
rep_id: assignedRep.id,
rep_name: assignedRep.name,
rep_email: assignedRep.email,
assigned_at: new Date().toISOString()
} : null,
fallback_required: !assignedRep,
lead_score: scoredLead.scores.total,
lead_grade: scoredLead.grade,
qualification: scoredLead.qualification_status
};
return { json: routingResult };CRM Integration
// n8n Function Node - CRM Sync
const routingResult = $input.first().json;
const scoredLead = $('Lead Scoring').first().json;
// Build CRM update payload
function buildCRMPayload(lead, routing) {
return {
// Contact fields
contact: {
id: lead.lead_id,
email: lead.email,
custom_fields: {
lead_score: lead.scores.total,
lead_grade: lead.grade,
demographic_score: lead.scores.demographic,
behavioral_score: lead.scores.behavioral,
engagement_score: lead.scores.engagement,
intent_score: lead.scores.intent,
last_scored_at: new Date().toISOString()
}
},
// Lead stage update
lifecycle: {
stage: mapQualificationToStage(lead.qualification_status),
status: 'active',
score: lead.scores.total
},
// Owner assignment
assignment: routing.assignment ? {
owner_id: routing.assignment.rep_id,
assigned_at: routing.assignment.assigned_at,
assignment_reason: `Auto-routed: ${routing.routing.segment} segment`
} : null,
// Task creation
task: routing.assignment ? {
title: `Follow up with ${lead.qualification_status}: ${lead.email}`,
description: buildTaskDescription(lead, routing),
due_date: routing.routing.sla_deadline,
priority: lead.scores.total >= 80 ? 'high' : 'normal',
assignee_id: routing.assignment.rep_id,
type: 'follow_up'
} : null,
// Activity log
activity: {
type: 'score_update',
timestamp: new Date().toISOString(),
details: {
previous_score: lead.previous_score,
new_score: lead.scores.total,
score_change: lead.score_change,
triggered_by: 'automated_scoring'
}
}
};
}
function mapQualificationToStage(qualification) {
const mapping = {
'Lead': 'new_lead',
'MQL': 'marketing_qualified',
'SQL': 'sales_qualified',
'Opportunity': 'opportunity'
};
return mapping[qualification] || 'new_lead';
}
function buildTaskDescription(lead, routing) {
return `
## Lead Overview
- **Score**: ${lead.scores.total} (Grade: ${lead.grade})
- **Status**: ${lead.qualification_status}
- **Segment**: ${routing.routing.segment}
## Score Breakdown
- Demographic: ${lead.scores.demographic}/100
- Behavioral: ${lead.scores.behavioral}/100
- Engagement: ${lead.scores.engagement}/100
- Intent: ${lead.scores.intent}/100
## Key Signals
${lead.score_breakdown.behavioral_factors.demo_requested ? '- ✅ Requested demo\n' : ''}
${lead.score_breakdown.behavioral_factors.pricing_visits > 0 ? `- 👀 Visited pricing page ${lead.score_breakdown.behavioral_factors.pricing_visits} times\n` : ''}
${lead.score_breakdown.behavioral_factors.downloads > 0 ? `- 📥 Downloaded ${lead.score_breakdown.behavioral_factors.downloads} resources\n` : ''}
## SLA
- **Deadline**: ${routing.routing.sla_deadline}
- **Time allowed**: ${routing.routing.sla_minutes} minutes
`.trim();
}
const crmPayload = buildCRMPayload(scoredLead, routingResult);
// Build notification for sales rep
const notification = routingResult.assignment ? {
type: 'new_lead_assignment',
recipient: routingResult.assignment.rep_email,
subject: `New ${scoredLead.qualification_status} Assigned: ${scoredLead.email}`,
body: {
lead_email: scoredLead.email,
score: scoredLead.scores.total,
grade: scoredLead.grade,
sla_deadline: routingResult.routing.sla_deadline,
quick_view_url: `${process.env.CRM_URL}/leads/${scoredLead.lead_id}`
},
channel: scoredLead.scores.total >= 80 ? 'slack_dm' : 'email'
} : null;
return {
json: {
crm_payload: crmPayload,
notification: notification,
metadata: {
lead_id: scoredLead.lead_id,
processed_at: new Date().toISOString()
}
}
};Analytics and Reporting
// n8n Function Node - Scoring Analytics
const allLeads = $input.all().map(i => i.json);
// Calculate scoring metrics
const metrics = {
total_leads: allLeads.length,
by_grade: { A: 0, B: 0, C: 0, D: 0, F: 0 },
by_qualification: { Lead: 0, MQL: 0, SQL: 0 },
score_distribution: [],
conversion_rates: {},
average_scores: {
demographic: 0,
behavioral: 0,
engagement: 0,
intent: 0,
total: 0
}
};
// Aggregate metrics
for (const lead of allLeads) {
metrics.by_grade[lead.grade] = (metrics.by_grade[lead.grade] || 0) + 1;
metrics.by_qualification[lead.qualification_status] = (metrics.by_qualification[lead.qualification_status] || 0) + 1;
metrics.average_scores.demographic += lead.scores.demographic;
metrics.average_scores.behavioral += lead.scores.behavioral;
metrics.average_scores.engagement += lead.scores.engagement;
metrics.average_scores.intent += lead.scores.intent;
metrics.average_scores.total += lead.scores.total;
}
// Calculate averages
const count = allLeads.length || 1;
for (const key in metrics.average_scores) {
metrics.average_scores[key] = Math.round(metrics.average_scores[key] / count);
}
// Score distribution buckets
const buckets = [
{ min: 0, max: 20, label: '0-20' },
{ min: 21, max: 40, label: '21-40' },
{ min: 41, max: 60, label: '41-60' },
{ min: 61, max: 80, label: '61-80' },
{ min: 81, max: 100, label: '81-100' }
];
for (const bucket of buckets) {
const count = allLeads.filter(l =>
l.scores.total >= bucket.min && l.scores.total <= bucket.max
).length;
metrics.score_distribution.push({
range: bucket.label,
count,
percentage: Math.round((count / allLeads.length) * 100)
});
}
// Calculate conversion rates (would need historical data)
metrics.conversion_rates = {
mql_to_sql: 0.35, // Placeholder
sql_to_opportunity: 0.45,
opportunity_to_won: 0.25
};
// Build report
const report = {
generated_at: new Date().toISOString(),
period: 'last_30_days',
metrics,
insights: generateInsights(metrics),
recommendations: generateRecommendations(metrics)
};
function generateInsights(metrics) {
const insights = [];
const aGradePercent = (metrics.by_grade.A / metrics.total_leads) * 100;
if (aGradePercent < 10) {
insights.push({
type: 'warning',
message: `Only ${aGradePercent.toFixed(1)}% of leads are A-grade. Consider reviewing acquisition channels.`
});
}
const avgScore = metrics.average_scores.total;
if (avgScore < 40) {
insights.push({
type: 'info',
message: `Average lead score is ${avgScore}. Focus on lead quality improvement.`
});
}
return insights;
}
function generateRecommendations(metrics) {
const recommendations = [];
if (metrics.average_scores.behavioral < 40) {
recommendations.push({
area: 'engagement',
recommendation: 'Implement more interactive content to boost behavioral scores'
});
}
if (metrics.by_qualification.MQL > metrics.by_qualification.SQL * 3) {
recommendations.push({
area: 'conversion',
recommendation: 'Review MQL to SQL conversion - consider adjusting thresholds or nurture campaigns'
});
}
return recommendations;
}
return { json: report };Best Practices
Lead Scoring Design
- Start simple: Begin with basic scoring and refine over time
- Validate with sales: Ensure scores correlate with actual conversions
- Regular calibration: Adjust weights based on performance data
- Negative scoring: Don't forget to penalize disengagement
Implementation Tips
- Update scores in real-time for timely action
- Implement score decay for inactive leads
- Create clear handoff processes at threshold crossings
- Monitor scoring model accuracy regularly
Automated lead scoring with n8n ensures consistent qualification while enabling sales teams to focus on the highest-potential opportunities.
Is your AI system compliant with the EU AI Act? Free risk assessment - find out in 2 minutes →