{
  "name": "Competitive Battlecard Auto-Updater",
  "nodes": [
    {
      "id": "0020ec5d-69cb-4d27-a98b-5429354c142f",
      "name": "Weekly Battlecard Check",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.4,
      "position": [0, 300],
      "parameters": {
        "rule": { "interval": [{ "field": "cronExpression", "expression": "0 9 * * 3" }] }
      }
    },
    {
      "id": "43b7d5e2-cfe5-4a60-aea2-f8ca39e9d1d9",
      "name": "Fetch Competitor-Named Closed Lost",
      "type": "n8n-nodes-base.salesforce",
      "typeVersion": 1.1,
      "position": [240, 300],
      "parameters": {
        "resource": "search",
        "operation": "query",
        "query": "SELECT Id, Name, Amount, CloseDate, Account.Name, Description FROM Opportunity WHERE StageName = 'Closed Lost' ORDER BY CloseDate DESC LIMIT 200"
      },
      "credentials": { "salesforceOAuth2Api": { "id": "553Tae1kPe5Wpthq", "name": "Salesforce account" } }
    },
    {
      "id": "07ee7297-94ec-425e-a117-7e15ab0b3f20",
      "name": "Analyze Competitive Signals",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [480, 300],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const competitors = [\n  { name: \"Veyron Data\", currentPositioning: \"Point-solution backup vendor. Fortavault wins by contrasting the one-click ransomware rollback in Fortavault Shield (minutes) against the multi-hour restore-from-backup recovery process at Veyron Data.\", currentObjectionHandling: \"If the prospect cites price, emphasize that the cost of extended downtime during a slow recovery outweighs the lower sticker price at Veyron Data. If the prospect cites an existing Veyron Data relationship, offer a side-by-side recovery-time demo.\" },\n  { name: \"Ashcombe Systems\", currentPositioning: \"Ransomware-detection-only point tool. Fortavault wins by positioning the full platform (backup, detection, zero-trust access, and compliance reporting) against a narrow single-function product and per-seat pricing at Ashcombe Systems.\", currentObjectionHandling: \"If the prospect only wants detection, reframe the deal around total attack-surface coverage rather than detection alone. If the prospect is price sensitive, show that bundled Fortavault pricing beats stacking Ashcombe Systems with a separate backup and compliance tool.\" },\n  { name: \"Cyphertide\", currentPositioning: \"Incumbent vendor with sticky renewals. Fortavault wins on switching-cost concerns by offering a no-risk parallel-run pilot alongside an existing Cyphertide contract.\", currentObjectionHandling: \"If the prospect says they are happy with the current vendor, ask when the Cyphertide contract renews and offer a parallel pilot timed to that date instead of an immediate replacement.\" },\n  { name: \"Northgate Cloud Security\", currentPositioning: \"Broad security platform that wins vendor-consolidation deals. Fortavault wins by going deep where Northgate Cloud Security is shallow: the compliance-reporting depth in Fortavault Insights and the ransomware-specific recovery speed in Fortavault Shield.\", currentObjectionHandling: \"If the prospect wants one platform and one vendor, concede the consolidation appeal but show the audit-ready compliance reports in Insights and the recovery-time benchmark in Shield as depth that a broad platform cannot match.\" },\n  { name: \"Ridgeline Compliance\", currentPositioning: \"Compliance-reporting-only niche player. Fortavault wins by positioning Insights as compliance-native but backed by a full data-protection platform rather than a bolt-on report generator.\", currentObjectionHandling: \"If the prospect only cares about compliance reporting, ask who handles backup and ransomware recovery today. Ridgeline Compliance does not, and Insights ships as part of a platform that does.\" }\n];\n\nconst mockSignals = {\n  \"Veyron Data\": { signalType: \"pricing\", detail: \"Veyron Data quietly cut list pricing 15 percent on the core backup tier, undercutting Fortavault Backup Cloud on a per-GB basis for the first time this quarter.\", weight: 2 },\n  \"Ashcombe Systems\": { signalType: \"review_sentiment\", detail: \"The G2 rating for Ashcombe Systems slipped from 4.3 to 3.6 over the last 30 days after a wave of reviews citing a broken product update that disabled existing detection rules.\", weight: 2 },\n  \"Cyphertide\": { signalType: \"funding\", detail: \"Cyphertide closed a 40 million dollar Series C explicitly earmarked for enterprise sales expansion, according to a press release.\", weight: 2 },\n  \"Northgate Cloud Security\": { signalType: \"positioning\", detail: \"Northgate Cloud Security launched a bundled Platform Consolidation pricing tier explicitly marketed at replacing three or more point solutions with a single contract.\", weight: 3 },\n  \"Ridgeline Compliance\": { signalType: \"layoffs\", detail: \"Ridgeline Compliance laid off approximately 18 percent of staff, including most of the customer success team, according to a local business press report.\", weight: 1 }\n};\n\nconst items = $input.all().map(i => i.json);\n\nconst lossesByCompetitor = {};\ncompetitors.forEach(c => { lossesByCompetitor[c.name] = { count: 0, totalAmount: 0, notes: [] }; });\n\nitems.forEach(opp => {\n  const desc = opp.Description || \"\";\n  competitors.forEach(c => {\n    if (desc.indexOf(c.name) !== -1) {\n      lossesByCompetitor[c.name].count += 1;\n      lossesByCompetitor[c.name].totalAmount += (opp.Amount || 0);\n      const accountName = opp.Account ? opp.Account.Name : \"\";\n      lossesByCompetitor[c.name].notes.push(\"- \" + opp.Name + \" (\" + accountName + \"), $\" + opp.Amount + \", closed \" + opp.CloseDate + \": \" + desc);\n    }\n  });\n});\n\nconst scored = competitors.map(c => {\n  const signal = mockSignals[c.name];\n  const loss = lossesByCompetitor[c.name];\n  const urgencyScore = signal.weight + (loss.count * 3);\n  return {\n    name: c.name,\n    currentPositioning: c.currentPositioning,\n    currentObjectionHandling: c.currentObjectionHandling,\n    signalType: signal.signalType,\n    signalDetail: signal.detail,\n    realLossCount: loss.count,\n    realLossTotalAmount: loss.totalAmount,\n    realLossSummary: loss.notes.length ? loss.notes.join(\" | \") : \"No real Closed Lost opportunities in the current data name this competitor.\",\n    urgencyScore: urgencyScore\n  };\n});\n\nscored.sort((a, b) => b.urgencyScore - a.urgencyScore);\nconst top = scored[0];\n\nlet urgencyLabel = \"Low\";\nif (top.urgencyScore >= 8) { urgencyLabel = \"High\"; }\nelse if (top.urgencyScore >= 4) { urgencyLabel = \"Medium\"; }\n\nconst allScoresSummary = scored.map(s => s.name + \": signal weight \" + mockSignals[s.name].weight + \" plus \" + s.realLossCount + \" real loss(es) = urgency \" + s.urgencyScore).join(\" | \");\n\nreturn [{\n  json: {\n    competitorName: top.name,\n    signalType: top.signalType,\n    signalDetail: top.signalDetail,\n    realLossCount: top.realLossCount,\n    realLossTotalAmount: top.realLossTotalAmount,\n    realLossSummary: top.realLossSummary,\n    previousPositioning: top.currentPositioning,\n    previousObjectionHandling: top.currentObjectionHandling,\n    urgencyScore: top.urgencyScore,\n    urgencyLabel: urgencyLabel,\n    allScoresSummary: allScoresSummary,\n    checkedAt: new Date().toISOString()\n  }\n}];"
      }
    },
    {
      "id": "274df37c-7e7b-4909-81d7-e45cc18d9c7b",
      "name": "Battlecard Update Drafter",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "typeVersion": 3.1,
      "position": [720, 300],
      "parameters": {
        "promptType": "define",
        "text": "=A competitive signal has been detected for Fortavault against {{ $json.competitorName }}.\n\nSignal type: {{ $json.signalType }}\nSignal detail: {{ $json.signalDetail }}\n\nReal Salesforce Closed Lost opportunities naming this competitor: {{ $json.realLossCount }} (total ${{ $json.realLossTotalAmount }})\n{{ $json.realLossSummary }}\n\nCurrent battlecard positioning:\n{{ $json.previousPositioning }}\n\nCurrent objection handling:\n{{ $json.previousObjectionHandling }}\n\nUrgency score: {{ $json.urgencyScore }} ({{ $json.urgencyLabel }})\nAll competitor scores this cycle: {{ $json.allScoresSummary }}\n\nDraft the updated battlecard section for this competitor.",
        "hasOutputParser": true,
        "options": {
          "systemMessage": "You are a competitive intelligence assistant for Fortavault, Inc., a fictional data protection and security platform (cloud backup, ransomware recovery, zero-trust access, compliance reporting). Products: Fortavault Backup Cloud, Fortavault Shield (ransomware detection plus one-click rollback), Fortavault Access (zero-trust), Fortavault Insights (compliance reporting). Key competitors: Veyron Data, Ashcombe Systems, Cyphertide, Northgate Cloud Security, Ridgeline Compliance. You are given a simulated competitive signal (pricing, review sentiment, funding, or layoffs) for one named competitor, plus real Salesforce Closed Lost opportunities whose loss-reason notes actually name that competitor. Revise the positioning paragraph and the objection-handling paragraph for this competitor so a rep reading it before a call understands what changed and why it matters, grounded specifically in the real losses given (cite the pattern in them, do not invent details not provided) and the simulated signal. If there are zero real losses for this competitor, say so explicitly rather than inventing one, and let the urgency rationale reflect that the update is signal-driven only. Write updatedPositioning and updatedObjectionHandling as plain, concise prose with no markdown formatting, no headers, no bullet lists, and no bold text - a rep needs to scan this in seconds, not read an essay. Each of those two fields must be at most 500 characters and 2-3 sentences. Keep urgencyRationale to one plain sentence, at most 220 characters. Respond with a JSON object with exactly three fields: updatedPositioning, updatedObjectionHandling, and urgencyRationale."
        }
      }
    },
    {
      "id": "23d39a57-45a8-42b6-8b11-3e727cca79ae",
      "name": "Anthropic Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
      "typeVersion": 1.5,
      "position": [720, 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": "97cf5b4f-45ad-42e9-9050-0209707fe7cc",
      "name": "Structured Output Parser",
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "typeVersion": 1.3,
      "position": [880, 500],
      "parameters": {
        "schemaType": "fromJson",
        "jsonSchemaExample": "{ \"updatedPositioning\": \"2-4 sentence revised positioning paragraph\", \"updatedObjectionHandling\": \"2-4 sentence revised objection handling paragraph\", \"urgencyRationale\": \"one sentence explaining why this competitor was chosen this cycle over the other four\" }"
      }
    },
    {
      "id": "7012a1dd-6c33-4167-96c6-e4a4f7c77ed8",
      "name": "Post Draft for Slack Approval",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.7,
      "position": [960, 300],
      "parameters": {
        "operation": "sendAndWait",
        "select": "channel",
        "channelId": { "__rl": true, "mode": "id", "value": "C0BQ3G96HL6", "cachedResultName": "general" },
        "message": "=🎯 Battlecard update proposed: {{ $(\"Analyze Competitive Signals\").item.json.competitorName }} (urgency: {{ $(\"Analyze Competitive Signals\").item.json.urgencyLabel }})\n\nSignal: {{ $(\"Analyze Competitive Signals\").item.json.signalDetail }}\n\nReal Salesforce losses naming this competitor: {{ $(\"Analyze Competitive Signals\").item.json.realLossCount }} (total ${{ $(\"Analyze Competitive Signals\").item.json.realLossTotalAmount }})\n\n*Updated positioning:*\n{{ $(\"Battlecard Update Drafter\").item.json.output.updatedPositioning.slice(0, 500) }}\n\n*Updated objection handling:*\n{{ $(\"Battlecard Update Drafter\").item.json.output.updatedObjectionHandling.slice(0, 500) }}\n\n*Why now:* {{ $(\"Battlecard Update Drafter\").item.json.output.urgencyRationale.slice(0, 220) }}\n\n_Full draft will be logged to the Battlecard Update Log Data Table on approval._",
        "approvalOptions": { "values": { "approvalType": "double", "approveLabel": "Approve Update", "disapproveLabel": "Skip" } },
        "options": {}
      },
      "webhookId": "fc87b451-2e18-414f-86d8-c2ae4b7c1409",
      "credentials": { "slackApi": { "id": "mv7TIi8kucFPqPg6", "name": "Slack account" } }
    },
    {
      "id": "0133c7c0-b2d5-4755-91b8-35556afe882b",
      "name": "Approved?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [1200, 300],
      "parameters": {
        "conditions": {
          "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "loose" },
          "conditions": [
            { "leftValue": "={{ $json.data.approved }}", "operator": { "type": "boolean", "operation": "equals" }, "rightValue": true }
          ],
          "combinator": "and"
        }
      }
    },
    {
      "id": "044d690b-7601-4fb0-9d9d-0575e67225ab",
      "name": "Log Battlecard Update",
      "type": "n8n-nodes-base.dataTable",
      "typeVersion": 1.1,
      "position": [1440, 200],
      "parameters": {
        "resource": "row",
        "operation": "insert",
        "dataTableId": { "__rl": true, "mode": "id", "value": "SiahCws4nC00xKL0", "cachedResultName": "Battlecard Update Log" },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "competitorName": "={{ $(\"Analyze Competitive Signals\").item.json.competitorName }}",
            "signalType": "={{ $(\"Analyze Competitive Signals\").item.json.signalType }}",
            "signalDetail": "={{ $(\"Analyze Competitive Signals\").item.json.signalDetail }}",
            "realLossCount": "={{ $(\"Analyze Competitive Signals\").item.json.realLossCount }}",
            "realLossTotalAmount": "={{ $(\"Analyze Competitive Signals\").item.json.realLossTotalAmount }}",
            "realLossSummary": "={{ $(\"Analyze Competitive Signals\").item.json.realLossSummary }}",
            "urgencyScore": "={{ $(\"Analyze Competitive Signals\").item.json.urgencyScore }}",
            "urgencyLabel": "={{ $(\"Analyze Competitive Signals\").item.json.urgencyLabel }}",
            "previousPositioning": "={{ $(\"Analyze Competitive Signals\").item.json.previousPositioning }}",
            "updatedPositioning": "={{ $(\"Battlecard Update Drafter\").item.json.output.updatedPositioning }}",
            "updatedObjectionHandling": "={{ $(\"Battlecard Update Drafter\").item.json.output.updatedObjectionHandling }}",
            "urgencyRationale": "={{ $(\"Battlecard Update Drafter\").item.json.output.urgencyRationale }}",
            "loggedAt": "={{ $now.toISO() }}"
          }
        },
        "options": {}
      }
    },
    {
      "id": "b68de204-6cc8-4ad2-bb1d-23807faea49b",
      "name": "Skip - Not Approved",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [1440, 420],
      "parameters": {}
    },
    {
      "id": "5f2ff240-fc3d-4997-8986-25680b5c2a11",
      "name": "Sticky Note eca9d894",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [100, 300],
      "parameters": {
        "content": "## Real vs. Simulated\nThe competitor signal feed (pricing/review/funding/layoff changes) is deliberately mocked - Veyron Data, Ashcombe Systems, Cyphertide, Northgate Cloud Security, and Ridgeline Compliance are fictional competitors with nothing real to scrape.\n\nThe grounding is real: the Salesforce query pulls actual Closed Lost opportunities whose loss-reason notes name a specific competitor, and those real counts/dollars drive which battlecard section gets updated and how urgently.",
        "color": 5
      }
    }
  ],
  "connections": {
    "Weekly Battlecard Check": { "main": [[{ "node": "Fetch Competitor-Named Closed Lost", "type": "main", "index": 0 }]] },
    "Fetch Competitor-Named Closed Lost": { "main": [[{ "node": "Analyze Competitive Signals", "type": "main", "index": 0 }]] },
    "Analyze Competitive Signals": { "main": [[{ "node": "Battlecard Update Drafter", "type": "main", "index": 0 }]] },
    "Battlecard Update Drafter": { "main": [[{ "node": "Post Draft for Slack Approval", "type": "main", "index": 0 }]] },
    "Anthropic Chat Model": { "ai_languageModel": [[{ "node": "Battlecard Update Drafter", "type": "ai_languageModel", "index": 0 }]] },
    "Structured Output Parser": { "ai_outputParser": [[{ "node": "Battlecard Update Drafter", "type": "ai_outputParser", "index": 0 }]] },
    "Post Draft for Slack Approval": { "main": [[{ "node": "Approved?", "type": "main", "index": 0 }]] },
    "Approved?": { "main": [[{ "node": "Log Battlecard Update", "type": "main", "index": 0 }], [{ "node": "Skip - Not Approved", "type": "main", "index": 0 }]] }
  },
  "settings": { "executionOrder": "v1" }
}
