Automatizare CRM cu n8n: Pattern-uri de integrare HubSpot si Salesforce
Automatizarea CRM este esentiala pentru scalarea operatiunilor de vanzari si mentinerea consistentei datelor intre sisteme. Acest ghid acopera pattern-uri complete de workflow n8n pentru integrarea HubSpot si Salesforce, inclusiv managementul lead-urilor, sincronizarea contactelor si urmarirea deal-urilor.
Arhitectura integrarii CRM
Strategie de sincronizare multi-CRM
// crm-sync-orchestrator.js
/**
* CRM synchronization orchestrator for n8n workflows.
* Handles bi-directional sync between HubSpot and Salesforce.
*/
class CRMSyncOrchestrator {
constructor(config) {
this.hubspotApiKey = config.hubspotApiKey;
this.salesforceConfig = config.salesforce;
this.syncRules = config.syncRules || {};
this.conflictResolution = config.conflictResolution || 'newest_wins';
}
/**
* Determine sync direction based on record metadata
*/
determineSyncDirection(hubspotRecord, salesforceRecord) {
const hubspotModified = new Date(hubspotRecord?.properties?.hs_lastmodifieddate);
const salesforceModified = new Date(salesforceRecord?.LastModifiedDate);
if (!salesforceRecord) return 'hubspot_to_salesforce';
if (!hubspotRecord) return 'salesforce_to_hubspot';
switch (this.conflictResolution) {
case 'newest_wins':
return hubspotModified > salesforceModified
? 'hubspot_to_salesforce'
: 'salesforce_to_hubspot';
case 'hubspot_primary':
return 'hubspot_to_salesforce';
case 'salesforce_primary':
return 'salesforce_to_hubspot';
case 'merge':
return 'bidirectional_merge';
default:
return 'newest_wins';
}
}
/**
* Map HubSpot contact to Salesforce Lead/Contact
*/
mapHubSpotToSalesforce(hubspotContact) {
const props = hubspotContact.properties;
return {
// Standard fields
FirstName: props.firstname || '',
LastName: props.lastname || 'Unknown',
Email: props.email,
Phone: props.phone,
Company: props.company,
Title: props.jobtitle,
Website: props.website,
// Address fields
Street: props.address,
City: props.city,
State: props.state,
PostalCode: props.zip,
Country: props.country,
// Custom fields mapping
HubSpot_Contact_ID__c: hubspotContact.id,
Lead_Source__c: this.mapLeadSource(props.hs_analytics_source),
Lead_Score__c: parseInt(props.hubspotscore) || 0,
// Lifecycle stage mapping
Status: this.mapLifecycleToStatus(props.lifecyclestage),
// Activity data
Description: this.buildDescription(props)
};
}
/**
* Map Salesforce Lead/Contact to HubSpot contact
*/
mapSalesforceToHubSpot(salesforceRecord) {
return {
properties: {
firstname: salesforceRecord.FirstName,
lastname: salesforceRecord.LastName,
email: salesforceRecord.Email,
phone: salesforceRecord.Phone,
company: salesforceRecord.Company,
jobtitle: salesforceRecord.Title,
website: salesforceRecord.Website,
address: salesforceRecord.Street,
city: salesforceRecord.City,
state: salesforceRecord.State,
zip: salesforceRecord.PostalCode,
country: salesforceRecord.Country,
// Custom properties
salesforce_lead_id: salesforceRecord.Id,
salesforce_lead_status: salesforceRecord.Status,
lifecyclestage: this.mapStatusToLifecycle(salesforceRecord.Status)
}
};
}
mapLeadSource(hsSource) {
const sourceMap = {
'ORGANIC_SEARCH': 'Web',
'PAID_SEARCH': 'Paid Search',
'SOCIAL_MEDIA': 'Social Media',
'EMAIL_MARKETING': 'Email',
'REFERRALS': 'Referral',
'DIRECT_TRAFFIC': 'Web',
'OTHER_CAMPAIGNS': 'Other'
};
return sourceMap[hsSource] || 'Other';
}
mapLifecycleToStatus(lifecycle) {
const statusMap = {
'subscriber': 'Open - Not Contacted',
'lead': 'Open - Not Contacted',
'marketingqualifiedlead': 'Working - Contacted',
'salesqualifiedlead': 'Working - Contacted',
'opportunity': 'Qualified',
'customer': 'Converted',
'evangelist': 'Converted'
};
return statusMap[lifecycle] || 'Open - Not Contacted';
}
mapStatusToLifecycle(status) {
const lifecycleMap = {
'Open - Not Contacted': 'lead',
'Working - Contacted': 'marketingqualifiedlead',
'Qualified': 'salesqualifiedlead',
'Converted': 'customer',
'Unqualified': 'other'
};
return lifecycleMap[status] || 'lead';
}
buildDescription(props) {
const parts = [];
if (props.hs_analytics_first_url) {
parts.push(`First page: ${props.hs_analytics_first_url}`);
}
if (props.hs_analytics_num_page_views) {
parts.push(`Page views: ${props.hs_analytics_num_page_views}`);
}
if (props.recent_conversion_event_name) {
parts.push(`Last conversion: ${props.recent_conversion_event_name}`);
}
return parts.join('\n');
}
}
module.exports = { CRMSyncOrchestrator };Workflow-uri de management al lead-urilor
Procesarea lead-urilor noi
{
"name": "CRM Lead Processing Pipeline",
"nodes": [
{
"name": "HubSpot Webhook Trigger",
"type": "n8n-nodes-base.hubspotTrigger",
"parameters": {
"eventsUi": {
"eventValues": [
{
"name": "contact.creation"
},
{
"name": "contact.propertyChange",
"property": "lifecyclestage"
}
]
}
},
"position": [250, 300]
},
{
"name": "Get Full Contact",
"type": "n8n-nodes-base.hubspot",
"parameters": {
"resource": "contact",
"operation": "get",
"contactId": "={{ $json.objectId }}",
"additionalFields": {
"properties": [
"email",
"firstname",
"lastname",
"company",
"phone",
"jobtitle",
"lifecyclestage",
"hubspotscore",
"hs_analytics_source",
"hs_analytics_first_url",
"recent_conversion_event_name"
]
}
},
"position": [450, 300]
},
{
"name": "Enrich Lead Data",
"type": "n8n-nodes-base.function",
"parameters": {
"functionCode": "// Enrich lead with additional data\nconst contact = $input.first().json;\nconst email = contact.properties.email;\n\n// Extract domain for company enrichment\nconst domain = email ? email.split('@')[1] : null;\n\n// Calculate lead quality score\nlet qualityScore = 0;\n\n// Has company email (not gmail, yahoo, etc)\nconst freeEmailDomains = ['gmail.com', 'yahoo.com', 'hotmail.com', 'outlook.com'];\nif (domain && !freeEmailDomains.includes(domain)) {\n qualityScore += 30;\n}\n\n// Has phone number\nif (contact.properties.phone) {\n qualityScore += 20;\n}\n\n// Has company name\nif (contact.properties.company) {\n qualityScore += 15;\n}\n\n// Has job title\nif (contact.properties.jobtitle) {\n qualityScore += 15;\n}\n\n// HubSpot score contribution\nconst hsScore = parseInt(contact.properties.hubspotscore) || 0;\nqualityScore += Math.min(hsScore / 5, 20);\n\nreturn {\n ...contact,\n enrichment: {\n domain,\n qualityScore,\n tier: qualityScore >= 70 ? 'A' : qualityScore >= 50 ? 'B' : qualityScore >= 30 ? 'C' : 'D',\n needsEnrichment: !contact.properties.company || !contact.properties.jobtitle\n }\n};"
},
"position": [650, 300]
},
{
"name": "Route by Lead Quality",
"type": "n8n-nodes-base.switch",
"parameters": {
"dataType": "string",
"value1": "={{ $json.enrichment.tier }}",
"rules": {
"rules": [
{
"value2": "A",
"output": 0
},
{
"value2": "B",
"output": 1
}
]
},
"fallbackOutput": 2
},
"position": [850, 300]
},
{
"name": "High Priority Lead",
"type": "n8n-nodes-base.function",
"parameters": {
"functionCode": "// Prepare high-priority lead for immediate sales follow-up\nconst lead = $input.first().json;\n\nreturn {\n lead,\n assignment: {\n queue: 'enterprise_sales',\n priority: 'high',\n sla_response_hours: 4,\n actions: ['immediate_notification', 'sync_to_salesforce', 'schedule_task']\n }\n};"
},
"position": [1050, 200]
},
{
"name": "Medium Priority Lead",
"type": "n8n-nodes-base.function",
"parameters": {
"functionCode": "// Prepare medium-priority lead\nconst lead = $input.first().json;\n\nreturn {\n lead,\n assignment: {\n queue: 'sales_development',\n priority: 'medium',\n sla_response_hours: 24,\n actions: ['sync_to_salesforce', 'add_to_nurture']\n }\n};"
},
"position": [1050, 300]
},
{
"name": "Low Priority Lead",
"type": "n8n-nodes-base.function",
"parameters": {
"functionCode": "// Prepare low-priority lead for nurture\nconst lead = $input.first().json;\n\nreturn {\n lead,\n assignment: {\n queue: 'marketing_nurture',\n priority: 'low',\n sla_response_hours: 72,\n actions: ['add_to_nurture', 'request_enrichment']\n }\n};"
},
"position": [1050, 400]
},
{
"name": "Merge Results",
"type": "n8n-nodes-base.merge",
"parameters": {
"mode": "append"
},
"position": [1250, 300]
},
{
"name": "Create Salesforce Lead",
"type": "n8n-nodes-base.salesforce",
"parameters": {
"resource": "lead",
"operation": "create",
"additionalFields": {
"firstName": "={{ $json.lead.properties.firstname }}",
"lastName": "={{ $json.lead.properties.lastname || 'Unknown' }}",
"email": "={{ $json.lead.properties.email }}",
"phone": "={{ $json.lead.properties.phone }}",
"company": "={{ $json.lead.properties.company || $json.lead.enrichment.domain || 'Unknown' }}",
"title": "={{ $json.lead.properties.jobtitle }}",
"leadSource": "HubSpot",
"status": "={{ $json.assignment.priority === 'high' ? 'Working - Contacted' : 'Open - Not Contacted' }}",
"customFields": {
"HubSpot_Contact_ID__c": "={{ $json.lead.id }}",
"Lead_Quality_Score__c": "={{ $json.lead.enrichment.qualityScore }}",
"Lead_Tier__c": "={{ $json.lead.enrichment.tier }}"
}
}
},
"position": [1450, 300]
},
{
"name": "Update HubSpot with SF ID",
"type": "n8n-nodes-base.hubspot",
"parameters": {
"resource": "contact",
"operation": "update",
"contactId": "={{ $json.lead.id }}",
"updateFields": {
"customProperties": {
"customProperty": [
{
"name": "salesforce_lead_id",
"value": "={{ $('Create Salesforce Lead').item.json.id }}"
},
{
"name": "salesforce_sync_status",
"value": "synced"
},
{
"name": "salesforce_sync_date",
"value": "={{ new Date().toISOString() }}"
}
]
}
}
},
"position": [1650, 300]
},
{
"name": "Notify Sales (High Priority)",
"type": "n8n-nodes-base.slack",
"parameters": {
"channel": "#sales-hot-leads",
"text": "",
"attachments": [],
"blocksUi": {
"blocksValues": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": "🔥 New High-Priority Lead"
}
},
{
"type": "section",
"fields": [
{
"type": "mrkdwn",
"text": "*Name:*\n={{ $json.lead.properties.firstname }} {{ $json.lead.properties.lastname }}"
},
{
"type": "mrkdwn",
"text": "*Company:*\n={{ $json.lead.properties.company || 'Not provided' }}"
},
{
"type": "mrkdwn",
"text": "*Email:*\n={{ $json.lead.properties.email }}"
},
{
"type": "mrkdwn",
"text": "*Quality Score:*\n={{ $json.lead.enrichment.qualityScore }}/100 (Tier {{ $json.lead.enrichment.tier }})"
}
]
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {
"type": "plain_text",
"text": "View in HubSpot"
},
"url": "https://app.hubspot.com/contacts/{{ $env.HUBSPOT_PORTAL_ID }}/contact/{{ $json.lead.id }}"
},
{
"type": "button",
"text": {
"type": "plain_text",
"text": "View in Salesforce"
},
"url": "https://{{ $env.SALESFORCE_INSTANCE }}.salesforce.com/{{ $('Create Salesforce Lead').item.json.id }}"
}
]
}
]
}
},
"position": [1850, 200],
"conditions": {
"string": [
{
"value1": "={{ $json.assignment.priority }}",
"value2": "high"
}
]
}
}
],
"connections": {
"HubSpot Webhook Trigger": {
"main": [[{"node": "Get Full Contact", "type": "main", "index": 0}]]
},
"Get Full Contact": {
"main": [[{"node": "Enrich Lead Data", "type": "main", "index": 0}]]
},
"Enrich Lead Data": {
"main": [[{"node": "Route by Lead Quality", "type": "main", "index": 0}]]
},
"Route by Lead Quality": {
"main": [
[{"node": "High Priority Lead", "type": "main", "index": 0}],
[{"node": "Medium Priority Lead", "type": "main", "index": 0}],
[{"node": "Low Priority Lead", "type": "main", "index": 0}]
]
},
"High Priority Lead": {
"main": [[{"node": "Merge Results", "type": "main", "index": 0}]]
},
"Medium Priority Lead": {
"main": [[{"node": "Merge Results", "type": "main", "index": 0}]]
},
"Low Priority Lead": {
"main": [[{"node": "Merge Results", "type": "main", "index": 0}]]
},
"Merge Results": {
"main": [[{"node": "Create Salesforce Lead", "type": "main", "index": 0}]]
},
"Create Salesforce Lead": {
"main": [[{"node": "Update HubSpot with SF ID", "type": "main", "index": 0}]]
},
"Update HubSpot with SF ID": {
"main": [[{"node": "Notify Sales (High Priority)", "type": "main", "index": 0}]]
}
}
}Sincronizarea Deal/Opportunity
// deal-opportunity-sync.js
/**
* Synchronize deals between HubSpot and Salesforce Opportunities.
*/
const DEAL_STAGE_MAPPING = {
// HubSpot to Salesforce
hubspotToSalesforce: {
'appointmentscheduled': 'Qualification',
'qualifiedtobuy': 'Needs Analysis',
'presentationscheduled': 'Proposal/Price Quote',
'decisionmakerboughtin': 'Negotiation/Review',
'contractsent': 'Negotiation/Review',
'closedwon': 'Closed Won',
'closedlost': 'Closed Lost'
},
// Salesforce to HubSpot
salesforceToHubspot: {
'Prospecting': 'appointmentscheduled',
'Qualification': 'qualifiedtobuy',
'Needs Analysis': 'qualifiedtobuy',
'Value Proposition': 'presentationscheduled',
'Id. Decision Makers': 'decisionmakerboughtin',
'Perception Analysis': 'decisionmakerboughtin',
'Proposal/Price Quote': 'contractsent',
'Negotiation/Review': 'contractsent',
'Closed Won': 'closedwon',
'Closed Lost': 'closedlost'
}
};
// n8n Function Node: Map HubSpot Deal to Salesforce Opportunity
function mapDealToOpportunity(deal) {
const properties = deal.properties;
return {
Name: properties.dealname,
Amount: parseFloat(properties.amount) || 0,
CloseDate: properties.closedate
? new Date(properties.closedate).toISOString().split('T')[0]
: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
StageName: DEAL_STAGE_MAPPING.hubspotToSalesforce[properties.dealstage] || 'Qualification',
Description: properties.description,
Probability: calculateProbability(properties.dealstage),
Type: 'New Business',
// Custom fields
HubSpot_Deal_ID__c: deal.id,
Deal_Source__c: properties.hs_analytics_source || 'Direct',
Pipeline__c: properties.pipeline
};
}
function calculateProbability(stage) {
const probabilities = {
'appointmentscheduled': 20,
'qualifiedtobuy': 40,
'presentationscheduled': 60,
'decisionmakerboughtin': 80,
'contractsent': 90,
'closedwon': 100,
'closedlost': 0
};
return probabilities[stage] || 10;
}
// n8n Function Node: Handle Deal Stage Changes
function handleDealStageChange(items) {
return items.map(item => {
const deal = item.json;
const previousStage = deal.properties.hs_deal_stage_history
? JSON.parse(deal.properties.hs_deal_stage_history).slice(-2, -1)[0]
: null;
const stageChange = {
dealId: deal.id,
dealName: deal.properties.dealname,
previousStage: previousStage?.value,
currentStage: deal.properties.dealstage,
amount: deal.properties.amount,
timestamp: new Date().toISOString()
};
// Determine required actions
const actions = [];
if (deal.properties.dealstage === 'closedwon') {
actions.push('create_customer_record');
actions.push('notify_success_team');
actions.push('trigger_onboarding');
} else if (deal.properties.dealstage === 'closedlost') {
actions.push('record_loss_reason');
actions.push('add_to_win_back_campaign');
} else if (deal.properties.dealstage === 'contractsent') {
actions.push('create_contract_task');
actions.push('notify_legal');
}
return {
json: {
...deal,
stageChange,
actions
}
};
});
}
module.exports = {
DEAL_STAGE_MAPPING,
mapDealToOpportunity,
handleDealStageChange
};Sincronizare bidirectionala a contactelor
{
"name": "Bi-Directional Contact Sync",
"nodes": [
{
"name": "Schedule Trigger",
"type": "n8n-nodes-base.scheduleTrigger",
"parameters": {
"rule": {
"interval": [
{
"field": "minutes",
"minutesInterval": 15
}
]
}
},
"position": [250, 300]
},
{
"name": "Get Recent HubSpot Changes",
"type": "n8n-nodes-base.hubspot",
"parameters": {
"resource": "contact",
"operation": "getRecentlyCreatedUpdated",
"returnAll": false,
"limit": 100,
"additionalFields": {
"properties": [
"email", "firstname", "lastname", "company", "phone",
"jobtitle", "lifecyclestage", "salesforce_contact_id",
"hs_lastmodifieddate"
]
}
},
"position": [450, 200]
},
{
"name": "Get Recent Salesforce Changes",
"type": "n8n-nodes-base.salesforce",
"parameters": {
"resource": "contact",
"operation": "getAll",
"returnAll": false,
"limit": 100,
"options": {
"conditionType": "simple",
"conditions": {
"conditions": [
{
"field": "LastModifiedDate",
"operation": ">=",
"value": "={{ new Date(Date.now() - 15 * 60 * 1000).toISOString() }}"
}
]
}
}
},
"position": [450, 400]
},
{
"name": "Process HubSpot Changes",
"type": "n8n-nodes-base.function",
"parameters": {
"functionCode": "// Filter contacts that need sync to Salesforce\nconst contacts = $input.all();\nconst toSync = [];\n\nfor (const item of contacts) {\n const contact = item.json;\n const props = contact.properties;\n\n // Skip if recently synced from Salesforce\n if (props.salesforce_sync_direction === 'from_salesforce') {\n const syncTime = new Date(props.salesforce_sync_date);\n const modTime = new Date(props.hs_lastmodifieddate);\n if (modTime <= syncTime) continue;\n }\n\n // Skip contacts without email\n if (!props.email) continue;\n\n toSync.push({\n source: 'hubspot',\n hubspotId: contact.id,\n salesforceId: props.salesforce_contact_id,\n email: props.email,\n data: {\n FirstName: props.firstname,\n LastName: props.lastname || 'Unknown',\n Email: props.email,\n Phone: props.phone,\n Title: props.jobtitle,\n MailingCity: props.city,\n MailingState: props.state,\n MailingCountry: props.country,\n HubSpot_Contact_ID__c: contact.id,\n HubSpot_Lifecycle_Stage__c: props.lifecyclestage\n },\n lastModified: props.hs_lastmodifieddate\n });\n}\n\nreturn toSync.map(s => ({ json: s }));"
},
"position": [650, 200]
},
{
"name": "Process Salesforce Changes",
"type": "n8n-nodes-base.function",
"parameters": {
"functionCode": "// Filter contacts that need sync to HubSpot\nconst contacts = $input.all();\nconst toSync = [];\n\nfor (const item of contacts) {\n const contact = item.json;\n\n // Skip if recently synced from HubSpot\n if (contact.HubSpot_Sync_Direction__c === 'from_hubspot') {\n const syncTime = new Date(contact.HubSpot_Sync_Date__c);\n const modTime = new Date(contact.LastModifiedDate);\n if (modTime <= syncTime) continue;\n }\n\n // Skip contacts without email\n if (!contact.Email) continue;\n\n toSync.push({\n source: 'salesforce',\n salesforceId: contact.Id,\n hubspotId: contact.HubSpot_Contact_ID__c,\n email: contact.Email,\n data: {\n properties: {\n firstname: contact.FirstName,\n lastname: contact.LastName,\n email: contact.Email,\n phone: contact.Phone,\n jobtitle: contact.Title,\n city: contact.MailingCity,\n state: contact.MailingState,\n country: contact.MailingCountry,\n salesforce_contact_id: contact.Id,\n salesforce_account_id: contact.AccountId\n }\n },\n lastModified: contact.LastModifiedDate\n });\n}\n\nreturn toSync.map(s => ({ json: s }));"
},
"position": [650, 400]
},
{
"name": "Merge Changes",
"type": "n8n-nodes-base.merge",
"parameters": {
"mode": "append"
},
"position": [850, 300]
},
{
"name": "Deduplicate by Email",
"type": "n8n-nodes-base.function",
"parameters": {
"functionCode": "// Deduplicate and handle conflicts\nconst items = $input.all();\nconst byEmail = new Map();\n\nfor (const item of items) {\n const record = item.json;\n const email = record.email.toLowerCase();\n\n if (!byEmail.has(email)) {\n byEmail.set(email, record);\n } else {\n // Conflict - use newest\n const existing = byEmail.get(email);\n const existingTime = new Date(existing.lastModified);\n const newTime = new Date(record.lastModified);\n\n if (newTime > existingTime) {\n byEmail.set(email, record);\n }\n }\n}\n\nreturn Array.from(byEmail.values()).map(r => ({ json: r }));"
},
"position": [1050, 300]
},
{
"name": "Route by Source",
"type": "n8n-nodes-base.switch",
"parameters": {
"dataType": "string",
"value1": "={{ $json.source }}",
"rules": {
"rules": [
{
"value2": "hubspot",
"output": 0
},
{
"value2": "salesforce",
"output": 1
}
]
}
},
"position": [1250, 300]
},
{
"name": "Upsert to Salesforce",
"type": "n8n-nodes-base.salesforce",
"parameters": {
"resource": "contact",
"operation": "upsert",
"externalId": "Email",
"additionalFields": "={{ $json.data }}"
},
"position": [1450, 200]
},
{
"name": "Update HubSpot Sync Status",
"type": "n8n-nodes-base.hubspot",
"parameters": {
"resource": "contact",
"operation": "update",
"contactId": "={{ $json.hubspotId }}",
"updateFields": {
"customProperties": {
"customProperty": [
{
"name": "salesforce_sync_status",
"value": "synced"
},
{
"name": "salesforce_sync_date",
"value": "={{ new Date().toISOString() }}"
},
{
"name": "salesforce_sync_direction",
"value": "to_salesforce"
}
]
}
}
},
"position": [1650, 200]
},
{
"name": "Upsert to HubSpot",
"type": "n8n-nodes-base.hubspot",
"parameters": {
"resource": "contact",
"operation": "upsert",
"email": "={{ $json.email }}",
"additionalFields": "={{ $json.data.properties }}"
},
"position": [1450, 400]
},
{
"name": "Update Salesforce Sync Status",
"type": "n8n-nodes-base.salesforce",
"parameters": {
"resource": "contact",
"operation": "update",
"recordId": "={{ $json.salesforceId }}",
"updateFields": {
"HubSpot_Sync_Status__c": "synced",
"HubSpot_Sync_Date__c": "={{ new Date().toISOString() }}",
"HubSpot_Sync_Direction__c": "to_hubspot",
"HubSpot_Contact_ID__c": "={{ $('Upsert to HubSpot').item.json.id }}"
}
},
"position": [1650, 400]
},
{
"name": "Log Sync Results",
"type": "n8n-nodes-base.function",
"parameters": {
"functionCode": "// Aggregate sync statistics\nconst hubspotResults = $('Update HubSpot Sync Status').all();\nconst salesforceResults = $('Update Salesforce Sync Status').all();\n\nconst stats = {\n timestamp: new Date().toISOString(),\n hubspotToSalesforce: hubspotResults.length,\n salesforceToHubspot: salesforceResults.length,\n totalSynced: hubspotResults.length + salesforceResults.length,\n errors: 0\n};\n\nreturn [{ json: stats }];"
},
"position": [1850, 300]
}
],
"connections": {
"Schedule Trigger": {
"main": [
[
{"node": "Get Recent HubSpot Changes", "type": "main", "index": 0},
{"node": "Get Recent Salesforce Changes", "type": "main", "index": 0}
]
]
},
"Get Recent HubSpot Changes": {
"main": [[{"node": "Process HubSpot Changes", "type": "main", "index": 0}]]
},
"Get Recent Salesforce Changes": {
"main": [[{"node": "Process Salesforce Changes", "type": "main", "index": 0}]]
},
"Process HubSpot Changes": {
"main": [[{"node": "Merge Changes", "type": "main", "index": 0}]]
},
"Process Salesforce Changes": {
"main": [[{"node": "Merge Changes", "type": "main", "index": 1}]]
},
"Merge Changes": {
"main": [[{"node": "Deduplicate by Email", "type": "main", "index": 0}]]
},
"Deduplicate by Email": {
"main": [[{"node": "Route by Source", "type": "main", "index": 0}]]
},
"Route by Source": {
"main": [
[{"node": "Upsert to Salesforce", "type": "main", "index": 0}],
[{"node": "Upsert to HubSpot", "type": "main", "index": 0}]
]
},
"Upsert to Salesforce": {
"main": [[{"node": "Update HubSpot Sync Status", "type": "main", "index": 0}]]
},
"Upsert to HubSpot": {
"main": [[{"node": "Update Salesforce Sync Status", "type": "main", "index": 0}]]
},
"Update HubSpot Sync Status": {
"main": [[{"node": "Log Sync Results", "type": "main", "index": 0}]]
},
"Update Salesforce Sync Status": {
"main": [[{"node": "Log Sync Results", "type": "main", "index": 0}]]
}
}
}Sincronizarea activitatilor si task-urilor
// activity-sync.js
/**
* Sync activities between CRM systems.
*/
// n8n Function: Convert HubSpot engagement to Salesforce Task
function convertEngagementToTask(engagement) {
const typeMapping = {
'EMAIL': 'Email',
'CALL': 'Call',
'MEETING': 'Meeting',
'NOTE': 'Other',
'TASK': 'Other'
};
const metadata = engagement.metadata || {};
return {
Subject: metadata.subject || `${engagement.type} Activity`,
Description: metadata.body || metadata.text || '',
Status: engagement.type === 'TASK'
? (metadata.status === 'COMPLETED' ? 'Completed' : 'Not Started')
: 'Completed',
Priority: metadata.priority || 'Normal',
ActivityDate: new Date(engagement.timestamp).toISOString().split('T')[0],
Type: typeMapping[engagement.type] || 'Other',
// Custom fields
HubSpot_Engagement_ID__c: engagement.id,
Source_System__c: 'HubSpot'
};
}
// n8n Function: Convert Salesforce Task to HubSpot engagement
function convertTaskToEngagement(task) {
const typeMapping = {
'Email': 'EMAIL',
'Call': 'CALL',
'Meeting': 'MEETING',
'Other': 'NOTE'
};
return {
engagement: {
type: typeMapping[task.Type] || 'NOTE',
timestamp: new Date(task.ActivityDate).getTime(),
active: task.Status !== 'Completed'
},
metadata: {
subject: task.Subject,
body: task.Description,
status: task.Status === 'Completed' ? 'COMPLETED' : 'NOT_STARTED'
},
associations: {
contactIds: task.WhoId ? [task.WhoId] : [],
companyIds: task.WhatId ? [task.WhatId] : [],
dealIds: []
}
};
}
// n8n Function: Sync meeting from calendar
function processMeetingSync(calendarEvent, contacts) {
const attendeeEmails = calendarEvent.attendees
?.map(a => a.email.toLowerCase()) || [];
// Match attendees to CRM contacts
const matchedContacts = contacts.filter(c =>
attendeeEmails.includes(c.email?.toLowerCase())
);
return {
event: calendarEvent,
matchedContacts,
hubspotEngagement: {
engagement: {
type: 'MEETING',
timestamp: new Date(calendarEvent.start.dateTime).getTime()
},
metadata: {
title: calendarEvent.summary,
body: calendarEvent.description,
startTime: new Date(calendarEvent.start.dateTime).getTime(),
endTime: new Date(calendarEvent.end.dateTime).getTime()
},
associations: {
contactIds: matchedContacts
.filter(c => c.hubspotId)
.map(c => c.hubspotId)
}
},
salesforceEvent: {
Subject: calendarEvent.summary,
Description: calendarEvent.description,
StartDateTime: calendarEvent.start.dateTime,
EndDateTime: calendarEvent.end.dateTime,
WhoId: matchedContacts.find(c => c.salesforceId)?.salesforceId
}
};
}
module.exports = {
convertEngagementToTask,
convertTaskToEngagement,
processMeetingSync
};Calitatea datelor si deduplicarea
// crm-deduplication.js
/**
* CRM data quality and deduplication utilities.
*/
class CRMDeduplicator {
constructor(options = {}) {
this.matchThreshold = options.matchThreshold || 0.85;
this.mergeStrategy = options.mergeStrategy || 'newest_primary';
}
/**
* Find potential duplicates in a contact list
*/
findDuplicates(contacts) {
const duplicateGroups = [];
const processed = new Set();
for (let i = 0; i < contacts.length; i++) {
if (processed.has(i)) continue;
const contact = contacts[i];
const group = [contact];
for (let j = i + 1; j < contacts.length; j++) {
if (processed.has(j)) continue;
const candidate = contacts[j];
const score = this.calculateMatchScore(contact, candidate);
if (score >= this.matchThreshold) {
group.push({ ...candidate, matchScore: score });
processed.add(j);
}
}
if (group.length > 1) {
processed.add(i);
duplicateGroups.push(group);
}
}
return duplicateGroups;
}
/**
* Calculate match score between two contacts
*/
calculateMatchScore(contact1, contact2) {
let score = 0;
let weights = 0;
// Email match (highest weight)
if (contact1.email && contact2.email) {
weights += 40;
if (contact1.email.toLowerCase() === contact2.email.toLowerCase()) {
score += 40;
}
}
// Name match
const name1 = `${contact1.firstname || ''} ${contact1.lastname || ''}`.toLowerCase().trim();
const name2 = `${contact2.firstname || ''} ${contact2.lastname || ''}`.toLowerCase().trim();
if (name1 && name2) {
weights += 30;
const nameSimilarity = this.stringSimilarity(name1, name2);
score += nameSimilarity * 30;
}
// Company match
if (contact1.company && contact2.company) {
weights += 15;
const companySimilarity = this.stringSimilarity(
contact1.company.toLowerCase(),
contact2.company.toLowerCase()
);
score += companySimilarity * 15;
}
// Phone match
const phone1 = this.normalizePhone(contact1.phone);
const phone2 = this.normalizePhone(contact2.phone);
if (phone1 && phone2) {
weights += 15;
if (phone1 === phone2) {
score += 15;
}
}
return weights > 0 ? score / weights : 0;
}
/**
* Calculate string similarity using Levenshtein distance
*/
stringSimilarity(str1, str2) {
if (!str1 || !str2) return 0;
if (str1 === str2) return 1;
const len1 = str1.length;
const len2 = str2.length;
const matrix = Array(len1 + 1).fill(null)
.map(() => Array(len2 + 1).fill(null));
for (let i = 0; i <= len1; i++) matrix[i][0] = i;
for (let j = 0; j <= len2; j++) matrix[0][j] = j;
for (let i = 1; i <= len1; i++) {
for (let j = 1; j <= len2; j++) {
const cost = str1[i - 1] === str2[j - 1] ? 0 : 1;
matrix[i][j] = Math.min(
matrix[i - 1][j] + 1,
matrix[i][j - 1] + 1,
matrix[i - 1][j - 1] + cost
);
}
}
const maxLen = Math.max(len1, len2);
return 1 - matrix[len1][len2] / maxLen;
}
/**
* Normalize phone number for comparison
*/
normalizePhone(phone) {
if (!phone) return null;
return phone.replace(/\D/g, '').slice(-10);
}
/**
* Merge duplicate contacts
*/
mergeContacts(duplicates) {
if (duplicates.length === 0) return null;
if (duplicates.length === 1) return duplicates[0];
// Sort by modification date (newest first)
const sorted = [...duplicates].sort((a, b) =>
new Date(b.lastModified || 0) - new Date(a.lastModified || 0)
);
const primary = { ...sorted[0] };
const mergedFrom = [];
// Merge fields from other records
for (let i = 1; i < sorted.length; i++) {
const record = sorted[i];
mergedFrom.push(record.id);
// Fill empty fields from secondary records
for (const [key, value] of Object.entries(record)) {
if (!primary[key] && value) {
primary[key] = value;
}
}
}
primary.mergedFrom = mergedFrom;
primary.mergedAt = new Date().toISOString();
return primary;
}
/**
* Validate contact data quality
*/
validateContact(contact) {
const issues = [];
const suggestions = [];
// Required fields
if (!contact.email) {
issues.push({ field: 'email', type: 'missing', severity: 'high' });
} else if (!this.isValidEmail(contact.email)) {
issues.push({ field: 'email', type: 'invalid', severity: 'high' });
}
if (!contact.lastname) {
issues.push({ field: 'lastname', type: 'missing', severity: 'medium' });
}
// Data quality checks
if (!contact.company) {
suggestions.push({
field: 'company',
message: 'Company name missing - consider enrichment'
});
}
if (!contact.phone) {
suggestions.push({
field: 'phone',
message: 'Phone number missing'
});
}
// Detect free email
if (contact.email) {
const freeProviders = ['gmail.com', 'yahoo.com', 'hotmail.com', 'outlook.com'];
const domain = contact.email.split('@')[1]?.toLowerCase();
if (freeProviders.includes(domain)) {
suggestions.push({
field: 'email',
message: 'Personal email - may need company email for B2B'
});
}
}
return {
valid: issues.filter(i => i.severity === 'high').length === 0,
issues,
suggestions,
qualityScore: this.calculateQualityScore(contact, issues)
};
}
isValidEmail(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
calculateQualityScore(contact, issues) {
let score = 100;
// Deduct for missing required fields
if (!contact.email) score -= 30;
if (!contact.lastname) score -= 15;
if (!contact.firstname) score -= 10;
// Deduct for missing optional fields
if (!contact.company) score -= 10;
if (!contact.phone) score -= 10;
if (!contact.jobtitle) score -= 5;
// Deduct for validation issues
for (const issue of issues) {
if (issue.type === 'invalid') {
score -= issue.severity === 'high' ? 20 : 10;
}
}
return Math.max(0, score);
}
}
module.exports = { CRMDeduplicator };Concluzie
Automatizarea CRM eficienta cu n8n permite sincronizarea fluenta a datelor intre HubSpot si Salesforce. Pattern-urile cheie acoperite includ:
- Pipeline-uri de procesare lead-uri cu scoring de calitate si rutare inteligenta
- Sincronizare bidirectionala cu gestionarea conflictelor si mentinerea consistentei datelor
- Sincronizare Deal/Opportunity cu maparea stadiilor intre sisteme
- Sincronizarea activitatilor mentinand istoricul de engagement unificat
- Managementul calitatii datelor cu deduplicare si validare
Aceste pattern-uri asigura ca echipele de vanzari au informatii precise si actualizate, indiferent de CRM-ul pe care il folosesc.