n8n Lead Scoring Automation: Building Intelligent Sales Qualification Workflows
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.