n8n Automation

Automatizare CRM cu n8n: Pattern-uri de integrare HubSpot si Salesforce

Petru Constantin
--18 min lectura
#n8n#crm#hubspot#salesforce#automation#lead-management#workflow

Automatizare CRM cu n8n: Pattern-uri de integrare HubSpot si Salesforce

Automatizarea CRM este esentiala pentru scalarea operatiunilor de vanzari si mentinerea consistentei datelor intre sisteme. Acest ghid acopera pattern-uri complete de workflow n8n pentru integrarea HubSpot si Salesforce, inclusiv managementul lead-urilor, sincronizarea contactelor si urmarirea deal-urilor.

Arhitectura integrarii CRM

Strategie de sincronizare multi-CRM

// 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 };

Workflow-uri de management al lead-urilor

Procesarea lead-urilor noi

{
  "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}]]
    }
  }
}

Sincronizarea Deal/Opportunity

// 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
};

Sincronizare bidirectionala a contactelor

{
  "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}]]
    }
  }
}

Sincronizarea activitatilor si task-urilor

// 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
};

Calitatea datelor si deduplicarea

// 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 };

Concluzie

Automatizarea CRM eficienta cu n8n permite sincronizarea fluenta a datelor intre HubSpot si Salesforce. Pattern-urile cheie acoperite includ:

  1. Pipeline-uri de procesare lead-uri cu scoring de calitate si rutare inteligenta
  2. Sincronizare bidirectionala cu gestionarea conflictelor si mentinerea consistentei datelor
  3. Sincronizare Deal/Opportunity cu maparea stadiilor intre sisteme
  4. Sincronizarea activitatilor mentinand istoricul de engagement unificat
  5. Managementul calitatii datelor cu deduplicare si validare

Aceste pattern-uri asigura ca echipele de vanzari au informatii precise si actualizate, indiferent de CRM-ul pe care il folosesc.

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.