n8n Automation

n8n integrare CRM: Salesforce si HubSpot

Petru Constantin
--11 min lectura
#n8n#CRM#Salesforce#HubSpot#workflow automation

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_log

Bune practici

  1. Foloseste intotdeauna operatiuni upsert pentru a preveni duplicatele
  2. Mapeaza ID-uri externe intre sisteme pentru sincronizare fiabila
  3. Gestioneaza rate limit-urile cu backoff exponential
  4. Inregistreaza toate operatiunile pentru debugging si audit
  5. Valideaza datele inainte de a le trimite catre CRM-ul tinta
  6. Foloseste webhook-uri pentru sincronizare in timp real cand este posibil
  7. 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 →

Ai nevoie de ajutor cu conformitatea EU AI Act sau securitatea AI?

Programeaza o consultatie gratuita de 30 de minute. Fara obligatii.

Programeaza un Apel

Weekly AI Security & Automation Digest

Get the latest on AI Security, workflow automation, secure integrations, and custom platform development delivered weekly.

No spam. Unsubscribe anytime.