{
  "nodes": [
    {
      "id": "771d2476-bba5-4b92-b19f-ce64a0cc3c44",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1776,
        -240
      ],
      "parameters": {
        "width": 480,
        "height": 896,
        "content": "## Send daily AI-summarized competitor content digests to Slack and email via ScraperAPI\n\n### How it works\n\n1. Triggers start the workflow manually or automatically on a schedule.\n2. Initialization nodes prepare data tables for storing and comparing snapshots.\n3. Crawling nodes set and initiate web scraping through configured URLs.\n4. Webhook nodes receive and parse scrape data for further analysis.\n5. Processing nodes compare the scraped content with historical snapshots.\n6. Conditional nodes determine if content changes are significant and proceed accordingly.\n\n### Setup steps\n\n- [ ] Configure credentials for ScraperAPI to enable web crawling.\n- [ ] Set up webhook URLs to listen for crawl results.\n- [ ] Input competitor start URLs in the Configure URLs node.\n- [ ] Set up Slack API credentials for posting digests.\n\n### Customization\n\nUsers can customize the competitor start URLs to monitor different websites or sections."
      },
      "typeVersion": 1
    },
    {
      "id": "8492f07b-bdf5-4978-b741-fdef15ee135c",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1216,
        -240
      ],
      "parameters": {
        "color": 7,
        "width": 416,
        "height": 272,
        "content": "## Manual start workflow\n\nInitializes the workflow interactively"
      },
      "typeVersion": 1
    },
    {
      "id": "9a2f258c-7cf9-480e-af10-7858cd1eb643",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1216,
        80
      ],
      "parameters": {
        "color": 7,
        "width": 672,
        "height": 272,
        "content": "## Scheduled crawl trigger\n\nStarts the crawl process on a schedule"
      },
      "typeVersion": 1
    },
    {
      "id": "b23182f9-e284-4ae0-881b-b3cf9053dae1",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1216,
        480
      ],
      "parameters": {
        "color": 7,
        "width": 656,
        "height": 272,
        "content": "## Receive crawl results\n\nHandles incoming scraped data"
      },
      "typeVersion": 1
    },
    {
      "id": "0cfa23c7-d815-4688-a13c-16e87fec96d6",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -512,
        480
      ],
      "parameters": {
        "color": 7,
        "width": 672,
        "height": 272,
        "content": "## Content comparison\n\nCompares new crawled data with previous snapshots"
      },
      "typeVersion": 1
    },
    {
      "id": "366af2c0-efae-4842-94e8-b7c5a9484530",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        224,
        320
      ],
      "parameters": {
        "color": 7,
        "width": 688,
        "height": 576,
        "content": "## Handle content updates\n\nProcesses changes using AI and archives data"
      },
      "typeVersion": 1
    },
    {
      "id": "d948c147-cda0-4601-be20-614e76435271",
      "name": "Sticky Note6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1216,
        1040
      ],
      "parameters": {
        "color": 7,
        "width": 1136,
        "height": 272,
        "content": "## Daily digest workflow\n\nCompiles and sends daily summaries of changes"
      },
      "typeVersion": 1
    },
    {
      "id": "c7d73d82-6e22-4e42-8b72-faf4e4cc5fb7",
      "name": "Sticky Note7",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -32,
        944
      ],
      "parameters": {
        "color": 7,
        "width": 240,
        "height": 624,
        "content": "## Broadcast daily digest\n\nSends compiled updates via various channels"
      },
      "typeVersion": 1
    },
    {
      "id": "42c2af9b-62ab-4df7-9dff-e2779ccc290a",
      "name": "Sticky Note8",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        256,
        928
      ],
      "parameters": {
        "color": 7,
        "width": 352,
        "height": 496,
        "content": "## AI summarization\n\nUses AI to summarize content changes"
      },
      "typeVersion": 1
    },
    {
      "id": "setup-trigger",
      "name": "When Manual Execution Begins",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [
        -1168,
        -120
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "init-history-table",
      "name": "Initialize Data Table",
      "type": "n8n-nodes-base.dataTable",
      "position": [
        -944,
        -120
      ],
      "parameters": {
        "columns": {
          "column": [
            {
              "name": "url",
              "type": "string"
            },
            {
              "name": "last_content_hash",
              "type": "string"
            },
            {
              "name": "last_content",
              "type": "string"
            },
            {
              "name": "last_summary",
              "type": "string"
            },
            {
              "name": "last_change_at",
              "type": "string"
            },
            {
              "name": "last_seen_at",
              "type": "string"
            }
          ]
        },
        "options": {
          "createIfNotExists": true
        },
        "resource": "table",
        "operation": "create",
        "tableName": "competitor_content"
      },
      "typeVersion": 1.1
    },
    {
      "id": "daily-crawl-trigger",
      "name": "Daily Crawl Start Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -1168,
        200
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 6 * * *"
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "configure-urls",
      "name": "Set Competitor Start URLs",
      "type": "n8n-nodes-base.code",
      "position": [
        -944,
        200
      ],
      "parameters": {
        "jsCode": "// Edit the competitor sites to crawl below. Each entry is a *start URL*;\n// the crawler follows links within the site up to `maxDepth`, scraping\n// every page whose URL matches `includeRegexp`.\n//\n// IMPORTANT: paste the \"Crawl Result Webhook\" Production URL into\n// CALLBACK_URL before the first scheduled run \u2014 that is where ScraperAPI\n// streams each scraped page. (Open the webhook node to copy its URL.)\n//\n// `countryCode` sets the ScraperAPI proxy region (us, gb, de, fr, etc.).\n// Tighten `includeRegexp` to a section (e.g. /news, /blog) to control\n// credit spend \u2014 crawling a whole site with rendering can be expensive.\n\nconst CALLBACK_URL = 'https://YOUR-N8N-HOST/webhook/competitor-crawl';\n\nconst sites = [\n  { startUrl: 'https://www.anthropic.com/news', countryCode: 'us', includeRegexp: 'https://www\\\\.anthropic\\\\.com/news.*', maxDepth: 2 },\n  { startUrl: 'https://openai.com/blog', countryCode: 'us', includeRegexp: 'https://openai\\\\.com/(blog|index).*', maxDepth: 2 },\n  { startUrl: 'https://blog.google/technology/ai/', countryCode: 'us', includeRegexp: 'https://blog\\\\.google/technology/ai.*', maxDepth: 2 },\n];\n\nreturn sites.map((s, i) => ({\n  json: { ...s, callbackUrl: CALLBACK_URL },\n  pairedItem: { item: i },\n}));\n",
        "language": "javaScript"
      },
      "typeVersion": 2
    },
    {
      "id": "start-crawl-job",
      "name": "Start Crawl with ScraperAPI",
      "type": "n8n-nodes-scraperapi-official.scraperApi",
      "maxTries": 2,
      "position": [
        -688,
        200
      ],
      "parameters": {
        "resource": "crawler",
        "operation": "crawlerJobCreate",
        "crawlerMaxDepth": "={{ $json.maxDepth }}",
        "crawlerStartUrl": "={{ $json.startUrl }}",
        "crawlerCallbackUrl": "={{ $json.callbackUrl }}",
        "crawlerUrlRegexpInclude": "={{ $json.includeRegexp }}",
        "crawlerOptionalParameters": {
          "crawlerApiParameters": {
            "crawlerApiRender": true,
            "crawlerApiCountryCode": "={{ $json.countryCode }}",
            "crawlerApiOutputFormat": "markdown"
          },
          "crawlerScheduleInterval": "once"
        }
      },
      "retryOnFail": true,
      "typeVersion": 1,
      "continueOnFail": true,
      "waitBetweenTries": 2000
    },
    {
      "id": "crawl-result-webhook",
      "name": "Webhook for Crawl Results",
      "type": "n8n-nodes-base.webhook",
      "position": [
        -1168,
        600
      ],
      "parameters": {
        "path": "competitor-crawl",
        "options": {},
        "httpMethod": "POST",
        "responseMode": "onReceived"
      },
      "typeVersion": 2
    },
    {
      "id": "parse-crawl-result",
      "name": "Parse Received Crawl Data",
      "type": "n8n-nodes-base.code",
      "position": [
        -944,
        600
      ],
      "parameters": {
        "jsCode": "// ScraperAPI streams one POST per scraped page to this webhook, plus a\n// final job-summary POST when the crawl finishes. Keep only successful\n// per-page results; drop the summary and any failed fetches.\n\nconst payload = $json.body || $json;\nconst results = Array.isArray(payload) ? payload : [payload];\n\nconst out = [];\nfor (const r of results) {\n  const resp = r && r.response;\n  const status = resp && resp.statusCode;\n  // A page result has a url and a response.body string; the job summary\n  // (jobState / completed / failed) and failed fetches are skipped.\n  if (!r || !r.url || !resp || typeof resp.body !== 'string') continue;\n  if (status && status >= 400) continue;\n  out.push({\n    json: { url: r.url, body: resp.body, statusCode: status || 200 },\n    pairedItem: { item: 0 },\n  });\n}\n\nreturn out;\n",
        "language": "javaScript"
      },
      "typeVersion": 2
    },
    {
      "id": "extract-content",
      "name": "Limit Content Length",
      "type": "n8n-nodes-base.code",
      "position": [
        -704,
        600
      ],
      "parameters": {
        "jsCode": "// Clean the markdown body, cap length to keep AI cost predictable,\n// and compute a stable hash of the content.\n\nconst item = $input.first().json;\nconst body = (item && item.body) || '';\nconst url = item && item.url;\n\nconst cleaned = String(body)\n  .replace(/\\u0000/g, '')\n  .replace(/\\r\\n/g, '\\n')\n  .replace(/\\n{3,}/g, '\\n\\n')\n  .trim();\n\nconst MAX = 8000;\nconst content = cleaned.length > MAX ? cleaned.slice(0, MAX) + '\\n\u2026(truncated)' : cleaned;\n\n// FNV-1a 32-bit hash \u2014 fast, dependency-free, plenty for change detection.\nlet h = 2166136261;\nfor (let i = 0; i < content.length; i++) {\n  h ^= content.charCodeAt(i);\n  h = Math.imul(h, 16777619);\n}\nconst hash = (h >>> 0).toString(16);\n\nreturn [{\n  json: {\n    url,\n    content,\n    hash,\n    scraped_at: new Date().toISOString(),\n  },\n  pairedItem: { item: 0 },\n}];\n",
        "language": "javaScript"
      },
      "typeVersion": 2
    },
    {
      "id": "lookup-previous",
      "name": "Retrieve Previous Snapshot",
      "type": "n8n-nodes-base.dataTable",
      "position": [
        -464,
        600
      ],
      "parameters": {
        "limit": 1,
        "filters": {
          "conditions": [
            {
              "keyName": "url",
              "keyValue": "={{ $json.url }}",
              "condition": "eq"
            }
          ]
        },
        "resource": "row",
        "matchType": "anyCondition",
        "operation": "get",
        "returnAll": false,
        "dataTableId": {
          "__rl": true,
          "mode": "name",
          "value": "competitor_content"
        }
      },
      "typeVersion": 1.1,
      "alwaysOutputData": true
    },
    {
      "id": "compare-snapshots",
      "name": "Compare New and Old Snapshots",
      "type": "n8n-nodes-base.code",
      "position": [
        -224,
        600
      ],
      "parameters": {
        "jsCode": "// Combine the current scrape with whatever Retrieve Previous returned.\n// First-time URLs have no previous row \u2014 store them as a silent baseline\n// (no AI, no digest entry) so the digest only ever reports real changes.\n\nconst current = $('Limit Content Length').first().json;\nconst previous = $input.first().json || {};\n\nconst hasPrevious = !!previous.last_content_hash;\nconst hashChanged = hasPrevious && previous.last_content_hash !== current.hash;\nconst significant = current.content && current.content.length > 200; // skip near-empty pages\n\nreturn [{\n  json: {\n    url: current.url,\n    current_content: current.content,\n    current_hash: current.hash,\n    current_seen_at: current.scraped_at,\n    previous_content: hasPrevious ? (previous.last_content || '') : '',\n    previous_hash: hasPrevious ? previous.last_content_hash : '',\n    previous_seen_at: hasPrevious ? (previous.last_seen_at || 'unknown') : 'never',\n    first_seen: !hasPrevious,\n    changed: hashChanged && significant,\n  },\n  pairedItem: { item: 0 },\n}];\n",
        "language": "javaScript"
      },
      "typeVersion": 2
    },
    {
      "id": "has-content-changed",
      "name": "If Content Has Changed",
      "type": "n8n-nodes-base.if",
      "position": [
        16,
        600
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "changed-cond",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $json.changed }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "ai-summarize-change",
      "name": "AI-Driven Change Summarization",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        304,
        1056
      ],
      "parameters": {
        "text": "=Page: {{ $json.url }}\n\nPrevious snapshot (taken {{ $json.previous_seen_at }}):\n---\n{{ $json.previous_content }}\n---\n\nCurrent snapshot (taken {{ $json.current_seen_at }}):\n---\n{{ $json.current_content }}\n---\n\nSummarise material changes (launches, pricing, messaging, features, leadership moves) as 2\u20134 short bullet points, one per line, each starting with \"- \". Ignore navigation, footer, cookie banners, unchanged boilerplate, and trivial metric changes such as view, star, follower or subscriber counts, timestamps and tracking parameters. If there is no material change, respond with ONLY the token NO_MATERIAL_CHANGE and nothing else \u2014 do not also describe the trivial differences.",
        "options": {
          "maxIterations": 3,
          "systemMessage": "You are a competitive intelligence analyst. You receive snapshots of competitor web pages and produce short, factual summaries of what has changed since the previous snapshot. Be specific and concise. Cite concrete details (product names, prices, dates) when present.",
          "enableStreaming": false
        },
        "promptType": "define"
      },
      "typeVersion": 3.1
    },
    {
      "id": "anthropic-chat-model",
      "name": "Execute Claude Sonnet Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
      "position": [
        304,
        1296
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "claude-sonnet-4-6",
          "cachedResultName": "Claude Sonnet 4.6"
        },
        "options": {
          "temperature": 0.3,
          "maxTokensToSample": 1024
        }
      },
      "typeVersion": 1.5
    },
    {
      "id": "is-material-change",
      "name": "If Change is Significant",
      "type": "n8n-nodes-base.if",
      "position": [
        512,
        520
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "material-cond",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ ($json.output || '').trim() !== '' && !($json.output || '').toUpperCase().includes('NO_MATERIAL_CHANGE') }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "save-change",
      "name": "Log Snapshot Changes",
      "type": "n8n-nodes-base.dataTable",
      "position": [
        768,
        440
      ],
      "parameters": {
        "columns": {
          "value": {
            "url": "={{ $('Compare New and Old Snapshots').item.json.url }}",
            "last_content": "={{ $('Compare New and Old Snapshots').item.json.current_content }}",
            "last_seen_at": "={{ $('Compare New and Old Snapshots').item.json.current_seen_at }}",
            "last_summary": "={{ $json.output }}",
            "last_change_at": "={{ $('Compare New and Old Snapshots').item.json.current_seen_at }}",
            "last_content_hash": "={{ $('Compare New and Old Snapshots').item.json.current_hash }}"
          },
          "mappingMode": "defineBelow"
        },
        "filters": {
          "conditions": [
            {
              "keyName": "url",
              "keyValue": "={{ $('Compare New and Old Snapshots').item.json.url }}",
              "condition": "eq"
            }
          ]
        },
        "options": {},
        "resource": "row",
        "matchType": "anyCondition",
        "operation": "upsert",
        "dataTableId": {
          "__rl": true,
          "mode": "name",
          "value": "competitor_content"
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "save-snapshot",
      "name": "Archive Current Snapshot",
      "type": "n8n-nodes-base.dataTable",
      "position": [
        768,
        720
      ],
      "parameters": {
        "columns": {
          "value": {
            "url": "={{ $('Compare New and Old Snapshots').item.json.url }}",
            "last_content": "={{ $('Compare New and Old Snapshots').item.json.current_content }}",
            "last_seen_at": "={{ $('Compare New and Old Snapshots').item.json.current_seen_at }}",
            "last_content_hash": "={{ $('Compare New and Old Snapshots').item.json.current_hash }}"
          },
          "mappingMode": "defineBelow"
        },
        "filters": {
          "conditions": [
            {
              "keyName": "url",
              "keyValue": "={{ $('Compare New and Old Snapshots').item.json.url }}",
              "condition": "eq"
            }
          ]
        },
        "options": {},
        "resource": "row",
        "matchType": "anyCondition",
        "operation": "upsert",
        "dataTableId": {
          "__rl": true,
          "mode": "name",
          "value": "competitor_content"
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "daily-digest-trigger",
      "name": "Daily Digest Start Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -1168,
        1160
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 18 * * *"
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "retrieve-todays-changes",
      "name": "Fetch Today's Changes",
      "type": "n8n-nodes-base.dataTable",
      "position": [
        -944,
        1160
      ],
      "parameters": {
        "filters": {
          "conditions": [
            {
              "keyName": "last_change_at",
              "keyValue": "={{ $now.startOf('day').toISO() }}",
              "condition": "gte"
            }
          ]
        },
        "resource": "row",
        "matchType": "allConditions",
        "operation": "get",
        "returnAll": true,
        "dataTableId": {
          "__rl": true,
          "mode": "name",
          "value": "competitor_content"
        }
      },
      "executeOnce": true,
      "typeVersion": 1.1,
      "alwaysOutputData": true
    },
    {
      "id": "aggregate-changes",
      "name": "Compile Change Data",
      "type": "n8n-nodes-base.aggregate",
      "position": [
        -704,
        1160
      ],
      "parameters": {
        "include": "allFields",
        "options": {},
        "aggregate": "aggregateAllItemData",
        "destinationFieldName": "changes"
      },
      "typeVersion": 1
    },
    {
      "id": "has-changes-today",
      "name": "If Changes Exist Today",
      "type": "n8n-nodes-base.if",
      "position": [
        -464,
        1160
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "has-changes-cond",
              "operator": {
                "type": "number",
                "operation": "gt"
              },
              "leftValue": "={{ ($json.changes || []).filter(c => c.last_summary && !String(c.last_summary).toUpperCase().includes('NO_MATERIAL_CHANGE')).length }}",
              "rightValue": 0
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "build-digest",
      "name": "Build Today's Digest",
      "type": "n8n-nodes-base.code",
      "position": [
        -224,
        1160
      ],
      "parameters": {
        "jsCode": "// Only rows with a last_summary are material changes recorded today.\nconst changes = (($input.first().json || {}).changes || [])\n  .filter(c => c.last_summary && !String(c.last_summary).toUpperCase().includes('NO_MATERIAL_CHANGE'));\n\nconst esc = (s) => String(s == null ? '' : s).replace(/[&<>\"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',\"'\":'&#39;'}[c]));\n\n// The agent emits one material change per line (optionally prefixed with a\n// \"- \"/\"*\"/\"\u2022\" marker). Split into bullet points; fall back to the whole\n// summary as a single bullet if it ever arrives as one block.\nconst toBullets = (summary) => {\n  const lines = String(summary == null ? '' : summary)\n    .split(/\\r?\\n+/)\n    .map(l => l.replace(/^\\s*[-*\u2022]\\s*/, '').trim())\n    .filter(Boolean);\n  return lines.length ? lines : [String(summary == null ? '' : summary).trim()];\n};\n\nconst today = new Date().toISOString().slice(0, 10);\n\n// Count total individual bullet points across all competitors\nconst totalBullets = changes.reduce((sum, c) => sum + toBullets(c.last_summary).length, 0);\nconst subject = `Competitor content update \u2014 ${totalBullets} change${totalBullets === 1 ? '' : 's'} on ${today}`;\n\nconst rowsHtml = changes.map(c => {\n  const items = toBullets(c.last_summary)\n    .map(b => `<li style=\"margin-bottom:4px;line-height:1.5\">${esc(b)}</li>`)\n    .join('');\n  return `<tr>\n  <td style=\"padding:12px;border:1px solid #ddd;vertical-align:top;width:30%\">\n    <a href=\"${esc(c.url)}\" style=\"color:#1f6fcc;text-decoration:none;word-break:break-all\" target=\"_blank\">${esc(c.url)}</a>\n  </td>\n  <td style=\"padding:12px;border:1px solid #ddd;vertical-align:top\">\n    <ul style=\"margin:0;padding-left:20px\">${items}</ul>\n  </td>\n</tr>`;\n}).join('');\n\nconst html = `<div style=\"font-family:-apple-system,Segoe UI,sans-serif;max-width:1200px\">\n  <h2 style=\"margin:0 0 12px\">${esc(subject)}</h2>\n  <p style=\"color:#555;margin:0 0 16px\">Pages monitored via ScraperAPI. Summaries written by AI on detected changes.</p>\n  <table style=\"width:100%;border-collapse:collapse;border:1px solid #ddd\">\n    <thead>\n      <tr style=\"background-color:#f5f5f5\">\n        <th style=\"padding:12px;border:1px solid #ddd;text-align:left;font-weight:600\">URL</th>\n        <th style=\"padding:12px;border:1px solid #ddd;text-align:left;font-weight:600\">Changes Detected</th>\n      </tr>\n    </thead>\n    <tbody>${rowsHtml}</tbody>\n  </table>\n</div>`;\n\nconst slackBlocks = JSON.stringify({\n  blocks: [\n    { type: 'header', text: { type: 'plain_text', text: subject } },\n    ...changes.flatMap(c => ([\n      { type: 'section', text: { type: 'mrkdwn', text: `*<${c.url}|${c.url}>*\\n${toBullets(c.last_summary).map(b => `\u2022 ${b}`).join('\\n')}` } },\n      { type: 'divider' },\n    ])),\n  ],\n});\n\nconst text = changes.map(c => `${c.url}\\n${toBullets(c.last_summary).map(b => `  \u2022 ${b}`).join('\\n')}`).join('\\n\\n');\n\nreturn [{ json: { count: totalBullets, subject, html, text, slackBlocks } }];",
        "language": "javaScript"
      },
      "typeVersion": 2
    },
    {
      "id": "post-to-slack",
      "name": "Post Daily Digest to Slack",
      "type": "n8n-nodes-base.slack",
      "position": [
        16,
        1080
      ],
      "parameters": {
        "text": "={{ $json.subject }}",
        "select": "channel",
        "blocksUi": "={{ $json.slackBlocks }}",
        "resource": "message",
        "channelId": {
          "__rl": true,
          "mode": "list",
          "value": ""
        },
        "operation": "post",
        "messageType": "block",
        "otherOptions": {
          "includeLinkToWorkflow": false
        }
      },
      "typeVersion": 2.4
    },
    {
      "id": "send-digest-gmail",
      "name": "Email Digest via Gmail",
      "type": "n8n-nodes-base.gmail",
      "position": [
        16,
        1240
      ],
      "parameters": {
        "sendTo": "",
        "message": "={{ $json.html }}",
        "options": {},
        "subject": "={{ $json.subject }}",
        "resource": "message",
        "emailType": "html",
        "operation": "send"
      },
      "typeVersion": 2.2
    },
    {
      "id": "send-digest-smtp",
      "name": "Send Digest with SMTP",
      "type": "n8n-nodes-base.emailSend",
      "position": [
        16,
        1400
      ],
      "parameters": {
        "html": "={{ $json.html }}",
        "options": {},
        "subject": "={{ $json.subject }}",
        "toEmail": "",
        "fromEmail": "",
        "emailFormat": "html"
      },
      "typeVersion": 2.1
    }
  ],
  "connections": {
    "Compile Change Data": {
      "main": [
        [
          {
            "node": "If Changes Exist Today",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Today's Digest": {
      "main": [
        [
          {
            "node": "Post Daily Digest to Slack",
            "type": "main",
            "index": 0
          },
          {
            "node": "Email Digest via Gmail",
            "type": "main",
            "index": 0
          },
          {
            "node": "Send Digest with SMTP",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Limit Content Length": {
      "main": [
        [
          {
            "node": "Retrieve Previous Snapshot",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Today's Changes": {
      "main": [
        [
          {
            "node": "Compile Change Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If Changes Exist Today": {
      "main": [
        [
          {
            "node": "Build Today's Digest",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    },
    "If Content Has Changed": {
      "main": [
        [
          {
            "node": "AI-Driven Change Summarization",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Archive Current Snapshot",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If Change is Significant": {
      "main": [
        [
          {
            "node": "Log Snapshot Changes",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Archive Current Snapshot",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Daily Crawl Start Trigger": {
      "main": [
        [
          {
            "node": "Set Competitor Start URLs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Received Crawl Data": {
      "main": [
        [
          {
            "node": "Limit Content Length",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Competitor Start URLs": {
      "main": [
        [
          {
            "node": "Start Crawl with ScraperAPI",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Webhook for Crawl Results": {
      "main": [
        [
          {
            "node": "Parse Received Crawl Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Daily Digest Start Trigger": {
      "main": [
        [
          {
            "node": "Fetch Today's Changes",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Retrieve Previous Snapshot": {
      "main": [
        [
          {
            "node": "Compare New and Old Snapshots",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Execute Claude Sonnet Model": {
      "ai_languageModel": [
        [
          {
            "node": "AI-Driven Change Summarization",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "When Manual Execution Begins": {
      "main": [
        [
          {
            "node": "Initialize Data Table",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Compare New and Old Snapshots": {
      "main": [
        [
          {
            "node": "If Content Has Changed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AI-Driven Change Summarization": {
      "main": [
        [
          {
            "node": "If Change is Significant",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}