{
  "name": "Fixing MAP-CRM Discrepancies",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            { "triggerAtHour": 7 }
          ]
        }
      },
      "id": "5c1f46c8-a188-4903-8d40-db9d499d70c7",
      "name": "Daily Sentinel Run",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.3,
      "position": [208, 464]
    },
    {
      "parameters": {
        "authentication": "appToken",
        "operation": "getAll",
        "returnAll": true,
        "additionalFields": {
          "propertiesCollection": {
            "propertiesValues": {
              "properties": ["email", "firstname", "lastname", "lifecyclestage", "company"],
              "propertyMode": "valueOnly"
            }
          }
        }
      },
      "id": "c9d9926b-8d36-46b2-95ae-0e00752de0c4",
      "name": "Get HubSpot Contacts",
      "type": "n8n-nodes-base.hubspot",
      "typeVersion": 2.2,
      "position": [432, 464],
      "credentials": {
        "hubspotAppToken": { "name": "HubSpot Service Key account" }
      }
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            { "id": "email", "name": "email", "type": "string", "value": "={{ $json.properties.email.value }}" },
            { "id": "firstName", "name": "firstName", "type": "string", "value": "={{ $json.properties.firstname.value }}" },
            { "id": "lastName", "name": "lastName", "type": "string", "value": "={{ $json.properties.lastname.value }}" },
            { "id": "hsLifecycleStage", "name": "hsLifecycleStage", "type": "string", "value": "={{ $json.properties.lifecyclestage.value }}" },
            { "id": "companyName", "name": "companyName", "type": "string", "value": "={{ $json.properties.company.value }}" },
            { "id": "domain", "name": "domain", "type": "string", "value": "={{ $json.properties.email.value.split(\"@\")[1] }}" },
            { "id": "contactId", "name": "contactId", "type": "string", "value": "={{ $json.vid }}" }
          ]
        },
        "options": {}
      },
      "id": "816bdc5b-7a72-4705-b699-73d457008d5d",
      "name": "Extract Contact Fields",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.5,
      "position": [656, 464]
    },
    {
      "parameters": {
        "resource": "search",
        "query": "=SELECT Id, Name, Website, (SELECT Id, Name, StageName, Amount, CloseDate FROM Opportunities ORDER BY CloseDate DESC) FROM Account WHERE Website LIKE '%{{ $json.domain }}%' LIMIT 1"
      },
      "id": "c6f7284e-4263-4779-8f5e-d3c973f8859b",
      "name": "Find Matching Salesforce Account",
      "type": "n8n-nodes-base.salesforce",
      "typeVersion": 1.1,
      "position": [880, 464],
      "credentials": {
        "salesforceOAuth2Api": { "name": "Salesforce account" }
      }
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const contact = $('Extract Contact Fields').item.json;\nconst sf = $json;\nconst oppRecords = (sf.Opportunities && sf.Opportunities.records) ? sf.Opportunities.records : [];\nconst hasClosedWon = oppRecords.some(function(o) { return o.StageName === 'Closed Won'; });\nconst hasAnyOpp = oppRecords.length > 0;\nconst hsStage = (contact.hsLifecycleStage || '').toLowerCase();\n\nlet mismatch = false;\nlet expectedStage = hsStage;\nlet reason = '';\n\nif (hsStage !== 'customer' && hasClosedWon) {\n  mismatch = true;\n  expectedStage = 'customer';\n  const won = oppRecords.filter(function(o) { return o.StageName === 'Closed Won'; })[0];\n  reason = 'Salesforce shows a Closed Won opportunity (\"' + won.Name + '\", closed ' + won.CloseDate + ') on the ' + sf.Name + ' account, but HubSpot still lists this contact at lifecycle stage \"' + contact.hsLifecycleStage + '\".';\n} else if (hsStage === 'customer' && !hasClosedWon) {\n  mismatch = true;\n  expectedStage = 'opportunity';\n  if (hasAnyOpp) {\n    const latest = oppRecords[0];\n    reason = 'HubSpot marks this contact as \"Customer\", but Salesforce shows no Closed Won opportunity for ' + sf.Name + ' — the most recent opportunity (\"' + latest.Name + '\") is in stage \"' + latest.StageName + '\".';\n  } else {\n    reason = 'HubSpot marks this contact as \"Customer\", but there is no opportunity at all on the matching Salesforce account (' + sf.Name + ').';\n  }\n}\n\nreturn {\n  json: {\n    email: contact.email,\n    firstName: contact.firstName,\n    lastName: contact.lastName,\n    companyName: contact.companyName,\n    contactId: contact.contactId,\n    hsLifecycleStage: contact.hsLifecycleStage,\n    sfAccountId: sf.Id,\n    sfAccountName: sf.Name,\n    opportunityCount: oppRecords.length,\n    hasClosedWon: hasClosedWon,\n    mismatch: mismatch,\n    expectedStage: expectedStage,\n    reason: reason\n  }\n};"
      },
      "id": "470e9b45-1f0e-4a6c-bfc8-c53363231455",
      "name": "Compare Lifecycle vs Salesforce Reality",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [1104, 464]
    },
    {
      "parameters": {
        "conditions": {
          "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 1 },
          "conditions": [
            { "leftValue": "={{ $json.mismatch }}", "operator": { "type": "boolean", "operation": "equals" }, "rightValue": true }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "53227946-20e8-438a-b92a-2ca7dfe96ea8",
      "name": "Only Discrepancies",
      "type": "n8n-nodes-base.filter",
      "typeVersion": 2.3,
      "position": [1328, 464]
    },
    {
      "parameters": {
        "options": {}
      },
      "id": "70cabb60-25e3-4ab4-af4b-c8dbee200dfe",
      "name": "Process One At A Time",
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 3,
      "position": [1552, 464]
    },
    {
      "parameters": {
        "promptType": "define",
        "text": "=A HubSpot lifecycle stage looks out of sync with Salesforce reality for Fortavault, Inc. (a fictional data protection and security platform).\n\nContact: {{ $json.firstName }} {{ $json.lastName }} ({{ $json.email }})\nCompany: {{ $json.companyName }} (matched Salesforce account: {{ $json.sfAccountName }})\nCurrent HubSpot lifecycle stage: {{ $json.hsLifecycleStage }}\nProposed corrected stage: {{ $json.expectedStage }}\nSalesforce facts: {{ $json.reason }}\n\nWrite a short (2-3 sentence) plain-English explanation for a Slack message aimed at the RevOps/marketing ops team, stating what is wrong and why the proposed stage is correct. Do not repeat the raw field names verbatim like a report - write it like a colleague flagging the issue.",
        "options": {
          "systemMessage": "You are a RevOps data-quality assistant for Fortavault, Inc., a fictional data protection and security platform (cloud backup, ransomware recovery, zero-trust access, compliance reporting). You review discrepancies between HubSpot lifecycle stages and Salesforce deal reality (surfaced by an automated sentinel) and write a short, specific, plain-English explanation for the team reviewing the flag in Slack. Be concrete and reference the actual facts given - never invent details not provided."
        }
      },
      "id": "a4c42708-c5d1-463c-a447-186012c29efd",
      "name": "Draft Discrepancy Explanation",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "typeVersion": 3.1,
      "position": [1776, 208]
    },
    {
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "claude-sonnet-4-6",
          "cachedResultName": "Claude Sonnet 4.6"
        },
        "options": { "temperature": 0.3 }
      },
      "id": "67323031-49b7-49c6-bfb2-756338eed5f2",
      "name": "Anthropic Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
      "typeVersion": 1.5,
      "position": [1848, 440],
      "credentials": {
        "anthropicApi": { "name": "Anthropic account" }
      }
    },
    {
      "parameters": {
        "operation": "sendAndWait",
        "select": "channel",
        "channelId": { "__rl": true, "mode": "id", "value": "C0BQ3G96HL6", "cachedResultName": "general" },
        "message": "=🔄 Lifecycle Stage Sentinel found a mismatch\n\n{{ $('Compare Lifecycle vs Salesforce Reality').item.json.firstName }} {{ $('Compare Lifecycle vs Salesforce Reality').item.json.lastName }} — {{ $('Compare Lifecycle vs Salesforce Reality').item.json.companyName }}\nHubSpot: {{ $('Compare Lifecycle vs Salesforce Reality').item.json.hsLifecycleStage }} → Proposed: {{ $('Compare Lifecycle vs Salesforce Reality').item.json.expectedStage }}\n\n{{ $json.output }}",
        "approvalOptions": {
          "values": { "approvalType": "double", "approveLabel": "Apply Fix", "disapproveLabel": "Skip" }
        },
        "options": {}
      },
      "id": "6b07d6fd-bc31-48a2-8fd8-fef50a2d62da",
      "name": "Request Approval",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.7,
      "position": [2128, 208],
      "credentials": {
        "slackApi": { "name": "Slack account" }
      }
    },
    {
      "parameters": {
        "conditions": {
          "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "loose", "version": 1 },
          "conditions": [
            { "leftValue": "={{ $json.data.approved }}", "operator": { "type": "boolean", "operation": "equals" }, "rightValue": true }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "ee0a6e2b-add6-4cab-8de5-241e49723054",
      "name": "Approved?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [2352, 208]
    },
    {
      "parameters": {
        "method": "PATCH",
        "url": "=https://api.hubapi.com/crm/v3/objects/contacts/{{ $('Compare Lifecycle vs Salesforce Reality').item.json.contactId }}",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "hubspotAppToken",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "{\n  \"properties\": {\n    \"lifecyclestage\": \"\"\n  }\n}",
        "options": {}
      },
      "id": "0e995876-dbed-467d-8b0f-efb83a971b09",
      "name": "Clear Lifecycle Stage",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.5,
      "position": [2576, 208],
      "credentials": {
        "hubspotAppToken": { "name": "HubSpot Service Key account" }
      }
    },
    {
      "parameters": {
        "method": "PATCH",
        "url": "=https://api.hubapi.com/crm/v3/objects/contacts/{{ $('Compare Lifecycle vs Salesforce Reality').item.json.contactId }}",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "hubspotAppToken",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"properties\": {\n    \"lifecyclestage\": \"{{ $('Compare Lifecycle vs Salesforce Reality').item.json.expectedStage }}\"\n  }\n}",
        "options": {}
      },
      "id": "094278af-c723-4cc2-983b-28798cfb746e",
      "name": "Update Lifecycle Stage",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.5,
      "position": [2800, 208],
      "credentials": {
        "hubspotAppToken": { "name": "HubSpot Service Key account" }
      }
    },
    {
      "parameters": {
        "dataTableId": { "__rl": true, "mode": "id", "value": "53xNldUA8FM9Xv39", "cachedResultName": "MAP-CRM Lifecycle Corrections" },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "contactEmail": "={{ $('Compare Lifecycle vs Salesforce Reality').item.json.email }}",
            "companyName": "={{ $('Compare Lifecycle vs Salesforce Reality').item.json.companyName }}",
            "previousStage": "={{ $('Compare Lifecycle vs Salesforce Reality').item.json.hsLifecycleStage }}",
            "correctedStage": "={{ $('Compare Lifecycle vs Salesforce Reality').item.json.expectedStage }}",
            "reason": "={{ $('Compare Lifecycle vs Salesforce Reality').item.json.reason }}",
            "sfAccountId": "={{ $('Compare Lifecycle vs Salesforce Reality').item.json.sfAccountId }}",
            "sfAccountName": "={{ $('Compare Lifecycle vs Salesforce Reality').item.json.sfAccountName }}",
            "correctedAt": "={{ $now.toISO() }}"
          }
        },
        "options": {}
      },
      "id": "7b42a02e-2e63-41bb-8b73-75a2336a06c3",
      "name": "Log Correction",
      "type": "n8n-nodes-base.dataTable",
      "typeVersion": 1.1,
      "position": [3024, 336]
    }
  ],
  "connections": {
    "Daily Sentinel Run": {
      "main": [[{ "node": "Get HubSpot Contacts", "type": "main", "index": 0 }]]
    },
    "Get HubSpot Contacts": {
      "main": [[{ "node": "Extract Contact Fields", "type": "main", "index": 0 }]]
    },
    "Extract Contact Fields": {
      "main": [[{ "node": "Find Matching Salesforce Account", "type": "main", "index": 0 }]]
    },
    "Find Matching Salesforce Account": {
      "main": [[{ "node": "Compare Lifecycle vs Salesforce Reality", "type": "main", "index": 0 }]]
    },
    "Compare Lifecycle vs Salesforce Reality": {
      "main": [[{ "node": "Only Discrepancies", "type": "main", "index": 0 }]]
    },
    "Only Discrepancies": {
      "main": [[{ "node": "Process One At A Time", "type": "main", "index": 0 }]]
    },
    "Process One At A Time": {
      "main": [null, [{ "node": "Draft Discrepancy Explanation", "type": "main", "index": 0 }]]
    },
    "Draft Discrepancy Explanation": {
      "main": [[{ "node": "Request Approval", "type": "main", "index": 0 }]]
    },
    "Anthropic Chat Model": {
      "ai_languageModel": [[{ "node": "Draft Discrepancy Explanation", "type": "ai_languageModel", "index": 0 }]]
    },
    "Request Approval": {
      "main": [[{ "node": "Approved?", "type": "main", "index": 0 }]]
    },
    "Approved?": {
      "main": [
        [{ "node": "Clear Lifecycle Stage", "type": "main", "index": 0 }],
        [{ "node": "Process One At A Time", "type": "main", "index": 0 }]
      ]
    },
    "Clear Lifecycle Stage": {
      "main": [[{ "node": "Update Lifecycle Stage", "type": "main", "index": 0 }]]
    },
    "Update Lifecycle Stage": {
      "main": [[{ "node": "Log Correction", "type": "main", "index": 0 }]]
    },
    "Log Correction": {
      "main": [[{ "node": "Process One At A Time", "type": "main", "index": 0 }]]
    }
  },
  "settings": {
    "executionOrder": "v1"
  }
}
