n8n CRM Automation: HubSpot and Salesforce Integration Patterns
CRM automation is essential for scaling sales operations and maintaining data consistency across systems. This guide covers comprehensive n8n workflow patterns for HubSpot and Salesforce integration, including lead management, contact synchronization, and deal tracking.
CRM Integration Architecture
Multi-CRM Sync Strategy
// 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 };Lead Management Workflows
New Lead Processing
{
"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}]]
}
}
}Deal/Opportunity Sync
// 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
};Bi-Directional Contact Sync
{
"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}]]
}
}
}Activity and Task Sync
// 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
};Data Quality and Deduplication
// 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 };Conclusion
Effective CRM automation with n8n enables seamless data synchronization between HubSpot and Salesforce. Key patterns covered include:
- Lead processing pipelines with quality scoring and intelligent routing
- Bi-directional sync handling conflicts and maintaining data consistency
- Deal/Opportunity sync with stage mapping between systems
- Activity synchronization keeping engagement history unified
- Data quality management with deduplication and validation
These patterns ensure sales teams have accurate, up-to-date information regardless of which CRM they use.