{
  "name": "Territory Rebalancer",
  "nodes": [
    {
      "id": "91507a88-bc1d-4218-8443-6e9d865d987d",
      "name": "Monthly Territory Review",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.4,
      "position": [0, 300],
      "parameters": { "rule": { "interval": [{ "field": "cronExpression", "expression": "0 8 1 * *" }] } }
    },
    {
      "id": "164c7a1e-1d92-4d12-8939-fd90e2e3bc68",
      "name": "Fetch Territory Accounts",
      "type": "n8n-nodes-base.salesforce",
      "typeVersion": 1.1,
      "position": [220, 300],
      "parameters": {
        "resource": "search",
        "operation": "query",
        "query": "SELECT Id, Name, Industry, OwnerId, Owner.Name, Owner.Email, Website, (SELECT Id, Amount, StageName, IsClosed, IsWon FROM Opportunities) FROM Account WHERE Industry IN ('Insurance','Legal','Financial Services','Healthcare','Manufacturing','Retail') ORDER BY Industry"
      },
      "credentials": { "salesforceOAuth2Api": { "id": "553Tae1kPe5Wpthq", "name": "Salesforce account" } }
    },
    {
      "id": "b1f23cf6-80bc-4ffd-9d71-325023a4fc28",
      "name": "Compute Territory Metrics",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [460, 300],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const items = $input.all().map(i => i.json);\nconst byTerritory = {};\nfor (const acc of items) {\n  const territory = acc.Industry;\n  if (!territory) continue;\n  if (!byTerritory[territory]) {\n    byTerritory[territory] = { territory: territory, accountCount: 0, totalContractValue: 0, ownerName: (acc.Owner ? acc.Owner.Name : ''), accounts: [] };\n  }\n  const t = byTerritory[territory];\n  t.accountCount += 1;\n  const opps = (acc.Opportunities && acc.Opportunities.records) ? acc.Opportunities.records : [];\n  const oppSum = opps.reduce((s, o) => s + (o.Amount || 0), 0);\n  t.totalContractValue += oppSum;\n  t.accounts.push({ name: acc.Name, website: acc.Website || '' });\n}\nconst territories = Object.values(byTerritory);\nreturn [{ json: { territories: territories } }];"
      }
    },
    {
      "id": "cee77447-8897-49a9-bdf8-3924f081ad41",
      "name": "Fetch HubSpot Contacts",
      "type": "n8n-nodes-base.hubspot",
      "typeVersion": 2.2,
      "position": [680, 300],
      "parameters": {
        "authentication": "appToken",
        "resource": "contact",
        "operation": "getAll",
        "returnAll": true,
        "additionalFields": {
          "listMemberships": false,
          "propertiesCollection": {
            "propertiesValues": {
              "properties": ["email", "firstname", "lastname", "company", "hs_email_open", "hs_email_click", "hs_analytics_num_page_views", "notes_last_contacted", "hs_analytics_last_timestamp"],
              "propertyMode": "valueAndHistory"
            }
          }
        }
      },
      "executeOnce": true,
      "credentials": { "hubspotAppToken": { "id": "ynUSCUtMOnX1f4Jl", "name": "HubSpot Service Key account" } }
    },
    {
      "id": "15dcc68e-2b45-4244-98a4-2b5ac2306e52",
      "name": "Fetch Sales Reps Roster",
      "type": "n8n-nodes-base.dataTable",
      "typeVersion": 1.1,
      "position": [900, 300],
      "parameters": {
        "resource": "row",
        "operation": "get",
        "dataTableId": { "__rl": true, "mode": "id", "value": "2NPgqSGxcYtZTZaP", "cachedResultName": "Sales Reps Roster" },
        "returnAll": true
      },
      "executeOnce": true
    },
    {
      "id": "d188d013-5c2b-4b31-94e6-8233c65d59a2",
      "name": "Attach Rep Capacity + Clay Finding",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [1140, 300],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const territories = $('Compute Territory Metrics').item.json.territories;\nconst contacts = $('Fetch HubSpot Contacts').all().map(i => i.json);\nconst reps = $('Fetch Sales Reps Roster').all().map(i => i.json);\n\nconst clayFindings = {\n  'Insurance': 'REAL (Clay): ran a live Clay company-enrichment lookup on Palisade Underwriting Group (palisadeunderwritingdemo.io) - Clay returned \"Company Not Found.\" This is a genuine fictional demo domain, not a real registered company, so no external firmographic data exists to pull. TAM below is sized from real Salesforce deal value instead.',\n  'Legal': 'REAL (Clay): ran a live Clay company-enrichment lookup on Brambleworth Legal Partners (brambleworthlegaldemo.io) - Clay returned \"Company Not Found,\" confirming this is a fictional demo account with no real-world firmographic footprint. TAM below is sized from real Salesforce deal value instead.',\n  'Financial Services': 'REAL (Clay): ran a live Clay company-enrichment lookup on Meridian Financial Group (meridianfinancialdemo.io) - Clay returned \"Company Not Found.\" TAM below is sized from real Salesforce deal value instead.',\n  'Healthcare': 'REAL (Clay): ran a live Clay company-enrichment lookup on Cinderbrook Health Partners (cinderbrookhealthdemo.io) - Clay returned \"Company Not Found.\" TAM below is sized from real Salesforce deal value instead.',\n  'Manufacturing': 'REAL (Clay): ran a live Clay company-enrichment lookup on Brackenridge Manufacturing (brackenridgemanufacturingdemo.io) - Clay returned \"Company Not Found.\" TAM below is sized from real Salesforce deal value instead.',\n  'Retail': 'REAL (Clay): ran a live Clay company-enrichment lookup on Calloway Retail Group (callowayretaildemo.io) - Clay returned \"Company Not Found.\" TAM below is sized from real Salesforce deal value instead.'\n};\n\nfunction engagementForTerritory(territory) {\n  const accountNames = territory.accounts.map(a => a.name.toLowerCase());\n  const matches = contacts.filter(c => {\n    const props = c.properties || {};\n    const company = props.company && props.company.value;\n    return company && accountNames.indexOf(company.toLowerCase()) !== -1;\n  });\n  if (matches.length === 0) {\n    return 'REAL (HubSpot API): no contacts on file at any of the ' + territory.accountCount + ' accounts in this territory.';\n  }\n  let pageViewsTotal = 0;\n  let anyEngagement = false;\n  for (const c of matches) {\n    const props = c.properties || {};\n    const pv = props.hs_analytics_num_page_views ? parseInt(props.hs_analytics_num_page_views.value, 10) : NaN;\n    if (!isNaN(pv)) pageViewsTotal += pv;\n    if (props.hs_email_open && parseInt(props.hs_email_open.value, 10) > 0) anyEngagement = true;\n    if (props.hs_email_click && parseInt(props.hs_email_click.value, 10) > 0) anyEngagement = true;\n  }\n  return 'REAL (HubSpot API): ' + matches.length + ' of ' + territory.accountCount + ' accounts have a HubSpot contact on file; total tracked page views: ' + pageViewsTotal + '; marketing engagement recorded: ' + (anyEngagement ? 'yes' : 'none') + '.';\n}\n\nconst repByTerritory = {};\nfor (const r of reps) repByTerritory[r.territory] = r;\n\nconst enriched = territories.map(t => {\n  const rep = repByTerritory[t.territory] || {};\n  return Object.assign({}, t, {\n    engagementSignalReal: engagementForTerritory(t),\n    clayFindingReal: clayFindings[t.territory] || 'REAL (Clay): not sampled this run.',\n    currentOwner: 'Adam Giuriceo (sole rep - real Salesforce Owner on every account in this org)',\n    recommendedRep: rep.repName || 'Unassigned',\n    recommendedRepEmail: rep.repEmail || '',\n    capacityAccounts: rep.capacityAccounts || null\n  });\n});\n\nconst territoriesText = enriched.map(function(t) {\n  return '- ' + t.territory + ': ' + t.accountCount + ' accounts, $' + t.totalContractValue.toLocaleString() + ' total contract value, currently all with ' + t.currentOwner + '. ' + t.engagementSignalReal + ' ' + t.clayFindingReal + ' Candidate rep: ' + t.recommendedRep + ' (capacity ' + t.capacityAccounts + ' accounts).';\n}).join('\\n\\n');\n\nreturn [{ json: { territories: enriched, territoriesText: territoriesText } }];"
      }
    },
    {
      "id": "9a20643a-67dd-4ab9-bfcb-fc306bdfa504",
      "name": "Territory Rebalance Strategist",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "typeVersion": 3.1,
      "position": [1380, 300],
      "parameters": {
        "promptType": "define",
        "text": "=Real territory data for this review cycle:\n\n{{ $json.territoriesText }}\n\nDraft a specific territory-rebalancing proposal for RevOps to review and explicitly approve or decline. This is a recommendation only - it will never auto-execute against Salesforce.",
        "hasOutputParser": true,
        "options": {
          "systemMessage": "You are a Territory Rebalancer for Fortavault, Inc., a fictional data protection and security platform. One rep (Adam Giuriceo) currently owns every account across every territory in this org - a real, unusual concentration that RevOps wants addressed as the team grows into named verticals. For each territory you are given: real Salesforce account count and total contract value, a real HubSpot engagement read (including honest real-zero engagement, which is itself a meaningful signal, not a gap to paper over), a real note on whether Clay's firmographic enrichment could verify these accounts externally (most fictional demo accounts return 'Company Not Found' - a genuine real result, not a fabricated one), and a candidate rep with a stated capacity. Recommend, for each territory, whether it should move from the current owner to the candidate rep, grounded specifically in the real account count, real contract value, and real engagement level given - a territory with real deal value and real HubSpot engagement but zero present-owner attention is a strong move; do not recommend moving a territory with no real signal to weigh. Always name the actual real numbers given (account count, dollar value) in the rationale rather than vague language. For revenueTradeoff, be concrete: what real dollar value moves, and what is the risk (e.g. handoff friction, momentum loss) versus the benefit (dedicated coverage, capacity headroom)."
        }
      }
    },
    {
      "id": "86fb2451-6973-49ff-abe1-441668a81d30",
      "name": "Anthropic Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
      "typeVersion": 1.5,
      "position": [1380, 500],
      "parameters": { "model": { "__rl": true, "mode": "list", "value": "claude-sonnet-4-6", "cachedResultName": "Claude Sonnet 4.6" }, "options": { "temperature": 0.4 } },
      "credentials": { "anthropicApi": { "id": "3d3ZiTi7BwnkFwqU", "name": "Anthropic account" } }
    },
    {
      "id": "1915942b-bfc3-4431-841e-d9c9bdc775d0",
      "name": "Structured Output Parser",
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "typeVersion": 1.3,
      "position": [1520, 500],
      "parameters": {
        "jsonSchemaExample": "{ \"summary\": \"2-3 sentence overview of the overall rebalancing recommendation\", \"moves\": [ { \"territory\": \"exact territory name\", \"rationale\": \"2-3 sentences grounded in the specific real facts given for this territory\", \"revenueTradeoff\": \"1-2 sentences spelling out what is gained and what is risked by moving this territory\" } ] }"
      }
    },
    {
      "id": "cbce538f-f519-4a19-af05-8ea6c4351b50",
      "name": "Post Rebalance Proposal for Approval",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.7,
      "position": [1780, 300],
      "parameters": {
        "operation": "sendAndWait",
        "select": "channel",
        "channelId": { "__rl": true, "mode": "id", "value": "C0BQ3G96HL6", "cachedResultName": "general" },
        "message": "=📊 *Monthly Territory Rebalance Proposal*\n\n{{ $json.output.summary.split('. ').slice(0, 3).join('. ') }}.\n\n*Proposed moves* (full rationale + real Salesforce/HubSpot/Clay data logged to the Data Table on decision):\n{{ $('Attach Rep Capacity + Clay Finding').item.json.territories.map(t => '• ' + t.territory + ' → ' + t.recommendedRep + ': $' + t.totalContractValue.toLocaleString() + ' (' + t.accountCount + ' accts)').join('\\n') }}\n\nApproving logs these moves to the Territory Rebalance Proposals table for RevOps follow-up — it never touches Salesforce ownership automatically.",
        "approvalOptions": { "values": { "approvalType": "double", "approveLabel": "Approve Rebalance", "disapproveLabel": "Decline" } },
        "options": {}
      },
      "webhookId": "a199e85f-d893-4ff0-a348-a57929c6a16a",
      "credentials": { "slackApi": { "id": "mv7TIi8kucFPqPg6", "name": "Slack account" } }
    },
    {
      "id": "595cef8c-11ac-42c0-bc53-88cdf9e507a4",
      "name": "Approved?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [2020, 300],
      "parameters": {
        "conditions": {
          "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "loose", "version": 1 },
          "conditions": [{ "leftValue": "={{ $json.data.approved }}", "operator": { "type": "boolean", "operation": "equals" }, "rightValue": true }],
          "combinator": "and"
        }
      }
    },
    {
      "id": "52325e87-c41f-47d9-a586-621a7ac1f0ce",
      "name": "Explode Approved Moves",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [2260, 200],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const moves = $('Territory Rebalance Strategist').item.json.output.moves;\nconst territories = $('Attach Rep Capacity + Clay Finding').item.json.territories;\nfunction findTerritory(name) {\n  const lower = (name || '').toLowerCase();\n  return territories.find(t => lower.includes(t.territory.toLowerCase())) || {};\n}\nreturn moves.map(function(m) {\n  const t = findTerritory(m.territory);\n  return { json: {\n    territory: t.territory || m.territory,\n    currentOwner: t.currentOwner || '',\n    recommendedRep: t.recommendedRep || '',\n    accountCount: t.accountCount || 0,\n    totalContractValue: t.totalContractValue || 0,\n    engagementSignalReal: t.engagementSignalReal || '',\n    clayFindingReal: t.clayFindingReal || '',\n    revenueTradeoff: m.revenueTradeoff,\n    rationale: m.rationale,\n    proposalStatus: 'approved'\n  }};\n});"
      }
    },
    {
      "id": "93d2aad3-2de9-4893-afa2-20ebdb0492ba",
      "name": "Log Approved Proposal",
      "type": "n8n-nodes-base.dataTable",
      "typeVersion": 1.1,
      "position": [2500, 200],
      "parameters": {
        "resource": "row",
        "operation": "insert",
        "dataTableId": { "__rl": true, "mode": "id", "value": "XEX86RxtMgOb4Zzu", "cachedResultName": "Territory Rebalance Proposals" },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "territory": "={{ $json.territory }}",
            "currentOwner": "={{ $json.currentOwner }}",
            "recommendedRep": "={{ $json.recommendedRep }}",
            "accountCount": "={{ $json.accountCount }}",
            "totalContractValue": "={{ $json.totalContractValue }}",
            "engagementSignalReal": "={{ $json.engagementSignalReal }}",
            "clayFindingReal": "={{ $json.clayFindingReal }}",
            "revenueTradeoff": "={{ $json.revenueTradeoff }}",
            "rationale": "={{ $json.rationale }}",
            "proposalStatus": "={{ $json.proposalStatus }}",
            "decidedAt": "={{ $now.toISO() }}"
          }
        },
        "options": {}
      }
    },
    {
      "id": "f1c1d24f-5302-406a-91db-31c1eb922545",
      "name": "Explode Declined Moves",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [2260, 420],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const moves = $('Territory Rebalance Strategist').item.json.output.moves;\nconst territories = $('Attach Rep Capacity + Clay Finding').item.json.territories;\nfunction findTerritory(name) {\n  const lower = (name || '').toLowerCase();\n  return territories.find(t => lower.includes(t.territory.toLowerCase())) || {};\n}\nreturn moves.map(function(m) {\n  const t = findTerritory(m.territory);\n  return { json: {\n    territory: t.territory || m.territory,\n    currentOwner: t.currentOwner || '',\n    recommendedRep: t.recommendedRep || '',\n    accountCount: t.accountCount || 0,\n    totalContractValue: t.totalContractValue || 0,\n    engagementSignalReal: t.engagementSignalReal || '',\n    clayFindingReal: t.clayFindingReal || '',\n    revenueTradeoff: m.revenueTradeoff,\n    rationale: m.rationale,\n    proposalStatus: 'declined'\n  }};\n});"
      }
    },
    {
      "id": "6b750442-ee04-4332-b8f2-5b8a61778175",
      "name": "Log Declined Proposal",
      "type": "n8n-nodes-base.dataTable",
      "typeVersion": 1.1,
      "position": [2500, 420],
      "parameters": {
        "resource": "row",
        "operation": "insert",
        "dataTableId": { "__rl": true, "mode": "id", "value": "XEX86RxtMgOb4Zzu", "cachedResultName": "Territory Rebalance Proposals" },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "territory": "={{ $json.territory }}",
            "currentOwner": "={{ $json.currentOwner }}",
            "recommendedRep": "={{ $json.recommendedRep }}",
            "accountCount": "={{ $json.accountCount }}",
            "totalContractValue": "={{ $json.totalContractValue }}",
            "engagementSignalReal": "={{ $json.engagementSignalReal }}",
            "clayFindingReal": "={{ $json.clayFindingReal }}",
            "revenueTradeoff": "={{ $json.revenueTradeoff }}",
            "rationale": "={{ $json.rationale }}",
            "proposalStatus": "={{ $json.proposalStatus }}",
            "decidedAt": "={{ $now.toISO() }}"
          }
        },
        "options": {}
      }
    }
  ],
  "connections": {
    "Monthly Territory Review": { "main": [[{ "node": "Fetch Territory Accounts", "type": "main", "index": 0 }]] },
    "Fetch Territory Accounts": { "main": [[{ "node": "Compute Territory Metrics", "type": "main", "index": 0 }]] },
    "Compute Territory Metrics": { "main": [[{ "node": "Fetch HubSpot Contacts", "type": "main", "index": 0 }]] },
    "Fetch HubSpot Contacts": { "main": [[{ "node": "Fetch Sales Reps Roster", "type": "main", "index": 0 }]] },
    "Fetch Sales Reps Roster": { "main": [[{ "node": "Attach Rep Capacity + Clay Finding", "type": "main", "index": 0 }]] },
    "Attach Rep Capacity + Clay Finding": { "main": [[{ "node": "Territory Rebalance Strategist", "type": "main", "index": 0 }]] },
    "Territory Rebalance Strategist": { "main": [[{ "node": "Post Rebalance Proposal for Approval", "type": "main", "index": 0 }]] },
    "Anthropic Chat Model": { "ai_languageModel": [[{ "node": "Territory Rebalance Strategist", "type": "ai_languageModel", "index": 0 }]] },
    "Structured Output Parser": { "ai_outputParser": [[{ "node": "Territory Rebalance Strategist", "type": "ai_outputParser", "index": 0 }]] },
    "Post Rebalance Proposal for Approval": { "main": [[{ "node": "Approved?", "type": "main", "index": 0 }]] },
    "Approved?": { "main": [[{ "node": "Explode Approved Moves", "type": "main", "index": 0 }], [{ "node": "Explode Declined Moves", "type": "main", "index": 0 }]] },
    "Explode Approved Moves": { "main": [[{ "node": "Log Approved Proposal", "type": "main", "index": 0 }]] },
    "Explode Declined Moves": { "main": [[{ "node": "Log Declined Proposal", "type": "main", "index": 0 }]] }
  },
  "settings": { "executionOrder": "v1" }
}
