CRM systems are the backbone of sales operations, but their true power emerges when integrated with other business tools. This guide demonstrates building production-ready CRM workflows with n8n for Salesforce and HubSpot.
Salesforce Integration Setup
Authentication Configuration
Configure OAuth 2.0 for Salesforce:
// Salesforce OAuth Configuration
{
"credentials": {
"oauthApi": {
"authorizationUrl": "https://login.salesforce.com/services/oauth2/authorize",
"accessTokenUrl": "https://login.salesforce.com/services/oauth2/token",
"clientId": "{{ $env.SALESFORCE_CLIENT_ID }}",
"clientSecret": "{{ $env.SALESFORCE_CLIENT_SECRET }}",
"scope": "api refresh_token",
"authQueryParameters": "prompt=consent"
}
}
}Lead Creation and Management
// Function Node: Create or Update Salesforce Lead
const salesforceInstance = $env.SALESFORCE_INSTANCE_URL;
const inputData = $input.first().json;
// Prepare lead data with field mapping
const leadData = {
FirstName: inputData.firstName,
LastName: inputData.lastName,
Email: inputData.email,
Company: inputData.company || 'Unknown',
Phone: inputData.phone,
LeadSource: inputData.source || 'Web',
Status: 'New',
Description: inputData.notes,
// Custom fields
Lead_Score__c: inputData.leadScore || 0,
UTM_Source__c: inputData.utmSource,
UTM_Medium__c: inputData.utmMedium,
UTM_Campaign__c: inputData.utmCampaign
};
// Remove null/undefined values
Object.keys(leadData).forEach(key => {
if (leadData[key] === null || leadData[key] === undefined) {
delete leadData[key];
}
});
return [{
json: {
operation: 'upsert',
objectType: 'Lead',
externalIdField: 'Email',
data: leadData
}
}];Salesforce SOQL Queries
// Function Node: Build Dynamic SOQL Query
const queryType = $input.first().json.queryType;
const params = $input.first().json.params || {};
const queries = {
// Get leads created in last 24 hours
newLeads: `
SELECT Id, FirstName, LastName, Email, Company, LeadSource, CreatedDate
FROM Lead
WHERE CreatedDate >= LAST_N_DAYS:1
ORDER BY CreatedDate DESC
LIMIT 100
`,
// Get opportunities by stage
opportunitiesByStage: `
SELECT Id, Name, Amount, StageName, CloseDate, Account.Name, Owner.Name
FROM Opportunity
WHERE StageName = '${params.stage || 'Prospecting'}'
AND IsClosed = false
ORDER BY Amount DESC
`,
// Get contacts with recent activity
activeContacts: `
SELECT Id, FirstName, LastName, Email, Account.Name,
(SELECT Id, Subject, ActivityDate FROM Tasks WHERE ActivityDate >= LAST_N_DAYS:30 LIMIT 5),
(SELECT Id, Subject, CreatedDate FROM EmailMessages WHERE CreatedDate >= LAST_N_DAYS:30 LIMIT 5)
FROM Contact
WHERE AccountId != null
LIMIT 50
`,
// Get accounts by annual revenue
topAccounts: `
SELECT Id, Name, AnnualRevenue, Industry, NumberOfEmployees,
(SELECT Id, Name, Amount, StageName FROM Opportunities WHERE IsClosed = false)
FROM Account
WHERE AnnualRevenue > ${params.minRevenue || 100000}
ORDER BY AnnualRevenue DESC
LIMIT 25
`
};
const query = queries[queryType];
if (!query) {
throw new Error(`Unknown query type: ${queryType}`);
}
return [{
json: {
query: query.trim().replace(/\s+/g, ' ')
}
}];HubSpot Integration Setup
HubSpot API Configuration
// HubSpot API Helper Functions
const hubspotApiKey = $env.HUBSPOT_API_KEY;
const hubspotBaseUrl = 'https://api.hubapi.com';
// Contact properties mapping
const propertyMapping = {
email: 'email',
firstName: 'firstname',
lastName: 'lastname',
phone: 'phone',
company: 'company',
website: 'website',
jobTitle: 'jobtitle',
leadSource: 'hs_lead_status',
lifecycleStage: 'lifecyclestage'
};
function mapToHubSpotProperties(data) {
const properties = [];
for (const [key, hubspotProp] of Object.entries(propertyMapping)) {
if (data[key] !== undefined && data[key] !== null) {
properties.push({
property: hubspotProp,
value: data[key]
});
}
}
return properties;
}
return [{
json: {
mapping: propertyMapping,
mapFunction: mapToHubSpotProperties.toString()
}
}];Contact Creation and Update
// Function Node: HubSpot Contact Upsert
const contact = $input.first().json;
// Build properties array
const properties = [
{ property: 'email', value: contact.email },
{ property: 'firstname', value: contact.firstName },
{ property: 'lastname', value: contact.lastName },
{ property: 'phone', value: contact.phone },
{ property: 'company', value: contact.company },
{ property: 'lifecyclestage', value: contact.lifecycleStage || 'lead' }
];
// Add custom properties
if (contact.customProperties) {
for (const [key, value] of Object.entries(contact.customProperties)) {
properties.push({ property: key, value: String(value) });
}
}
// Filter out empty values
const filteredProperties = properties.filter(p =>
p.value !== undefined && p.value !== null && p.value !== ''
);
return [{
json: {
email: contact.email,
properties: filteredProperties
}
}];Bi-Directional CRM Sync
Salesforce to HubSpot Sync
// Function Node: Transform Salesforce Lead to HubSpot Contact
const salesforceLead = $input.first().json;
// Map Salesforce fields to HubSpot properties
const hubspotContact = {
properties: [
{ property: 'email', value: salesforceLead.Email },
{ property: 'firstname', value: salesforceLead.FirstName },
{ property: 'lastname', value: salesforceLead.LastName },
{ property: 'phone', value: salesforceLead.Phone },
{ property: 'company', value: salesforceLead.Company },
{ property: 'jobtitle', value: salesforceLead.Title },
// Lifecycle mapping
{ property: 'lifecyclestage', value: mapLeadStatus(salesforceLead.Status) },
// Source tracking
{ property: 'hs_analytics_source', value: salesforceLead.LeadSource },
// Custom sync field
{ property: 'salesforce_lead_id', value: salesforceLead.Id },
{ property: 'last_synced_from_salesforce', value: new Date().toISOString() }
].filter(p => p.value != null)
};
function mapLeadStatus(sfStatus) {
const statusMap = {
'New': 'lead',
'Working': 'marketingqualifiedlead',
'Qualified': 'salesqualifiedlead',
'Converted': 'opportunity',
'Unqualified': 'other'
};
return statusMap[sfStatus] || 'lead';
}
return [{
json: {
...hubspotContact,
syncMetadata: {
source: 'salesforce',
sourceId: salesforceLead.Id,
syncTime: new Date().toISOString()
}
}
}];HubSpot to Salesforce Sync
// Function Node: Transform HubSpot Contact to Salesforce Lead
const hubspotContact = $input.first().json;
const properties = hubspotContact.properties;
// Map HubSpot properties to Salesforce fields
const salesforceLead = {
Email: properties.email,
FirstName: properties.firstname,
LastName: properties.lastname,
Phone: properties.phone,
Company: properties.company || 'Unknown',
Title: properties.jobtitle,
// Status mapping
Status: mapLifecycleStage(properties.lifecyclestage),
// Lead source
LeadSource: mapHubSpotSource(properties.hs_analytics_source),
// Custom fields
HubSpot_Contact_ID__c: String(hubspotContact.vid),
Last_Synced_From_HubSpot__c: new Date().toISOString()
};
function mapLifecycleStage(stage) {
const stageMap = {
'subscriber': 'New',
'lead': 'New',
'marketingqualifiedlead': 'Working',
'salesqualifiedlead': 'Qualified',
'opportunity': 'Qualified',
'customer': 'Converted'
};
return stageMap[stage] || 'New';
}
function mapHubSpotSource(source) {
const sourceMap = {
'ORGANIC_SEARCH': 'Web',
'PAID_SEARCH': 'Paid Search',
'EMAIL_MARKETING': 'Email',
'SOCIAL_MEDIA': 'Social',
'REFERRALS': 'Referral',
'DIRECT_TRAFFIC': 'Direct'
};
return sourceMap[source] || 'Other';
}
// Remove null values
Object.keys(salesforceLead).forEach(key => {
if (salesforceLead[key] === null || salesforceLead[key] === undefined) {
delete salesforceLead[key];
}
});
return [{
json: {
objectType: 'Lead',
externalIdField: 'Email',
data: salesforceLead
}
}];Lead Enrichment Workflow
Contact Data Enrichment
// Function Node: Enrich Lead Data
const lead = $input.first().json;
// Build enrichment request
const enrichmentData = {
email: lead.email,
domain: extractDomain(lead.email),
companyName: lead.company
};
function extractDomain(email) {
if (!email) return null;
const parts = email.split('@');
return parts.length > 1 ? parts[1] : null;
}
// Free email domains to skip company enrichment
const freeEmailDomains = [
'gmail.com', 'yahoo.com', 'hotmail.com', 'outlook.com',
'aol.com', 'icloud.com', 'protonmail.com'
];
const shouldEnrichCompany = enrichmentData.domain &&
!freeEmailDomains.includes(enrichmentData.domain.toLowerCase());
return [{
json: {
...lead,
enrichment: {
shouldEnrichCompany,
domain: enrichmentData.domain,
originalData: lead
}
}
}];Merge Enriched Data
// Function Node: Merge Enrichment Results
const original = $input.first().json.originalData;
const clearbitData = $input.first().json.clearbitResult || {};
const apolloData = $input.first().json.apolloResult || {};
// Merge with priority: Original > Clearbit > Apollo
const enrichedLead = {
// Original required fields
email: original.email,
// Enriched personal info
firstName: original.firstName || clearbitData.person?.name?.givenName || apolloData.firstName,
lastName: original.lastName || clearbitData.person?.name?.familyName || apolloData.lastName,
phone: original.phone || apolloData.phone,
jobTitle: original.jobTitle || clearbitData.person?.employment?.title || apolloData.title,
linkedinUrl: clearbitData.person?.linkedin || apolloData.linkedin_url,
// Enriched company info
company: original.company || clearbitData.company?.name || apolloData.organization?.name,
companyDomain: clearbitData.company?.domain || apolloData.organization?.website_url,
industry: clearbitData.company?.category?.industry || apolloData.organization?.industry,
employeeCount: clearbitData.company?.metrics?.employees || apolloData.organization?.estimated_num_employees,
annualRevenue: clearbitData.company?.metrics?.estimatedAnnualRevenue,
companyLinkedIn: clearbitData.company?.linkedin || apolloData.organization?.linkedin_url,
companyLocation: formatCompanyLocation(clearbitData.company?.geo || apolloData.organization),
// Lead scoring inputs
enrichmentScore: calculateEnrichmentScore(clearbitData, apolloData),
enrichedAt: new Date().toISOString(),
enrichmentSources: getEnrichmentSources(clearbitData, apolloData)
};
function formatCompanyLocation(geo) {
if (!geo) return null;
const parts = [geo.city, geo.state, geo.country].filter(Boolean);
return parts.join(', ');
}
function calculateEnrichmentScore(clearbit, apollo) {
let score = 0;
// Score based on data completeness
if (clearbit.person) score += 30;
if (clearbit.company) score += 30;
if (apollo.email) score += 20;
if (apollo.organization) score += 20;
return score;
}
function getEnrichmentSources(clearbit, apollo) {
const sources = [];
if (Object.keys(clearbit).length > 0) sources.push('clearbit');
if (Object.keys(apollo).length > 0) sources.push('apollo');
return sources;
}
return [{
json: enrichedLead
}];Deal/Opportunity Sync
Opportunity Stage Mapping
// Function Node: Opportunity Stage Sync Handler
const opportunity = $input.first().json;
const direction = $input.first().json.direction; // 'sf_to_hs' or 'hs_to_sf'
// Stage mappings between systems
const stageMap = {
// Salesforce Stage -> HubSpot Deal Stage
sf_to_hs: {
'Prospecting': 'appointmentscheduled',
'Qualification': 'qualifiedtobuy',
'Needs Analysis': 'presentationscheduled',
'Value Proposition': 'presentationscheduled',
'Proposal/Price Quote': 'decisionmakerboughtin',
'Negotiation/Review': 'contractsent',
'Closed Won': 'closedwon',
'Closed Lost': 'closedlost'
},
// HubSpot Deal Stage -> Salesforce Stage
hs_to_sf: {
'appointmentscheduled': 'Prospecting',
'qualifiedtobuy': 'Qualification',
'presentationscheduled': 'Value Proposition',
'decisionmakerboughtin': 'Proposal/Price Quote',
'contractsent': 'Negotiation/Review',
'closedwon': 'Closed Won',
'closedlost': 'Closed Lost'
}
};
let mappedData;
if (direction === 'sf_to_hs') {
mappedData = {
properties: [
{ property: 'dealname', value: opportunity.Name },
{ property: 'amount', value: opportunity.Amount },
{ property: 'dealstage', value: stageMap.sf_to_hs[opportunity.StageName] || 'appointmentscheduled' },
{ property: 'closedate', value: new Date(opportunity.CloseDate).getTime() },
{ property: 'pipeline', value: 'default' },
{ property: 'salesforce_opportunity_id', value: opportunity.Id }
]
};
} else {
mappedData = {
objectType: 'Opportunity',
data: {
Name: opportunity.properties.dealname,
Amount: parseFloat(opportunity.properties.amount) || 0,
StageName: stageMap.hs_to_sf[opportunity.properties.dealstage] || 'Prospecting',
CloseDate: new Date(parseInt(opportunity.properties.closedate)).toISOString().split('T')[0],
HubSpot_Deal_ID__c: String(opportunity.dealId)
}
};
}
return [{
json: mappedData
}];Activity and Note Sync
Task and Activity Logging
// Function Node: Sync Activities Between CRMs
const activity = $input.first().json;
const targetCRM = $input.first().json.target; // 'salesforce' or 'hubspot'
if (targetCRM === 'hubspot') {
// Create HubSpot engagement
return [{
json: {
engagement: {
active: true,
type: mapActivityType(activity.Type),
timestamp: new Date(activity.ActivityDate || activity.CreatedDate).getTime()
},
associations: {
contactIds: activity.contactIds || [],
companyIds: activity.companyIds || [],
dealIds: activity.dealIds || []
},
metadata: {
subject: activity.Subject,
body: activity.Description,
status: activity.Status === 'Completed' ? 'COMPLETED' : 'NOT_STARTED',
forObjectType: 'CONTACT'
}
}
}];
} else {
// Create Salesforce Task
return [{
json: {
objectType: 'Task',
data: {
Subject: activity.metadata?.subject || activity.subject,
Description: activity.metadata?.body || activity.body,
Status: activity.metadata?.status === 'COMPLETED' ? 'Completed' : 'Not Started',
Priority: 'Normal',
WhoId: activity.contactId,
WhatId: activity.dealId,
ActivityDate: new Date().toISOString().split('T')[0]
}
}
}];
}
function mapActivityType(sfType) {
const typeMap = {
'Call': 'CALL',
'Email': 'EMAIL',
'Meeting': 'MEETING',
'Task': 'TASK',
'Other': 'NOTE'
};
return typeMap[sfType] || 'NOTE';
}Error Handling and Retry Logic
Robust Sync Error Handler
// Function Node: CRM Sync Error Handler
const error = $input.first().json.error;
const operation = $input.first().json.operation;
const record = $input.first().json.record;
// Categorize error
const errorCategory = categorizeError(error);
// Determine retry strategy
const retryConfig = getRetryConfig(errorCategory);
function categorizeError(err) {
const errorMessage = err.message || err.toString();
if (errorMessage.includes('RATE_LIMIT') || errorMessage.includes('429')) {
return 'rate_limit';
}
if (errorMessage.includes('DUPLICATE') || errorMessage.includes('already exists')) {
return 'duplicate';
}
if (errorMessage.includes('NOT_FOUND') || errorMessage.includes('404')) {
return 'not_found';
}
if (errorMessage.includes('INVALID') || errorMessage.includes('validation')) {
return 'validation';
}
if (errorMessage.includes('AUTH') || errorMessage.includes('401') || errorMessage.includes('403')) {
return 'auth';
}
return 'unknown';
}
function getRetryConfig(category) {
const configs = {
rate_limit: {
shouldRetry: true,
delay: 60000, // 1 minute
maxRetries: 5
},
duplicate: {
shouldRetry: false,
action: 'update_instead'
},
not_found: {
shouldRetry: false,
action: 'create_instead'
},
validation: {
shouldRetry: false,
action: 'log_and_skip'
},
auth: {
shouldRetry: true,
delay: 1000,
maxRetries: 1,
action: 'refresh_token'
},
unknown: {
shouldRetry: true,
delay: 5000,
maxRetries: 3
}
};
return configs[category] || configs.unknown;
}
return [{
json: {
errorCategory,
retryConfig,
operation,
record,
timestamp: new Date().toISOString(),
originalError: error
}
}];Complete Workflow Example
Full CRM Sync Workflow Structure
# Workflow: Bi-directional CRM Sync
name: CRM Sync - Salesforce <> HubSpot
nodes:
# Trigger: Run on schedule and webhooks
- name: Schedule Trigger
type: n8n-nodes-base.scheduleTrigger
parameters:
rule:
interval:
- field: hours
hoursInterval: 1
- name: Webhook Trigger
type: n8n-nodes-base.webhook
parameters:
path: crm-sync
method: POST
# Determine sync direction
- name: Determine Sync Type
type: n8n-nodes-base.switch
parameters:
rules:
- value: salesforce
output: 0
- value: hubspot
output: 1
# Salesforce Branch
- name: Get Salesforce Changes
type: n8n-nodes-base.salesforce
parameters:
operation: getAll
resource: lead
options:
condition: LastModifiedDate >= LAST_N_HOURS:1
- name: Transform SF to HS
type: n8n-nodes-base.function
# Transform code here
- name: Upsert to HubSpot
type: n8n-nodes-base.hubspot
parameters:
operation: upsert
resource: contact
# HubSpot Branch
- name: Get HubSpot Changes
type: n8n-nodes-base.hubspot
parameters:
operation: getRecentlyUpdated
resource: contact
- name: Transform HS to SF
type: n8n-nodes-base.function
# Transform code here
- name: Upsert to Salesforce
type: n8n-nodes-base.salesforce
parameters:
operation: upsert
resource: lead
# Common: Error handling and logging
- name: Log Sync Result
type: n8n-nodes-base.postgres
parameters:
operation: insert
table: crm_sync_logBest Practices
- Always use upsert operations to prevent duplicates
- Map external IDs between systems for reliable sync
- Handle rate limits with exponential backoff
- Log all operations for debugging and audit
- Validate data before sending to target CRM
- Use webhooks for real-time sync when available
- Test field mappings thoroughly before production
CRM integrations with n8n provide the flexibility to build sophisticated automation while maintaining data consistency across systems. The key is robust error handling and comprehensive logging to ensure reliable synchronization.