{
  "name": "Intent Signal Monitor",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "50 7 * * 1"
            }
          ]
        }
      },
      "id": "33c20d4b-354e-4d00-86c2-a1ec397b6bd6",
      "name": "Weekly Signal Scan",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        96
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "a1",
              "name": "asOf",
              "value": "={{ $json['As Of Date'] ? String($json['As Of Date']).slice(0, 10) : $today.toFormat('yyyy-MM-dd') }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "03b6f23b-8bbd-4ef1-82bf-e0ab843a1e3a",
      "name": "Set Run Date",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        288,
        336
      ]
    },
    {
      "parameters": {
        "formTitle": "Intent Signal Monitor: backtest run",
        "formDescription": "Replay the weekly scan as of a past date, using research snapshots cut off at that date.",
        "formFields": {
          "values": [
            {
              "fieldLabel": "As Of Date",
              "fieldType": "date",
              "requiredField": true
            }
          ]
        },
        "options": {}
      },
      "id": "350961af-3d11-416a-a01e-6f11d3795bfb",
      "name": "Backtest Run",
      "type": "n8n-nodes-base.formTrigger",
      "typeVersion": 2.6,
      "position": [
        0,
        288
      ],
      "webhookId": "a344948d-d660-4203-a4dc-09890d11cd59"
    },
    {
      "parameters": {
        "operation": "get",
        "dataTableId": {
          "__rl": true,
          "mode": "id",
          "value": "IDp9Yxjqc1Ii2cNd",
          "cachedResultName": "Intent Watchlist"
        },
        "returnAll": true
      },
      "id": "39600eb7-b8db-402e-a9ac-df68e43acc5c",
      "name": "Get Watchlist",
      "type": "n8n-nodes-base.dataTable",
      "typeVersion": 1.1,
      "position": [
        512,
        336
      ],
      "executeOnce": true
    },
    {
      "parameters": {
        "operation": "get",
        "dataTableId": {
          "__rl": true,
          "mode": "id",
          "value": "GUBgaMYwWtyrLsLs",
          "cachedResultName": "Intent Research Snapshots"
        },
        "returnAll": true
      },
      "id": "e8df9cdd-1a6a-4525-9f72-bf3c1bd3403a",
      "name": "Get Research Snapshots",
      "type": "n8n-nodes-base.dataTable",
      "typeVersion": 1.1,
      "position": [
        736,
        336
      ],
      "executeOnce": true,
      "alwaysOutputData": true
    },
    {
      "parameters": {
        "operation": "get",
        "dataTableId": {
          "__rl": true,
          "mode": "id",
          "value": "vIEEIthaVKEiIfyX",
          "cachedResultName": "Intent Signal History"
        },
        "returnAll": true
      },
      "id": "b4371ea3-a965-44e8-8b10-592012a486c3",
      "name": "Get Signal History",
      "type": "n8n-nodes-base.dataTable",
      "typeVersion": 1.1,
      "position": [
        960,
        336
      ],
      "executeOnce": true,
      "alwaysOutputData": true
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.hubapi.com/crm/v3/objects/companies/search",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "hubspotAppToken",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ filterGroups: [{ filters: [{ propertyName: 'domain', operator: 'IN', values: $('Get Watchlist').all().map(i => i.json.domain).filter(Boolean) }] }], properties: ['name','domain','lifecyclestage','industry'], limit: 100 }) }}",
        "options": {}
      },
      "id": "38126da9-9caa-4a5c-9b36-76b6377d97d1",
      "name": "Search HubSpot Companies",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        816,
        336
      ],
      "executeOnce": true,
      "credentials": {
        "hubspotAppToken": {
          "id": "ynUSCUtMOnX1f4Jl",
          "name": "HubSpot Service Key account"
        }
      }
    },
    {
      "parameters": {
        "resource": "search",
        "query": "=SELECT Id, Name, StageName, CreatedDate, Account.Name FROM Opportunity WHERE IsClosed = false AND Account.Name IN ({{ $('Get Watchlist').all().map(i => \"'\" + String(i.json.accountName).replace(/'/g, \"\\\\'\") + \"'\").join(',') }})"
      },
      "id": "153ef0ba-d5ef-4355-b48b-0f0eda13b722",
      "name": "Get Salesforce Open Opps",
      "type": "n8n-nodes-base.salesforce",
      "typeVersion": 1.1,
      "position": [
        1040,
        336
      ],
      "executeOnce": true,
      "alwaysOutputData": true,
      "credentials": {
        "salesforceOAuth2Api": {
          "id": "553Tae1kPe5Wpthq",
          "name": "Salesforce account"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Deterministic signal strength per watched account, as of the run date.\n// Claude only labels evidence lines (Evidence Relevance Check); every point below is counted in code.\nconst asOf = $('Set Run Date').first().json.asOf;\nconst watch = $('Get Watchlist').all().map(i => i.json).filter(r => r.accountName && r.status !== 'removed');\nconst snaps = $('Get Research Snapshots').all().map(i => i.json).filter(r => r.accountName);\nconst hist = $('Get Signal History').all().map(i => i.json).filter(r => r.accountName && r.runAsOf);\nconst hsCos = ($('Search HubSpot Companies').first().json.results || []);\nconst sfOpps = $('Get Salesforce Open Opps').all().map(i => i.json).filter(r => r.Id);\nconst prep = $('Prepare Evidence Lines').first().json;\nconst labelOut = ($('Evidence Relevance Check').first().json.output || {});\nconst labels = {}; for (const l of (labelOut.lines || [])) labels[String(l.id)] = l;\n\nconst DAY = 86400000;\nconst t0 = Date.parse(asOf + 'T00:00:00Z');\nfunction daysBefore(d) { const t = Date.parse(String(d).slice(0, 10) + 'T00:00:00Z'); return isNaN(t) ? null : Math.round((t0 - t) / DAY); }\nfunction norm(s) { return String(s || '').toLowerCase().replace(/[^a-z0-9]/g, ''); }\nfunction root(d) { return String(d || '').toLowerCase().replace(/^www\\./, ''); }\nconst hsByDomain = {}; for (const c of hsCos) { const p = c.properties || {}; if (p.domain) hsByDomain[root(p.domain)] = p; }\n\nconst out = [];\nfor (const w of watch) {\n  const snap = snaps.find(s => norm(s.accountName) === norm(w.accountName) && String(s.asOf).slice(0, 10) === asOf);\n  // previous = most recent logged run strictly before this as-of date; ties broken by the latest loggedAt\n  const prior = hist.filter(h => norm(h.accountName) === norm(w.accountName) && String(h.runAsOf).slice(0, 10) < asOf)\n    .sort((a, b) => String(b.runAsOf).localeCompare(String(a.runAsOf)) || String(b.loggedAt).localeCompare(String(a.loggedAt)))[0];\n  const previous = prior ? Number(prior.signalStrength) : null;\n\n  if (!snap) {\n    out.push({ runAsOf: asOf, accountName: w.accountName, domain: w.domain, signalStrength: previous == null ? 0 : previous, previousStrength: previous == null ? '' : previous, delta: 0,\n      outcome: 'no-research', strengthBreakdown: 'No research snapshot for ' + asOf + '; strength carried forward, no alert.', countedEvidence: [] });\n    continue;\n  }\n\n  const acct = (prep.accounts || []).find(a => norm(a.accountName) === norm(w.accountName)) || { current: [], prior: [] };\n  const lab = l => labels[l.id] || { kind: 'other', aboutCompanyItself: false, sameAsId: '', reason: 'not labelled' };\n  const priorById = {}; for (const l of acct.prior) priorById[l.id] = l;\n  const parts = [], counted = [], dropped = [];\n  let s = 0;\n\n  // Funding: latest dated funding across all snapshots up to this as-of (Claygent sometimes misses a round it found before).\n  const mine = snaps.filter(x => norm(x.accountName) === norm(w.accountName) && String(x.asOf).slice(0, 10) <= asOf);\n  const fundDates = mine.map(x => String(x.fundingDate || '').slice(0, 10)).filter(d => /^\\d{4}-\\d\\d-\\d\\d$/.test(d) && d <= asOf).sort();\n  const bestFunding = fundDates.length ? fundDates[fundDates.length - 1] : null;\n  const fd = bestFunding ? daysBefore(bestFunding) : null;\n  if (bestFunding && bestFunding !== String(snap.fundingDate || '').slice(0, 10)) parts.push('funding date ' + bestFunding + ' carried from an earlier snapshot');\n  if (fd != null && fd >= 0 && fd <= 180) { s += 30; parts.push('funding ' + fd + 'd ago +30'); }\n  else if (fd != null && fd > 180 && fd <= 365) { s += 15; parts.push('funding ' + fd + 'd ago +15'); }\n  else parts.push(fd == null ? 'no dated funding +0' : 'funding older than a year +0');\n\n  // Roles: current lines labelled security_role, about the company itself, dated within 30 days,\n  // and not a relisting of a posting the previous snapshot already saw before this window.\n  let roles = 0;\n  for (const l of acct.current) {\n    const L = lab(l), age = daysBefore(l.date);\n    if (L.kind !== 'security_role') continue;\n    if (!L.aboutCompanyItself) { dropped.push('role not at this company: ' + l.text.slice(0, 80)); continue; }\n    if (age == null || age < 0 || age > 30) continue;\n    const same = L.sameAsId && priorById[L.sameAsId];\n    if (same && daysBefore(same.date) > 30) { dropped.push('relisted posting first seen ' + same.date + ': ' + l.text.slice(0, 80)); continue; }\n    roles++; counted.push(l.text);\n  }\n  s += Math.min(roles, 5) * 8; parts.push(roles + ' security/compliance role(s) posted in last 30d +' + Math.min(roles, 5) * 8);\n\n  // Events: compliance_event about the company itself within 90 days, from this snapshot plus any the prior snapshot found\n  // that this one missed (still inside the window, not already matched by a current line).\n  let events = 0; const matchedPrior = new Set();\n  for (const l of acct.current) {\n    const L = lab(l), age = daysBefore(l.date);\n    if (L.kind === 'other' && /blog|marketing|customers|advice/i.test(L.reason || '')) dropped.push('not a first-party event: ' + l.text.slice(0, 80));\n    if (L.kind !== 'compliance_event') continue;\n    if (L.sameAsId) matchedPrior.add(L.sameAsId);\n    if (!L.aboutCompanyItself) { dropped.push('event not at this company: ' + l.text.slice(0, 80)); continue; }\n    if (age == null || age < 0 || age > 90) continue;\n    events++; counted.push(l.text);\n  }\n  let carried = 0;\n  for (const l of acct.prior) {\n    const L = lab(l), age = daysBefore(l.date);\n    if (L.kind !== 'compliance_event' || !L.aboutCompanyItself || matchedPrior.has(l.id)) continue;\n    if (age == null || age < 0 || age > 90) continue;\n    carried++; counted.push(l.text + ' (from the ' + acct.priorAsOf + ' research)');\n  }\n  events += carried;\n  s += Math.min(events, 3) * 10;\n  parts.push(events + ' compliance/security event(s) in last 90d +' + Math.min(events, 3) * 10 + (carried ? ' (' + carried + ' carried from ' + acct.priorAsOf + ')' : ''));\n  const claimed = (Number(snap.securityRoles) || 0) + (Number(snap.complianceEvents) || 0);\n  if (claimed > roles + events - carried) parts.push('Claygent claimed ' + (Number(snap.securityRoles) || 0) + ' role(s) / ' + (Number(snap.complianceEvents) || 0) + ' event(s); only evidence that passed the relevance check scores');\n  if (dropped.length) parts.push('dropped: ' + dropped.join(' | '));\n  s = Math.min(100, s);\n\n  const hs = hsByDomain[root(w.domain)];\n  const hubspotSignal = hs ? ('In HubSpot as ' + hs.name + (hs.lifecyclestage ? ' (' + hs.lifecyclestage + ')' : '')) : 'Not in HubSpot';\n  const opp = sfOpps.find(o => norm((o.Account || {}).Name) === norm(w.accountName) && String(o.CreatedDate).slice(0, 10) <= asOf);\n  const salesforceSignal = opp ? ('Open opp \"' + opp.Name + '\" (' + opp.StageName + ', created ' + String(opp.CreatedDate).slice(0, 10) + ')') : 'No open Salesforce opportunity';\n\n  const delta = previous == null ? 0 : s - previous;\n  let outcome;\n  if (opp) outcome = 'graduated';\n  else if (previous == null) outcome = 'baseline';\n  else if (delta >= 8) outcome = 'accelerating'; // at least one new evidence-backed signal since last run\n  else if (delta <= -8) outcome = 'cooling';\n  else outcome = 'steady';\n\n  out.push({ runAsOf: asOf, accountName: w.accountName, domain: w.domain, signalStrength: s, previousStrength: previous == null ? '' : previous, delta,\n    outcome, strengthBreakdown: parts.join('; '), hubspotSignal, salesforceSignal,\n    fundingSummary: snap.fundingSummary || '', securityRoles: roles, complianceEvents: events, countedEvidence: counted,\n    watchRowId: w.id, graduationNote: opp ? salesforceSignal : '' });\n}\nreturn out.map(j => ({ json: j }));\n"
      },
      "id": "746f4524-8388-4359-9ec3-33bf615737d9",
      "name": "Compute Signal Strength",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2240,
        192
      ],
      "executeOnce": true
    },
    {
      "parameters": {
        "operation": "update",
        "dataTableId": {
          "__rl": true,
          "mode": "id",
          "value": "IDp9Yxjqc1Ii2cNd",
          "cachedResultName": "Intent Watchlist"
        },
        "matchType": "allConditions",
        "filters": {
          "conditions": [
            {
              "keyName": "accountName",
              "keyValue": "={{ $json.accountName }}"
            }
          ]
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "lastSignalStrength": "={{ $json.signalStrength }}",
            "previousSignalStrength": "={{ $json.previousStrength === '' ? null : $json.previousStrength }}",
            "lastAsOf": "={{ $json.runAsOf }}",
            "status": "={{ $json.outcome === 'graduated' ? 'graduated' : 'watching' }}",
            "graduatedAt": "={{ $json.outcome === 'graduated' ? $json.runAsOf : '' }}",
            "graduationNote": "={{ $json.graduationNote || '' }}"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "lastSignalStrength",
              "displayName": "lastSignalStrength",
              "type": "number",
              "display": true,
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "previousSignalStrength",
              "displayName": "previousSignalStrength",
              "type": "number",
              "display": true,
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "lastAsOf",
              "displayName": "lastAsOf",
              "type": "string",
              "display": true,
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "status",
              "displayName": "status",
              "type": "string",
              "display": true,
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "graduatedAt",
              "displayName": "graduatedAt",
              "type": "string",
              "display": true,
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "graduationNote",
              "displayName": "graduationNote",
              "type": "string",
              "display": true,
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ]
        },
        "options": {}
      },
      "id": "c64e78fd-56ef-41c4-baec-52405b35d453",
      "name": "Update Watchlist",
      "type": "n8n-nodes-base.dataTable",
      "typeVersion": 1.1,
      "position": [
        2464,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "return $('Compute Signal Strength').all().map(i => { const r = i.json; return { json: {\n  runAsOf: r.runAsOf, accountName: r.accountName, signalStrength: r.signalStrength, previousStrength: r.previousStrength === '' ? null : r.previousStrength,\n  delta: r.delta, outcome: r.outcome, strengthBreakdown: r.strengthBreakdown, hubspotSignal: r.hubspotSignal || '', salesforceSignal: r.salesforceSignal || '',\n  note: '', loggedAt: $now.toISO() } }; });\n"
      },
      "id": "4f4263b0-c3c0-4d12-9df8-cfd5186cb5de",
      "name": "Build History Rows",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2464,
        192
      ],
      "executeOnce": true
    },
    {
      "parameters": {
        "dataTableId": {
          "__rl": true,
          "mode": "id",
          "value": "vIEEIthaVKEiIfyX",
          "cachedResultName": "Intent Signal History"
        },
        "columns": {
          "mappingMode": "autoMapInputData",
          "value": {}
        },
        "options": {}
      },
      "id": "2a441be4-e21e-4114-a059-dacc84988109",
      "name": "Log Signal History",
      "type": "n8n-nodes-base.dataTable",
      "typeVersion": 1.1,
      "position": [
        2752,
        192
      ]
    },
    {
      "parameters": {
        "jsCode": "// Only accounts that moved enough to act on go to Claude and Slack. Baseline runs post nothing.\nconst rows = $('Compute Signal Strength').all().map(i => i.json);\nconst hits = rows.filter(r => r.outcome === 'accelerating' || r.outcome === 'graduated')\n  .sort((a, b) => (b.outcome === 'graduated') - (a.outcome === 'graduated') || b.delta - a.delta);\nif (!hits.length) return [];\nreturn [{ json: { asOf: rows[0].runAsOf, accounts: hits.map(r => ({ accountName: r.accountName, outcome: r.outcome, signalStrength: r.signalStrength, previousStrength: r.previousStrength, delta: r.delta, strengthBreakdown: r.strengthBreakdown, fundingSummary: r.fundingSummary, countedEvidence: r.countedEvidence, hubspotSignal: r.hubspotSignal, salesforceSignal: r.salesforceSignal })) } }];\n"
      },
      "id": "2e3733ea-1118-4ec4-91e4-4ec82e6bddcf",
      "name": "Pick Alert Accounts",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2464,
        496
      ],
      "executeOnce": true
    },
    {
      "parameters": {
        "promptType": "define",
        "text": "=Run as of {{ $json.asOf }}. Accounts that moved:\n{{ JSON.stringify($json.accounts, null, 1) }}",
        "hasOutputParser": true,
        "options": {
          "systemMessage": "You write one-line \"why now\" notes for Fortavault, Inc. (a fictional data protection and security platform) sales reps. Each account below is a real company on a watchlist whose buying-signal score was ALREADY computed in code; you never change scores, outcomes or numbers.\n\nFor each account write ONE sentence under 200 characters that names the specific new evidence behind the move (for example a named funding round, a specific role title, a certification or incident) and a concrete outreach angle for a data protection vendor.\n\nRules: use only the facts in strengthBreakdown, fundingSummary and countedEvidence (the evidence lines that passed a relevance check and actually scored). Never invent roles, dates, amounts, people or events. If the evidence is thin, say what is known and keep the angle modest. Do not describe a score change as large unless delta is 20 or more. Return every accountName exactly as given."
        }
      },
      "id": "3aa8fbe0-fe5e-4f83-9145-59600a60d0fc",
      "name": "Why-Now Note Writer",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "typeVersion": 3.1,
      "position": [
        2688,
        496
      ],
      "executeOnce": true
    },
    {
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "claude-sonnet-4-6",
          "cachedResultName": "Claude Sonnet 4.6"
        },
        "options": {
          "temperature": 0.4
        }
      },
      "id": "210327e0-85be-478f-87cf-c5c5de9a79c1",
      "name": "Claude Sonnet",
      "type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
      "typeVersion": 1.3,
      "position": [
        2704,
        720
      ],
      "credentials": {
        "anthropicApi": {
          "id": "3d3ZiTi7BwnkFwqU",
          "name": "Anthropic account"
        }
      }
    },
    {
      "parameters": {
        "jsonSchemaExample": "{ \"accounts\": [ { \"accountName\": \"Acme\", \"note\": \"x\" } ] }"
      },
      "id": "89898be5-5dd1-4bc4-9433-370195ec6595",
      "name": "Note Parser",
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "typeVersion": 1.3,
      "position": [
        2832,
        720
      ]
    },
    {
      "parameters": {
        "jsCode": "const a = $('Pick Alert Accounts').first().json;\nconst out = $('Why-Now Note Writer').first().json.output || {};\n// Slack treats text between two ~ as strikethrough; make Claude's notes Slack-safe.\nconst notes = {}; for (const n of (out.accounts || [])) notes[String(n.accountName).toLowerCase()] = String(n.note || '').replace(/~/g, '≈');\nconst all = $('Compute Signal Strength').all().map(i => i.json);\nconst steady = all.filter(r => !['accelerating', 'graduated'].includes(r.outcome));\nconst icon = { accelerating: ':chart_with_upwards_trend:', graduated: ':mortar_board:' };\nconst lines = a.accounts.map(r => icon[r.outcome] + ' *' + r.accountName + '* — ' + (r.outcome === 'graduated' ? 'graduated to pipeline: ' + r.salesforceSignal : 'signal ' + r.previousStrength + ' → *' + r.signalStrength + '* (+' + r.delta + ')') +\n  '\\n      _' + (notes[r.accountName.toLowerCase()] || r.strengthBreakdown) + '_\\n      ' + r.hubspotSignal + ' · ' + r.salesforceSignal);\nconst text = ':satellite_antenna: *Weekly Intent Signal Monitor — Fortavault* (as of ' + a.asOf + ')\\n' +\n  all.length + ' watchlist accounts re-researched. *' + a.accounts.length + '* moved enough to act on (at least one new evidence-backed signal since the last run, or an open opp appeared):\\n\\n' +\n  lines.join('\\n\\n') +\n  '\\n\\n_Holding steady or cooling: ' + (steady.map(r => r.accountName + ' ' + r.signalStrength).join(', ') || 'none') + '._\\n' +\n  'Strength = funding recency + open security/compliance roles (30d) + compliance events (90d), scored in code from Clay Claygent research. Full history in the Intent Signal History table.';\nreturn [{ json: { slackText: text.slice(0, 2900) } }];\n"
      },
      "id": "36f655e6-eade-4588-92a3-ca7733d7a3fc",
      "name": "Build Signal Digest",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3040,
        496
      ]
    },
    {
      "parameters": {
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "id",
          "value": "C0BQ3G96HL6",
          "cachedResultName": "general"
        },
        "text": "={{ $json.slackText }}",
        "otherOptions": {
          "includeLinkToWorkflow": false
        }
      },
      "id": "d18a1873-4ced-4dbf-8a54-e49e98428887",
      "name": "Post Digest to Slack",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.7,
      "position": [
        3264,
        496
      ],
      "webhookId": "4eeb43c0-d364-47e3-8a9b-f8b2ed43a1ce",
      "credentials": {
        "slackApi": {
          "id": "mv7TIi8kucFPqPg6",
          "name": "Slack account"
        }
      }
    },
    {
      "id": "b4eb5a45-61b7-4e95-9a0d-954821f72909",
      "name": "Prepare Evidence Lines",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1568,
        192
      ],
      "parameters": {
        "jsCode": "// Split each account's Claygent evidence into dated lines for the relevance check (current snapshot + the one before it).\nconst asOf = $('Set Run Date').first().json.asOf;\nconst watch = $('Get Watchlist').all().map(i => i.json).filter(r => r.accountName && r.status !== 'removed');\nconst snaps = $('Get Research Snapshots').all().map(i => i.json).filter(r => r.accountName);\nfunction norm(s) { return String(s || '').toLowerCase().replace(/[^a-z0-9]/g, ''); }\nfunction lines(sn, tag) {\n  return String((sn && sn.evidence) || '').split(/\\n+/).map(t => t.replace(/^[\\s\\-•*]+/, '').trim())\n    .map(t => { const m = t.match(/(20\\d\\d-\\d\\d-\\d\\d)/); return m ? { date: m[1], text: t.slice(0, 320) } : null; })\n    .filter(Boolean).map((l, i) => Object.assign({ id: tag + i }, l));\n}\nconst accounts = watch.map((w, a) => {\n  const mine = snaps.filter(s => norm(s.accountName) === norm(w.accountName) && String(s.asOf).slice(0, 10) <= asOf)\n    .sort((x, y) => String(y.asOf).localeCompare(String(x.asOf)));\n  const cur = mine.find(s => String(s.asOf).slice(0, 10) === asOf);\n  const prev = mine.find(s => String(s.asOf).slice(0, 10) < asOf);\n  return { accountName: w.accountName, domain: w.domain, current: lines(cur, 'a' + a + 'c'), prior: lines(prev, 'a' + a + 'p'), priorAsOf: prev ? String(prev.asOf).slice(0, 10) : '' };\n});\nreturn [{ json: { asOf, accounts, lineCount: accounts.reduce((n, x) => n + x.current.length + x.prior.length, 0) } }];\n",
        "mode": "runOnceForAllItems"
      },
      "executeOnce": true
    },
    {
      "id": "5c26a1d0-7646-4788-a8d3-6fe3fce5156b",
      "name": "Evidence Relevance Check",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "typeVersion": 3.1,
      "position": [
        1792,
        192
      ],
      "parameters": {
        "hasOutputParser": true,
        "options": {
          "systemMessage": "You label evidence lines for a B2B buying-signal monitor. A data protection and security vendor watches these real companies for signs they are about to buy security or compliance tooling. You do not score anything; code scores from your labels, so be strict and literal.\n\nFor EVERY line id you are given (current and prior), return:\n- kind: one of\n  \"security_role\" = an open job posting at this company for security, privacy, GRC or compliance work (a role whose title is plainly security/privacy/compliance; general engineering, GTM or ops roles are \"other\" even if the description mentions compliance);\n  \"compliance_event\" = something that happened at this company itself: it obtained or started a certification (SOC 2, ISO 27001, HITRUST...), disclosed a breach or incident, faced a regulatory action, appointed a security/privacy leader, or selected a security vendor;\n  \"funding\" = a funding round, IPO or investment;\n  \"other\" = anything else, including the company's own blog or marketing content explaining a topic to its customers, policy page updates, product announcements, and notes saying nothing was found.\n- aboutCompanyItself: true only if the line describes the company's own action or posting (not advice to customers, not another company).\n- sameAsId: for a CURRENT line, the id of a PRIOR line that describes the same job posting (same title) or the same event, else \"\". A posting seen again later is the same posting even if the listing date differs.\n- reason: under 15 words.\n\nNever invent facts. If unsure, choose \"other\". Return every id exactly once."
        },
        "promptType": "define",
        "text": "=Run as of {{ $json.asOf }}. Label every line id below.\n{{ JSON.stringify($json.accounts, null, 1) }}"
      },
      "executeOnce": true
    },
    {
      "id": "f85d0c08-a39c-49d5-8246-faa25f77d939",
      "name": "Claude Classifier",
      "type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
      "typeVersion": 1.3,
      "position": [
        1792,
        416
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "cachedResultName": "Claude Sonnet 4.6",
          "mode": "list",
          "value": "claude-sonnet-4-6"
        },
        "options": {
          "maxTokensToSample": 8000,
          "temperature": 0
        }
      },
      "credentials": {
        "anthropicApi": {
          "id": "3d3ZiTi7BwnkFwqU",
          "name": "Anthropic account"
        }
      }
    },
    {
      "id": "82c72e6a-02fc-465d-bf8f-18c96754bf45",
      "name": "Label Parser",
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "typeVersion": 1.3,
      "position": [
        1920,
        416
      ],
      "parameters": {
        "jsonSchemaExample": "{ \"lines\": [ { \"id\": \"a0c0\", \"kind\": \"security_role\", \"aboutCompanyItself\": true, \"sameAsId\": \"\", \"reason\": \"x\" } ] }",
        "schemaType": "fromJson"
      }
    }
  ],
  "connections": {
    "Weekly Signal Scan": {
      "main": [
        [
          {
            "node": "Set Run Date",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Run Date": {
      "main": [
        [
          {
            "node": "Get Watchlist",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Backtest Run": {
      "main": [
        [
          {
            "node": "Set Run Date",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Watchlist": {
      "main": [
        [
          {
            "node": "Get Research Snapshots",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Research Snapshots": {
      "main": [
        [
          {
            "node": "Get Signal History",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Signal History": {
      "main": [
        [
          {
            "node": "Search HubSpot Companies",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Search HubSpot Companies": {
      "main": [
        [
          {
            "node": "Get Salesforce Open Opps",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Compute Signal Strength": {
      "main": [
        [
          {
            "node": "Update Watchlist",
            "type": "main",
            "index": 0
          },
          {
            "node": "Build History Rows",
            "type": "main",
            "index": 0
          },
          {
            "node": "Pick Alert Accounts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build History Rows": {
      "main": [
        [
          {
            "node": "Log Signal History",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Pick Alert Accounts": {
      "main": [
        [
          {
            "node": "Why-Now Note Writer",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Why-Now Note Writer": {
      "main": [
        [
          {
            "node": "Build Signal Digest",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Claude Sonnet": {
      "ai_languageModel": [
        [
          {
            "node": "Why-Now Note Writer",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Note Parser": {
      "ai_outputParser": [
        [
          {
            "node": "Why-Now Note Writer",
            "type": "ai_outputParser",
            "index": 0
          }
        ]
      ]
    },
    "Build Signal Digest": {
      "main": [
        [
          {
            "node": "Post Digest to Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Salesforce Open Opps": {
      "main": [
        [
          {
            "node": "Prepare Evidence Lines",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Evidence Lines": {
      "main": [
        [
          {
            "node": "Evidence Relevance Check",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Evidence Relevance Check": {
      "main": [
        [
          {
            "node": "Compute Signal Strength",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Claude Classifier": {
      "ai_languageModel": [
        [
          {
            "node": "Evidence Relevance Check",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Label Parser": {
      "ai_outputParser": [
        [
          {
            "node": "Evidence Relevance Check",
            "type": "ai_outputParser",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "pinData": {},
  "meta": {
    "templateCredsSetupCompleted": false
  }
}