{
  "name": "Duplicate Contact Consolidator",
  "nodes": [
    {
      "id": "1a42eb6a-eacc-405e-923c-53586179dd4d",
      "name": "Weekly Dedupe Scan",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        96
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "40 8 * * 1"
            }
          ]
        }
      }
    },
    {
      "id": "e5981ce9-e5a6-4a62-8cb7-b10c9bc4d3fa",
      "name": "Get Contacts",
      "type": "n8n-nodes-base.salesforce",
      "typeVersion": 1.1,
      "position": [
        224,
        96
      ],
      "parameters": {
        "resource": "search",
        "operation": "query",
        "query": "SELECT Id, FirstName, LastName, Name, Email, Title, Phone, MobilePhone, AccountId, Account.Name, CreatedDate, LastModifiedDate FROM Contact WHERE Email != null"
      },
      "executeOnce": true,
      "alwaysOutputData": true,
      "credentials": {
        "salesforceOAuth2Api": {
          "id": "553Tae1kPe5Wpthq",
          "name": "Salesforce account"
        }
      }
    },
    {
      "id": "8d118bb5-d8a1-411b-ab46-0caee92450d4",
      "name": "Get Open Leads",
      "type": "n8n-nodes-base.salesforce",
      "typeVersion": 1.1,
      "position": [
        448,
        96
      ],
      "parameters": {
        "resource": "search",
        "operation": "query",
        "query": "SELECT Id, Name, Email, Company, Status FROM Lead WHERE IsConverted = false AND Email != null"
      },
      "executeOnce": true,
      "alwaysOutputData": true,
      "credentials": {
        "salesforceOAuth2Api": {
          "id": "553Tae1kPe5Wpthq",
          "name": "Salesforce account"
        }
      }
    },
    {
      "id": "fe597fcd-8d8f-4f0e-9058-0e5fd594949c",
      "name": "Get Opportunity Roles",
      "type": "n8n-nodes-base.salesforce",
      "typeVersion": 1.1,
      "position": [
        672,
        96
      ],
      "parameters": {
        "resource": "search",
        "operation": "query",
        "query": "SELECT ContactId FROM OpportunityContactRole"
      },
      "executeOnce": true,
      "alwaysOutputData": true,
      "credentials": {
        "salesforceOAuth2Api": {
          "id": "553Tae1kPe5Wpthq",
          "name": "Salesforce account"
        }
      }
    },
    {
      "id": "56cb99b2-76b8-4a8c-9a9b-f91f1afa2f75",
      "name": "Get Cases",
      "type": "n8n-nodes-base.salesforce",
      "typeVersion": 1.1,
      "position": [
        896,
        96
      ],
      "parameters": {
        "resource": "search",
        "operation": "query",
        "query": "SELECT ContactId FROM Case WHERE ContactId != null"
      },
      "executeOnce": true,
      "alwaysOutputData": true,
      "credentials": {
        "salesforceOAuth2Api": {
          "id": "553Tae1kPe5Wpthq",
          "name": "Salesforce account"
        }
      }
    },
    {
      "id": "cbfc6119-8b40-4bac-9ea6-993b76144be0",
      "name": "Get Tasks",
      "type": "n8n-nodes-base.salesforce",
      "typeVersion": 1.1,
      "position": [
        1120,
        96
      ],
      "parameters": {
        "resource": "search",
        "operation": "query",
        "query": "SELECT WhoId FROM Task WHERE WhoId != null"
      },
      "executeOnce": true,
      "alwaysOutputData": true,
      "credentials": {
        "salesforceOAuth2Api": {
          "id": "553Tae1kPe5Wpthq",
          "name": "Salesforce account"
        }
      }
    },
    {
      "id": "47be6178-e577-4011-aa7d-a3983aad2310",
      "name": "Get Events",
      "type": "n8n-nodes-base.salesforce",
      "typeVersion": 1.1,
      "position": [
        1344,
        96
      ],
      "parameters": {
        "resource": "search",
        "operation": "query",
        "query": "SELECT WhoId FROM Event WHERE WhoId != null"
      },
      "executeOnce": true,
      "alwaysOutputData": true,
      "credentials": {
        "salesforceOAuth2Api": {
          "id": "553Tae1kPe5Wpthq",
          "name": "Salesforce account"
        }
      }
    },
    {
      "id": "8936b362-962d-40bd-b287-80d7406ab4a2",
      "name": "Plan Consolidation",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1568,
        96
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "// Plan the consolidation in code: same-email Contact groups -> one survivor each; open Leads that share an email with a Contact -> convert onto it.\n// Survivor rule (decided for this build): most attached history wins (opportunity roles + cases + tasks + events); ties go to the most recently modified.\nconst contacts = $('Get Contacts').all().map(i => i.json).filter(c => c.Id && c.Email);\nconst leads = $('Get Open Leads').all().map(i => i.json).filter(l => l.Id && l.Email);\nconst count = (node, key) => { const m = {}; for (const r of $(node).all().map(i => i.json)) if (r[key]) m[r[key]] = (m[r[key]] || 0) + 1; return m; };\nconst roles = count('Get Opportunity Roles', 'ContactId'), cases = count('Get Cases', 'ContactId'), tasks = count('Get Tasks', 'WhoId'), events = count('Get Events', 'WhoId');\nconst hist = c => ({ roles: roles[c.Id] || 0, cases: cases[c.Id] || 0, tasks: tasks[c.Id] || 0, events: events[c.Id] || 0 });\nconst score = h => h.roles + h.cases + h.tasks + h.events;\nconst desc = h => [h.roles && h.roles + ' opportunity role' + (h.roles > 1 ? 's' : ''), h.cases && h.cases + ' case' + (h.cases > 1 ? 's' : ''), h.tasks && h.tasks + ' task' + (h.tasks > 1 ? 's' : ''), h.events && h.events + ' event' + (h.events > 1 ? 's' : '')].filter(Boolean).join(', ') || 'no history';\nconst key = e => String(e).trim().toLowerCase();\n\nconst byEmail = {}; for (const c of contacts) (byEmail[key(c.Email)] = byEmail[key(c.Email)] || []).push(c);\nconst merges = [];\nfor (const [email, group] of Object.entries(byEmail)) {\n  if (group.length < 2) continue;\n  const ranked = group.map(c => ({ c, h: hist(c) })).sort((a, b) => score(b.h) - score(a.h) || String(b.c.LastModifiedDate).localeCompare(String(a.c.LastModifiedDate)) || a.c.Id.localeCompare(b.c.Id));\n  const keep = ranked[0], dupes = ranked.slice(1, 3); // Database.merge takes at most 2 duplicates per call\n  const newest = group.slice().sort((a, b) => String(b.LastModifiedDate).localeCompare(String(a.LastModifiedDate)))[0];\n  // Fill blanks on the survivor from the duplicates before merging (merge keeps the survivor's field values).\n  const fill = {}; for (const f of ['Phone', 'Title', 'MobilePhone']) if (!keep.c[f]) { const src = dupes.find(d => d.c[f]); if (src) fill[f] = src.c[f]; }\n  merges.push({ email, survivorId: keep.c.Id, survivorName: keep.c.Name, survivorTitle: keep.c.Title || '', survivorHistory: desc(keep.h), survivorScore: score(keep.h),\n    duplicates: dupes.map(d => ({ id: d.c.Id, name: d.c.Name, title: d.c.Title || '', history: desc(d.h), score: score(d.h) })),\n    reason: score(keep.h) > Math.max(...dupes.map(d => score(d.h))) ? 'most attached history (' + desc(keep.h) + ')' : 'history tied; most recently modified',\n    newestIsSurvivor: newest.Id === keep.c.Id, fill, account: (keep.c.Account || {}).Name || '' });\n}\nconst conversions = [];\nfor (const l of leads) {\n  const group = byEmail[key(l.Email)]; if (!group) continue;\n  const target = merges.find(m => m.email === key(l.Email)) ? group.find(c => c.Id === merges.find(m => m.email === key(l.Email)).survivorId) : group[0];\n  conversions.push({ email: key(l.Email), leadId: l.Id, leadName: l.Name, leadCompany: l.Company, leadStatus: l.Status, contactId: target.Id, contactName: target.Name,\n    accountId: target.AccountId, account: (target.Account || {}).Name || '' });\n}\nif (!merges.length && !conversions.length) return [];\nconst emails = [...new Set([...merges.map(m => m.email), ...conversions.map(c => c.email)])];\nconst lines = [':broom: *Duplicate Consolidation: approval needed*', ''];\nif (merges.length) {\n  lines.push('*' + merges.length + ' Contact merge' + (merges.length > 1 ? 's' : '') + '* (Salesforce Database.merge: the duplicate\\'s opportunities, cases and activities move to the survivor)');\n  for (const m of merges) {\n    lines.push('• `' + m.email + '`' + (m.account ? ' at ' + m.account : ''));\n    lines.push('    keep *' + m.survivorName + '*' + (m.survivorTitle ? ', ' + m.survivorTitle : '') + ' (' + m.survivorHistory + ')');\n    for (const d of m.duplicates) lines.push('    merge in ' + d.name + (d.title ? ', ' + d.title : '') + ' (' + d.history + ')');\n    lines.push('    why: ' + m.reason + (m.newestIsSurvivor ? '' : '; the duplicate was edited more recently, but has less history'));\n    if (Object.keys(m.fill).length) lines.push('    gap fill before merge: ' + Object.entries(m.fill).map(([k, v]) => k + ' ' + v).join(', '));\n  }\n  lines.push('');\n}\nif (conversions.length) {\n  lines.push('*' + conversions.length + ' Lead conversion' + (conversions.length > 1 ? 's' : '') + '* onto the existing Contact (no new Contact, no opportunity)');\n  for (const c of conversions) lines.push('• `' + c.email + '`: Lead ' + c.leadName + ' (' + c.leadCompany + ', status \"' + c.leadStatus + '\"' + (/convert/i.test(c.leadStatus) ? ' yet never actually converted' : '') + ') → Contact ' + c.contactName + (c.account ? ' at ' + c.account : ''));\n  lines.push('');\n}\nlines.push('Then each canonical record is upserted into HubSpot by email, and both systems are re-checked for exactly one record per email.');\nreturn [{ json: { merges, conversions, emails, approvalText: lines.join('\\n') } }];\n"
      },
      "executeOnce": true
    },
    {
      "id": "ed3d159c-6ec4-4cc3-8d71-e8e5978e0fb3",
      "name": "Ask Approval in Slack",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.3,
      "position": [
        1792,
        96
      ],
      "parameters": {
        "resource": "message",
        "operation": "sendAndWait",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "id",
          "value": "C0BQ3G96HL6",
          "cachedResultName": "general"
        },
        "message": "={{ $json.approvalText }}",
        "responseType": "approval",
        "approvalOptions": {
          "values": {
            "approvalType": "double",
            "approveLabel": "Approve merge",
            "disapproveLabel": "Decline"
          }
        },
        "options": {
          "limitWaitTime": {
            "values": {
              "limitType": "afterTimeInterval",
              "resumeAmount": 2,
              "resumeUnit": "hours"
            }
          },
          "appendAttribution": false
        }
      },
      "webhookId": "2964dbc5-5bb7-4103-9671-d5fe1a77399e",
      "credentials": {
        "slackApi": {
          "id": "mv7TIi8kucFPqPg6",
          "name": "Slack account"
        }
      }
    },
    {
      "id": "78436346-56cd-4279-89b7-0cf216e3553b",
      "name": "Approved?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        2016,
        96
      ],
      "parameters": {
        "conditions": {
          "combinator": "and",
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "leftValue": "={{ $json.data && $json.data.approved }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ]
        },
        "looseTypeValidation": true,
        "options": {}
      }
    },
    {
      "id": "ddcfde67-e732-4b76-a0c9-b520f600dfe7",
      "name": "Build Salesforce Actions",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2240,
        96
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "// Approved: turn the plan into ordered Salesforce calls. Gap-fill PATCH runs before its merge.\n// Merges go to the DuplicateMergeService Apex endpoint (Database.merge); conversions to LeadConvertService (Database.convertLead).\n// Neither operation exists in Salesforce's standard REST API.\nconst p = $('Plan Consolidation').first().json;\nconst BASE = 'https://resilient-goat-2jzqdi-dev-ed.trailblaze.my.salesforce.com';\nconst out = [];\nfor (const m of p.merges) {\n  if (Object.keys(m.fill).length) out.push({ kind: 'gap-fill', email: m.email, method: 'PATCH', url: BASE + '/services/data/v60.0/sobjects/Contact/' + m.survivorId, body: m.fill, survivorId: m.survivorId, detail: 'copied ' + Object.keys(m.fill).join(', ') + ' to survivor' });\n  out.push({ kind: 'merge', email: m.email, method: 'POST', url: BASE + '/services/apexrest/mergeDuplicates/', survivorId: m.survivorId, duplicateIds: m.duplicates.map(d => d.id).join(','), reason: m.reason,\n    body: { objectType: 'Contact', masterId: m.survivorId, duplicateIds: m.duplicates.map(d => d.id) } });\n}\nfor (const c of p.conversions) out.push({ kind: 'lead-convert', email: c.email, method: 'POST', url: BASE + '/services/apexrest/convertLeadToContact/', survivorId: c.contactId, duplicateIds: c.leadId,\n  reason: 'open Lead shares email with existing Contact', body: { leadId: c.leadId, contactId: c.contactId, accountId: c.accountId, convertedStatus: 'Closed - Converted' } });\nreturn out.map(json => ({ json }));\n"
      }
    },
    {
      "id": "f6385926-e6bc-4f77-9cae-156ee5044e54",
      "name": "Run Salesforce Action",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2464,
        96
      ],
      "parameters": {
        "method": "={{ $json.method }}",
        "url": "={{ $json.url }}",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "salesforceOAuth2Api",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($json.body) }}",
        "options": {}
      },
      "onError": "continueRegularOutput",
      "credentials": {
        "salesforceOAuth2Api": {
          "id": "553Tae1kPe5Wpthq",
          "name": "Salesforce account"
        }
      }
    },
    {
      "id": "50207862-823c-4064-90ba-78bf729f7d84",
      "name": "Collect Results",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2688,
        96
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "// Pair each Salesforce call with its response. A failed call never stops the run; it is logged and verification will catch it.\nconst acts = $('Build Salesforce Actions').all().map(i => i.json);\nreturn $input.all().map((it, i) => {\n  const a = acts[i] || {}, r = it.json || {};\n  let ok, msg;\n  if (a.kind === 'merge' || a.kind === 'lead-convert') { ok = r.success === true; msg = r.message || (r.error && (r.error.description || r.error.message)) || ''; }\n  else { ok = !r.error; msg = r.error ? (r.error.description || r.error.message || '') : ''; }\n  return { json: { kind: a.kind, email: a.email, survivorId: a.survivorId, duplicateIds: a.duplicateIds || '', reason: a.reason || a.detail || '', ok, message: String(msg).slice(0, 300) } };\n});\n"
      }
    },
    {
      "id": "70bc89be-a4b7-40bf-be7e-c481ef4de298",
      "name": "Get Canonical Contacts",
      "type": "n8n-nodes-base.salesforce",
      "typeVersion": 1.1,
      "position": [
        2912,
        96
      ],
      "parameters": {
        "resource": "search",
        "operation": "query",
        "query": "=SELECT Id, FirstName, LastName, Email, Title, Phone, Account.Name FROM Contact WHERE Email IN ({{ $('Plan Consolidation').first().json.emails.map(e => \"'\" + e.replace(/'/g, \"\\\\'\") + \"'\").join(',') }})"
      },
      "executeOnce": true,
      "alwaysOutputData": true,
      "credentials": {
        "salesforceOAuth2Api": {
          "id": "553Tae1kPe5Wpthq",
          "name": "Salesforce account"
        }
      }
    },
    {
      "id": "d239286a-3c9d-4253-9a4a-840736c9c4ea",
      "name": "Build HubSpot Upsert",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3136,
        96
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "// Upsert each canonical Salesforce Contact into HubSpot by email (create-or-update; HubSpot already enforces one contact per email).\nconst emails = $('Plan Consolidation').first().json.emails;\nconst sf = $('Get Canonical Contacts').all().map(i => i.json).filter(c => c.Id);\nconst inputs = emails.map(e => sf.find(c => String(c.Email).toLowerCase() === e)).filter(Boolean).map(c => ({ idProperty: 'email', id: String(c.Email).toLowerCase(),\n  properties: Object.fromEntries(Object.entries({ email: String(c.Email).toLowerCase(), firstname: c.FirstName, lastname: c.LastName, jobtitle: c.Title, phone: c.Phone, company: (c.Account || {}).Name }).filter(([k, v]) => v)) }));\nreturn [{ json: { inputs } }];\n"
      },
      "executeOnce": true
    },
    {
      "id": "11a89a2f-1c1e-4ea5-aee4-439db7d6a6e0",
      "name": "Upsert to HubSpot",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        3360,
        96
      ],
      "parameters": {
        "method": "POST",
        "url": "https://api.hubapi.com/crm/v3/objects/contacts/batch/upsert",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "hubspotAppToken",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($json) }}",
        "options": {}
      },
      "onError": "continueRegularOutput",
      "credentials": {
        "hubspotAppToken": {
          "id": "ynUSCUtMOnX1f4Jl",
          "name": "HubSpot Service Key account"
        }
      }
    },
    {
      "id": "ade48f5c-f603-4730-b530-10f24d9d3a5f",
      "name": "Wait for HubSpot Index",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [
        3584,
        96
      ],
      "parameters": {
        "resume": "timeInterval",
        "amount": 10,
        "unit": "seconds"
      },
      "webhookId": "aebdfb59-2294-4cc4-9002-16a51e25f4c2"
    },
    {
      "id": "824d87a9-c7fb-48b7-9385-bd1d1aef372e",
      "name": "Verify Salesforce Contacts",
      "type": "n8n-nodes-base.salesforce",
      "typeVersion": 1.1,
      "position": [
        3808,
        96
      ],
      "parameters": {
        "resource": "search",
        "operation": "query",
        "query": "=SELECT Id, Email FROM Contact WHERE Email IN ({{ $('Plan Consolidation').first().json.emails.map(e => \"'\" + e.replace(/'/g, \"\\\\'\") + \"'\").join(',') }})"
      },
      "executeOnce": true,
      "alwaysOutputData": true,
      "credentials": {
        "salesforceOAuth2Api": {
          "id": "553Tae1kPe5Wpthq",
          "name": "Salesforce account"
        }
      }
    },
    {
      "id": "73031064-32a8-4454-a9b1-eb24105ef920",
      "name": "Verify Salesforce Leads",
      "type": "n8n-nodes-base.salesforce",
      "typeVersion": 1.1,
      "position": [
        4032,
        96
      ],
      "parameters": {
        "resource": "search",
        "operation": "query",
        "query": "=SELECT Id, Email FROM Lead WHERE IsConverted = false AND Email IN ({{ $('Plan Consolidation').first().json.emails.map(e => \"'\" + e.replace(/'/g, \"\\\\'\") + \"'\").join(',') }})"
      },
      "executeOnce": true,
      "alwaysOutputData": true,
      "credentials": {
        "salesforceOAuth2Api": {
          "id": "553Tae1kPe5Wpthq",
          "name": "Salesforce account"
        }
      }
    },
    {
      "id": "adb45e22-2889-42aa-8c52-d93efd7e5cdf",
      "name": "Verify HubSpot",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        4256,
        96
      ],
      "parameters": {
        "method": "POST",
        "url": "https://api.hubapi.com/crm/v3/objects/contacts/search",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "hubspotAppToken",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ filterGroups: [{ filters: [{ propertyName: 'email', operator: 'IN', values: $('Plan Consolidation').first().json.emails }] }], properties: ['email','firstname','lastname'], limit: 100 }) }}",
        "options": {}
      },
      "executeOnce": true,
      "credentials": {
        "hubspotAppToken": {
          "id": "ynUSCUtMOnX1f4Jl",
          "name": "HubSpot Service Key account"
        }
      }
    },
    {
      "id": "be9547b2-1eb4-4356-9de9-0875e013f137",
      "name": "Build Audit Rows",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        4480,
        96
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "// Verify: exactly one Salesforce Contact, zero open Leads and exactly one HubSpot contact per email. Build the audit rows and the Slack result.\nconst p = $('Plan Consolidation').first().json;\nconst res = $('Collect Results').all().map(i => i.json);\nconst sfC = $('Verify Salesforce Contacts').all().map(i => i.json).filter(c => c.Id);\nconst sfL = $('Verify Salesforce Leads').all().map(i => i.json).filter(l => l.Id);\nconst hs = ($('Verify HubSpot').first().json.results || []).map(r => r.properties || {});\nconst up = $('Upsert to HubSpot').first().json;\nconst now = $now.toISO();\nconst rows = res.map(r => ({ runAt: now, email: r.email, action: r.kind, survivorId: r.survivorId, duplicateIds: r.duplicateIds, survivorReason: r.reason, result: r.ok ? 'success' : 'failed', detail: r.message,\n  sfContactCount: null, sfOpenLeadCount: null, hubspotCount: null, verified: null }));\nconst checks = p.emails.map(e => {\n  const c = sfC.filter(x => String(x.Email).toLowerCase() === e).length, l = sfL.filter(x => String(x.Email).toLowerCase() === e).length, h = hs.filter(x => String(x.email).toLowerCase() === e).length;\n  return { email: e, c, l, h, ok: c === 1 && l === 0 && h === 1 };\n});\nfor (const k of checks) rows.push({ runAt: now, email: k.email, action: 'verify', survivorId: '', duplicateIds: '', survivorReason: '', result: k.ok ? 'verified' : 'NOT VERIFIED',\n  detail: 'Salesforce contacts ' + k.c + ', open leads ' + k.l + ', HubSpot contacts ' + k.h, sfContactCount: k.c, sfOpenLeadCount: k.l, hubspotCount: k.h, verified: k.ok });\nrows.push({ runAt: now, email: '(hubspot)', action: 'hubspot-upsert', survivorId: '', duplicateIds: '', survivorReason: '', result: up.status || (up.error ? 'failed' : 'success'),\n  detail: ((up.results || []).length) + ' records upserted (' + (up.results || []).filter(r => r.new).length + ' created)', sfContactCount: null, sfOpenLeadCount: null, hubspotCount: null, verified: null });\nreturn rows.map(json => ({ json }));\n"
      },
      "executeOnce": true
    },
    {
      "id": "be3bd40d-cfe6-45fc-983a-b328d4f98cb6",
      "name": "Log to Audit Table",
      "type": "n8n-nodes-base.dataTable",
      "typeVersion": 1.1,
      "position": [
        4704,
        0
      ],
      "parameters": {
        "resource": "row",
        "operation": "insert",
        "dataTableId": {
          "__rl": true,
          "mode": "id",
          "value": "2a5vewCjYZTcGdtv",
          "cachedResultName": "Dedupe Audit Log"
        },
        "columns": {
          "mappingMode": "autoMapInputData",
          "value": {}
        }
      }
    },
    {
      "id": "681026a3-9c6b-4f1a-bc80-af7ce244be03",
      "name": "Build Result Post",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        4704,
        192
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const rows = $('Build Audit Rows').all().map(i => i.json);\nconst acts = rows.filter(r => !['verify', 'hubspot-upsert'].includes(r.action)), checks = rows.filter(r => r.action === 'verify'), hs = rows.find(r => r.action === 'hubspot-upsert');\nconst allOk = checks.every(k => k.verified) && acts.every(r => r.result === 'success');\nconst icon = { merge: ':link:', 'lead-convert': ':arrows_counterclockwise:', 'gap-fill': ':pencil2:' };\nconst text = (allOk ? ':white_check_mark: *Duplicate Consolidation: done and verified*' : ':rotating_light: *Duplicate Consolidation: verification FAILED, check the Dedupe Audit Log*') + '\\n' +\n  acts.map(r => (icon[r.action] || '•') + ' ' + r.action + ' `' + r.email + '`: ' + (r.result === 'success' ? 'success' : 'failed: ' + r.detail)).join('\\n') +\n  '\\n:arrows_clockwise: HubSpot upsert by email: ' + (hs ? hs.detail : 'n/a') +\n  '\\n\\n*Verification (one record per email in each system):*\\n' +\n  checks.map(k => (k.verified ? ':large_green_circle:' : ':red_circle:') + ' `' + k.email + '`: ' + k.detail).join('\\n');\nreturn [{ json: { slackText: text } }];\n"
      },
      "executeOnce": true
    },
    {
      "id": "ef7ad023-6fad-4547-8a62-b4974b7bee86",
      "name": "Post Result to Slack",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.7,
      "position": [
        4928,
        192
      ],
      "parameters": {
        "resource": "message",
        "operation": "post",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "id",
          "value": "C0BQ3G96HL6",
          "cachedResultName": "general"
        },
        "text": "={{ $json.slackText }}",
        "otherOptions": {
          "includeLinkToWorkflow": false
        }
      },
      "webhookId": "2cc15d9b-2245-40e9-84a9-f9f24f44a011",
      "credentials": {
        "slackApi": {
          "id": "mv7TIi8kucFPqPg6",
          "name": "Slack account"
        }
      }
    }
  ],
  "connections": {
    "Weekly Dedupe Scan": {
      "main": [
        [
          {
            "node": "Get Contacts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Contacts": {
      "main": [
        [
          {
            "node": "Get Open Leads",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Open Leads": {
      "main": [
        [
          {
            "node": "Get Opportunity Roles",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Opportunity Roles": {
      "main": [
        [
          {
            "node": "Get Cases",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Cases": {
      "main": [
        [
          {
            "node": "Get Tasks",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Tasks": {
      "main": [
        [
          {
            "node": "Get Events",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Events": {
      "main": [
        [
          {
            "node": "Plan Consolidation",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Plan Consolidation": {
      "main": [
        [
          {
            "node": "Ask Approval in Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Ask Approval in Slack": {
      "main": [
        [
          {
            "node": "Approved?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Approved?": {
      "main": [
        [
          {
            "node": "Build Salesforce Actions",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Salesforce Actions": {
      "main": [
        [
          {
            "node": "Run Salesforce Action",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Run Salesforce Action": {
      "main": [
        [
          {
            "node": "Collect Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Collect Results": {
      "main": [
        [
          {
            "node": "Get Canonical Contacts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Canonical Contacts": {
      "main": [
        [
          {
            "node": "Build HubSpot Upsert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build HubSpot Upsert": {
      "main": [
        [
          {
            "node": "Upsert to HubSpot",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upsert to HubSpot": {
      "main": [
        [
          {
            "node": "Wait for HubSpot Index",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait for HubSpot Index": {
      "main": [
        [
          {
            "node": "Verify Salesforce Contacts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Verify Salesforce Contacts": {
      "main": [
        [
          {
            "node": "Verify Salesforce Leads",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Verify Salesforce Leads": {
      "main": [
        [
          {
            "node": "Verify HubSpot",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Verify HubSpot": {
      "main": [
        [
          {
            "node": "Build Audit Rows",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Audit Rows": {
      "main": [
        [
          {
            "node": "Log to Audit Table",
            "type": "main",
            "index": 0
          },
          {
            "node": "Build Result Post",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Result Post": {
      "main": [
        [
          {
            "node": "Post Result to Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "pinData": {},
  "meta": {
    "templateCredsSetupCompleted": false
  }
}