Sistemele CRM sunt coloana vertebrala a operatiunilor de vanzari, dar puterea lor reala apare cand sunt integrate cu alte instrumente de business. Acest ghid demonstreaza cum sa construiesti workflow-uri CRM gata de productie cu n8n pentru Salesforce si HubSpot.
Configurare integrare Salesforce
Configurare autentificare
Configureaza OAuth 2.0 pentru 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"
}
}
}Creare si gestionare lead-uri
// 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
}
}];Interogari SOQL in Salesforce
// Function Node: Build Dynamic SOQL Query
const queryType = $input.first().json.queryType;
const params = $input.first().json.params || {};
const queries = {
// Lead-uri create in ultimele 24 de ore
newLeads: `
SELECT Id, FirstName, LastName, Email, Company, LeadSource, CreatedDate
FROM Lead
WHERE CreatedDate >= LAST_N_DAYS:1
ORDER BY CreatedDate DESC
LIMIT 100
`,
// Oportunitati dupa etapa
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
`,
// Contacte cu activitate recenta
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
`,
// Conturi dupa venituri anuale
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, ' ')
}
}];Configurare integrare HubSpot
Configurare API HubSpot
// 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()
}
}];Creare si actualizare contacte
// 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
}
}];Sincronizare bidirectionala CRM
Sincronizare Salesforce catre HubSpot
// 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()
}
}
}];Sincronizare HubSpot catre Salesforce
// 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
}
}];Workflow de imbogatire lead-uri
Imbogatire date de contact
// 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
}
}
}];Combinare date imbogatite
// 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
}];Sincronizare deal-uri/oportunitati
Mapare etape oportunitati
// 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
}];Sincronizare activitati si note
Inregistrare taskuri si activitati
// 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';
}Gestionare erori si logica de reincercare
Handler robust pentru erori de sincronizare
// 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
}
}];Exemplu complet de workflow
Structura workflow de sincronizare CRM completa
# 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_logBune practici
- Foloseste intotdeauna operatiuni upsert pentru a preveni duplicatele
- Mapeaza ID-uri externe intre sisteme pentru sincronizare fiabila
- Gestioneaza rate limit-urile cu backoff exponential
- Inregistreaza toate operatiunile pentru debugging si audit
- Valideaza datele inainte de a le trimite catre CRM-ul tinta
- Foloseste webhook-uri pentru sincronizare in timp real cand este posibil
- Testeaza maparea campurilor riguros inainte de productie
Integrarile CRM cu n8n ofera flexibilitatea de a construi automatizari sofisticate, mentinand in acelasi timp consistenta datelor intre sisteme. Cheia sta in gestionarea robusta a erorilor si logarea detaliata pentru a asigura o sincronizare fiabila.
Sistemul tau AI e conform cu EU AI Act? Evaluare gratuita de risc - afla in 2 minute →