Automatizarea Lead Scoring cu n8n: Construirea de Workflow-uri Inteligente pentru Calificarea Vanzarilor
Un sistem eficient de lead scoring ajuta echipele de vanzari sa se concentreze pe prospectii cu cel mai mare potential. Acest ghid iti arata cum sa construiesti automatizari inteligente de lead scoring cu n8n.
Framework-ul de Lead Scoring
Motorul Principal de Scorare
// 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 };Actualizari de Scor in Timp Real
// 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()
}
};Rutarea Lead-urilor
// 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 };Integrarea cu CRM
// 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()
}
}
};Analiza si Raportare
// 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 };Bune Practici
Designul Lead Scoring-ului
- Incepe simplu: Porneste cu un scoring de baza si rafineaza pe parcurs
- Valideaza cu echipa de vanzari: Asigura-te ca scorurile se coreleaza cu conversiile reale
- Calibrare regulata: Ajusteaza ponderile pe baza datelor de performanta
- Scorare negativa: Nu uita sa penalizezi lipsa de engagement
Sfaturi de Implementare
- Actualizeaza scorurile in timp real pentru actiuni prompte
- Implementeaza score decay pentru lead-urile inactive
- Creeaza procese clare de predare la depasirea pragurilor
- Monitorizeaza regulat acuratetea modelului de scoring
Automatizarea lead scoring-ului cu n8n asigura calificare consistenta, permitand in acelasi timp echipelor de vanzari sa se concentreze pe oportunitatile cu cel mai mare potential.