{
  "name": "Campaign Fatigue Firewall",
  "nodes": [
    {
      "id": "8ac08685-f52f-4554-a217-259146089ea7",
      "name": "Daily Fatigue Check",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.4,
      "position": [
        224,
        224
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "triggerAtHour": 7
            }
          ]
        }
      }
    },
    {
      "id": "9f5ea53f-3d19-4823-add7-a4804a86a4e3",
      "name": "Config",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.5,
      "position": [
        448,
        224
      ],
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "cap-id",
              "name": "touchCapPerWindow",
              "value": 3,
              "type": "number"
            },
            {
              "id": "window-id",
              "name": "windowDays",
              "value": 14,
              "type": "number"
            }
          ]
        },
        "options": {}
      }
    },
    {
      "id": "3982d1eb-2832-45b0-ac4c-297be279d795",
      "name": "Get HubSpot Contacts",
      "type": "n8n-nodes-base.hubspot",
      "typeVersion": 2.2,
      "position": [
        672,
        224
      ],
      "parameters": {
        "authentication": "appToken",
        "operation": "getAll",
        "returnAll": true,
        "additionalFields": {
          "propertiesCollection": {
            "propertiesValues": {
              "properties": [
                "email",
                "firstname",
                "lastname",
                "company"
              ],
              "propertyMode": "valueOnly"
            }
          }
        }
      },
      "credentials": {
        "hubspotAppToken": {
          "id": "ynUSCUtMOnX1f4Jl",
          "name": "HubSpot Service Key account"
        }
      }
    },
    {
      "id": "1939d27f-da9f-4165-859f-c18e650a4115",
      "name": "Filter Demo Contacts",
      "type": "n8n-nodes-base.filter",
      "typeVersion": 2.3,
      "position": [
        896,
        224
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": false,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 1
          },
          "conditions": [
            {
              "leftValue": "={{ $json.properties.email && $json.properties.email.value ? $json.properties.email.value : '' }}",
              "operator": {
                "type": "string",
                "operation": "endsWith"
              },
              "rightValue": "demo.io"
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "eead8646-1b51-40c9-90e6-e8b3af04907d",
      "name": "Get Contact Engagements",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.5,
      "position": [
        1120,
        224
      ],
      "parameters": {
        "url": "=https://api.hubapi.com/engagements/v1/engagements/associated/CONTACT/{{ $json.vid }}/paged?limit=100",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "hubspotAppToken",
        "options": {}
      },
      "credentials": {
        "hubspotAppToken": {
          "id": "ynUSCUtMOnX1f4Jl",
          "name": "HubSpot Service Key account"
        }
      }
    },
    {
      "id": "dfbfb31b-9b2e-4ff8-90e7-2917a79d8167",
      "name": "Compute Touch Frequency",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1344,
        224
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const cap = Number($('Config').first().json.touchCapPerWindow);\nconst windowDays = Number($('Config').first().json.windowDays);\nconst contact = $('Filter Demo Contacts').item.json;\nconst email = contact.properties && contact.properties.email ? contact.properties.email.value : '';\nconst firstname = contact.properties && contact.properties.firstname ? contact.properties.firstname.value : '';\nconst lastname = contact.properties && contact.properties.lastname ? contact.properties.lastname.value : '';\nconst company = contact.properties && contact.properties.company ? contact.properties.company.value : '';\nconst cutoff = Date.now() - windowDays * 24 * 60 * 60 * 1000;\nconst results = $json.results || [];\nlet count = 0;\nfor (let i = 0; i < results.length; i++) {\n  const ts = results[i].engagement && results[i].engagement.timestamp ? results[i].engagement.timestamp : 0;\n  if (ts >= cutoff) {\n    count = count + 1;\n  }\n}\nreturn {\n  json: {\n    vid: contact.vid,\n    email: email,\n    name: (firstname + ' ' + lastname).trim(),\n    company: company,\n    touchCountInWindow: count,\n    cap: cap,\n    windowDays: windowDays\n  }\n};"
      }
    },
    {
      "id": "b43977df-7258-44d5-89fc-302f3c204a83",
      "name": "Over Cap Only",
      "type": "n8n-nodes-base.filter",
      "typeVersion": 2.3,
      "position": [
        1568,
        224
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 1
          },
          "conditions": [
            {
              "leftValue": "={{ $json.touchCountInWindow }}",
              "operator": {
                "type": "number",
                "operation": "gt"
              },
              "rightValue": "={{ $json.cap }}"
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "bd57f7f8-058d-410f-91fe-bdc93f2d9f11",
      "name": "Get Open Salesforce Opportunities",
      "type": "n8n-nodes-base.salesforce",
      "typeVersion": 1.1,
      "position": [
        1792,
        224
      ],
      "executeOnce": true,
      "parameters": {
        "resource": "opportunity",
        "operation": "getAll",
        "returnAll": true,
        "options": {
          "fields": "Id,Name,StageName,Amount,AccountId,IsClosed"
        }
      },
      "credentials": {
        "salesforceOAuth2Api": {
          "id": "553Tae1kPe5Wpthq",
          "name": "Salesforce account"
        }
      }
    },
    {
      "id": "eec22a57-b520-436e-b0dd-ad5ff1d894fb",
      "name": "Match And Decide",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2016,
        224
      ],
      "parameters": {
        "jsCode": "const earlyStages = ['Prospecting', 'Qualification'];\nconst opportunities = $input.all();\nconst items = $('Over Cap Only').all();\nconst out = [];\nfor (let i = 0; i < items.length; i++) {\n  const c = items[i].json;\n  const companyLower = (c.company || '').toLowerCase();\n  let match = null;\n  for (let j = 0; j < opportunities.length; j++) {\n    const o = opportunities[j].json;\n    if (o.IsClosed === false && o.Name && companyLower && o.Name.toLowerCase().indexOf(companyLower) !== -1) {\n      match = o;\n      break;\n    }\n  }\n  let decision = 'suppress';\n  if (match && earlyStages.indexOf(match.StageName) === -1) {\n    decision = 'override_keep';\n  }\n  out.push({\n    json: {\n      vid: c.vid,\n      email: c.email,\n      name: c.name,\n      company: c.company,\n      touchCountInWindow: c.touchCountInWindow,\n      cap: c.cap,\n      windowDays: c.windowDays,\n      decision: decision,\n      openOpportunityName: match ? match.Name : '',\n      openOpportunityStage: match ? match.StageName : '',\n      openOpportunityAmount: match && match.Amount ? match.Amount : null\n    }\n  });\n}\nreturn out;"
      }
    },
    {
      "id": "f7128a19-0fc1-4b6a-ab9f-c304b069f2d8",
      "name": "Draft Suppression Explanation",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "typeVersion": 3.1,
      "position": [
        2240,
        224
      ],
      "parameters": {
        "promptType": "define",
        "text": "=Contact: {{ $json.name }} at {{ $json.company }}\nTouch count in last {{ $json.windowDays }} days: {{ $json.touchCountInWindow }} (cap: {{ $json.cap }})\nDecision: {{ $json.decision }}\nMatched open Salesforce opportunity: {{ $json.openOpportunityName || 'none' }} (stage: {{ $json.openOpportunityStage || 'n/a' }})\n\nIf decision is 'suppress', write a one to two sentence explanation of which campaigns/sequences should suppress this contact and why, given the touch count exceeded the cap with no active late-stage deal.\nIf decision is 'override_keep', write a one to two sentence explanation of why the deal-stage override kept this contact in active outreach despite exceeding the cap, referencing the open opportunity and its stage.\nBe concise and specific to the numbers given. Do not invent additional facts.",
        "hasOutputParser": true,
        "options": {
          "systemMessage": "You are a revenue operations assistant writing short, factual internal notes about campaign touch-frequency decisions. Use only the data provided in the prompt."
        }
      }
    },
    {
      "id": "518a7283-f95f-423b-bde7-67e2f4378b2f",
      "name": "Claude Sonnet Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
      "typeVersion": 1.6,
      "position": [
        2248,
        456
      ],
      "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": "2331f5e1-d22e-4932-b849-3e95e94b3731",
      "name": "Explanation Parser",
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "typeVersion": 1.3,
      "position": [
        2376,
        456
      ],
      "parameters": {
        "jsonSchemaExample": "{ \"explanation\": \"Suppressing further sequence emails to Priya Nandan (Brightpath Analytics): 4 touches in the last 14 days exceeds the cap of 3, and there is no active Salesforce opportunity to justify the extra outreach.\" }"
      }
    },
    {
      "id": "b786f6e7-5aad-4d20-bae5-7fd890496cfa",
      "name": "Log Suppression Decision",
      "type": "n8n-nodes-base.dataTable",
      "typeVersion": 1.1,
      "position": [
        2592,
        128
      ],
      "parameters": {
        "dataTableId": {
          "__rl": true,
          "mode": "id",
          "value": "dwq3hZmxbP2p1F2q",
          "cachedResultName": "Campaign Fatigue Suppressions"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "contactEmail": "={{ $('Match And Decide').item.json.email }}",
            "contactName": "={{ $('Match And Decide').item.json.name }}",
            "company": "={{ $('Match And Decide').item.json.company }}",
            "touchCountInWindow": "={{ $('Match And Decide').item.json.touchCountInWindow }}",
            "touchCap": "={{ $('Match And Decide').item.json.cap }}",
            "decision": "={{ $('Match And Decide').item.json.decision }}",
            "openOpportunityName": "={{ $('Match And Decide').item.json.openOpportunityName }}",
            "openOpportunityStage": "={{ $('Match And Decide').item.json.openOpportunityStage }}",
            "rationale": "={{ $json.output.explanation }}",
            "slackPosted": true,
            "loggedAt": "={{ $now.toISO() }}"
          },
          "schema": [
            {
              "id": "contactEmail",
              "displayName": "contactEmail",
              "type": "string",
              "display": true,
              "canBeUsedToMatch": true
            },
            {
              "id": "contactName",
              "displayName": "contactName",
              "type": "string",
              "display": true,
              "canBeUsedToMatch": true
            },
            {
              "id": "company",
              "displayName": "company",
              "type": "string",
              "display": true,
              "canBeUsedToMatch": true
            },
            {
              "id": "touchCountInWindow",
              "displayName": "touchCountInWindow",
              "type": "number",
              "display": true,
              "canBeUsedToMatch": true
            },
            {
              "id": "touchCap",
              "displayName": "touchCap",
              "type": "number",
              "display": true,
              "canBeUsedToMatch": true
            },
            {
              "id": "decision",
              "displayName": "decision",
              "type": "string",
              "display": true,
              "canBeUsedToMatch": true
            },
            {
              "id": "openOpportunityName",
              "displayName": "openOpportunityName",
              "type": "string",
              "display": true,
              "canBeUsedToMatch": true
            },
            {
              "id": "openOpportunityStage",
              "displayName": "openOpportunityStage",
              "type": "string",
              "display": true,
              "canBeUsedToMatch": true
            },
            {
              "id": "rationale",
              "displayName": "rationale",
              "type": "string",
              "display": true,
              "canBeUsedToMatch": true
            },
            {
              "id": "slackPosted",
              "displayName": "slackPosted",
              "type": "boolean",
              "display": true,
              "canBeUsedToMatch": true
            },
            {
              "id": "loggedAt",
              "displayName": "loggedAt",
              "type": "string",
              "display": true,
              "canBeUsedToMatch": true
            }
          ]
        },
        "options": {}
      }
    },
    {
      "id": "07c4b5fb-5784-43a8-9815-47b091a289f3",
      "name": "Build Slack Digest",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2592,
        320
      ],
      "parameters": {
        "jsCode": "const contacts = $('Match And Decide').all();\nconst lines = [];\nfor (let i = 0; i < contacts.length; i++) {\n  const c = contacts[i].json;\n  let line = '';\n  if (c.decision === 'override_keep') {\n    line = ':large_yellow_circle: *' + c.name + '* (' + c.company + ') \\u2014 ' + c.touchCountInWindow + ' touches/' + c.windowDays + 'd (cap ' + c.cap + ') \\u2192 *KEPT* \\u2014 open deal: ' + c.openOpportunityName + ' (' + c.openOpportunityStage + ')';\n  } else {\n    const reason = c.openOpportunityName ? ('only a closed deal on file: ' + c.openOpportunityName) : 'no matching CRM deal';\n    line = ':red_circle: *' + c.name + '* (' + c.company + ') \\u2014 ' + c.touchCountInWindow + ' touches/' + c.windowDays + 'd (cap ' + c.cap + ') \\u2192 *SUPPRESSED* \\u2014 ' + reason;\n  }\n  lines.push(line);\n}\nconst header = '*Campaign Fatigue Firewall \\u2014 ' + contacts.length + ' contact(s) flagged today*\\n';\nconst text = header + lines.join('\\n');\nreturn [{ json: { text: text } }];"
      }
    },
    {
      "id": "a0ebc844-2f3a-432d-bde8-e60bbc46423c",
      "name": "Post Digest to Slack",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.7,
      "position": [
        2816,
        320
      ],
      "webhookId": "60de308b-90c1-4031-a241-6e03955451e6",
      "parameters": {
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "id",
          "value": "C0BQ3G96HL6",
          "cachedResultName": "general"
        },
        "text": "={{ $json.text }}",
        "otherOptions": {}
      },
      "credentials": {
        "slackApi": {
          "id": "mv7TIi8kucFPqPg6",
          "name": "Slack account"
        }
      }
    }
  ],
  "connections": {
    "Daily Fatigue Check": {
      "main": [
        [
          {
            "node": "Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Config": {
      "main": [
        [
          {
            "node": "Get HubSpot Contacts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get HubSpot Contacts": {
      "main": [
        [
          {
            "node": "Filter Demo Contacts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter Demo Contacts": {
      "main": [
        [
          {
            "node": "Get Contact Engagements",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Contact Engagements": {
      "main": [
        [
          {
            "node": "Compute Touch Frequency",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Compute Touch Frequency": {
      "main": [
        [
          {
            "node": "Over Cap Only",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Over Cap Only": {
      "main": [
        [
          {
            "node": "Get Open Salesforce Opportunities",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Open Salesforce Opportunities": {
      "main": [
        [
          {
            "node": "Match And Decide",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Match And Decide": {
      "main": [
        [
          {
            "node": "Draft Suppression Explanation",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Draft Suppression Explanation": {
      "main": [
        [
          {
            "node": "Log Suppression Decision",
            "type": "main",
            "index": 0
          },
          {
            "node": "Build Slack Digest",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Claude Sonnet Model": {
      "ai_languageModel": [
        [
          {
            "node": "Draft Suppression Explanation",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Explanation Parser": {
      "ai_outputParser": [
        [
          {
            "node": "Draft Suppression Explanation",
            "type": "ai_outputParser",
            "index": 0
          }
        ]
      ]
    },
    "Build Slack Digest": {
      "main": [
        [
          {
            "node": "Post Digest to Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  }
}