n8n Automation

n8n CRM Integration: Salesforce and HubSpot Automation Workflows

DeviDevs Team
10 min read
#n8n#CRM#Salesforce#HubSpot#workflow automation

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_log

Best Practices

  1. Always use upsert operations to prevent duplicates
  2. Map external IDs between systems for reliable sync
  3. Handle rate limits with exponential backoff
  4. Log all operations for debugging and audit
  5. Validate data before sending to target CRM
  6. Use webhooks for real-time sync when available
  7. 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.

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.