{
  "name": "Competitive intel tracker",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 5 * * *"
            }
          ]
        }
      },
      "id": "2d2d2d2d-0002-0000-0000-000000000001",
      "name": "Daily Cron — 5am UTC",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1,
      "position": [240, 300],
      "notesInFlow": true,
      "notes": "Crawl runs daily at 05:00 in the workflow timezone (set in Settings). Digest fan-out is gated to Mondays only by the Weekly-Digest IF node."
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT\n  page_id,\n  competitor_name,\n  page_type,\n  url,\n  last_content_hash,\n  last_content_text,\n  last_seen_at\nFROM competitor_tracked_pages\nWHERE active = true\nORDER BY competitor_name, page_type\nLIMIT 200;",
        "options": {}
      },
      "id": "2d2d2d2d-0002-0000-0000-000000000002",
      "name": "Pull Tracked Pages",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.4,
      "position": [460, 300],
      "credentials": {
        "postgres": {
          "id": "PLACEHOLDER_POSTGRES_CRED_ID",
          "name": "Postgres — competitive-intel"
        }
      },
      "notesInFlow": true,
      "notes": "Source-of-truth table for the tracked-pages list. Twenty to thirty rows is typical; cap at 200 to fail closed if the list grows unmanageably."
    },
    {
      "parameters": {
        "batchSize": 1,
        "options": {
          "reset": false
        }
      },
      "id": "2d2d2d2d-0002-0000-0000-000000000003",
      "name": "Iterate One Page At A Time",
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 3,
      "position": [680, 300],
      "notesInFlow": true,
      "notes": "Batch size 1 — each iteration handles one URL so per-page failure does not abort the run. Pair with a Wait node downstream to throttle."
    },
    {
      "parameters": {
        "amount": 4,
        "unit": "seconds"
      },
      "id": "2d2d2d2d-0002-0000-0000-000000000004",
      "name": "Throttle — 4s Between Fetches",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [900, 300],
      "notesInFlow": true,
      "notes": "Spreads ~30 fetches over ~2 minutes. Combined with one-request-per-page-per-day this keeps us well under any reasonable rate limit."
    },
    {
      "parameters": {
        "method": "GET",
        "url": "={{ $json.url }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            { "name": "User-Agent", "value": "ooligo-intel-bot/1.0 (+https://ooligo.com/bots)" },
            { "name": "Accept", "value": "text/html,application/xhtml+xml" }
          ]
        },
        "options": {
          "timeout": 20000,
          "redirect": {
            "redirect": {
              "followRedirects": true,
              "maxRedirects": 3
            }
          },
          "response": {
            "response": {
              "fullResponse": true,
              "neverError": true
            }
          }
        }
      },
      "id": "2d2d2d2d-0002-0000-0000-000000000005",
      "name": "Fetch Page HTML",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [1120, 300],
      "notesInFlow": true,
      "notes": "neverError:true so a 403/503 from anti-bot does not kill the batch — we record it and move on."
    },
    {
      "parameters": {
        "jsCode": "// Strip noise from the HTML, normalize, and hash. The 'noise' is anything that\n// re-renders on every deploy without representing a content change: build IDs,\n// CSRF tokens, current-year strings, server-rendered timestamps, CDN cache\n// busters in asset URLs. Without this filter the digest fires every day with\n// nothing actually changed and the Slack channel gets muted within a week.\n\nconst page = $('Iterate One Page At A Time').item.json;\nconst response = $json;\nconst statusCode = response.statusCode || response.status || 0;\nconst rawBody = typeof response.body === 'string' ? response.body : JSON.stringify(response.body || '');\n\nfunction stripNoise(html) {\n  return html\n    // Remove <script> and <style> blocks entirely\n    .replace(/<script[\\s\\S]*?<\\/script>/gi, '')\n    .replace(/<style[\\s\\S]*?<\\/style>/gi, '')\n    .replace(/<noscript[\\s\\S]*?<\\/noscript>/gi, '')\n    .replace(/<!--[\\s\\S]*?-->/g, '')\n    // Strip all tags to plain text\n    .replace(/<[^>]+>/g, ' ')\n    // Decode common entities\n    .replace(/&nbsp;/g, ' ').replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '\"')\n    // Mask volatile values\n    .replace(/\\b\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?Z?\\b/g, '<TS>')\n    .replace(/\\b(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]*\\s+\\d{1,2},?\\s+20\\d{2}\\b/g, '<DATE>')\n    .replace(/\\b20\\d{2}\\b/g, '<YEAR>')\n    .replace(/[a-f0-9]{32,}/gi, '<HASH>')\n    .replace(/\\b[A-Z0-9]{16,}\\b/g, '<TOKEN>')\n    // Collapse whitespace\n    .replace(/\\s+/g, ' ')\n    .trim();\n}\n\nconst normalized = stripNoise(rawBody);\n\nconst crypto = require('crypto');\nconst contentHash = crypto.createHash('sha256').update(normalized).digest('hex');\n\n// Materiality pre-filter: very small diffs are not worth a Claude call.\nconst prevText = page.last_content_text || '';\nconst lengthDelta = Math.abs(normalized.length - prevText.length);\nconst lengthRatio = prevText.length === 0 ? 1 : lengthDelta / prevText.length;\n\nreturn [{\n  json: {\n    page_id: page.page_id,\n    competitor_name: page.competitor_name,\n    page_type: page.page_type,\n    url: page.url,\n    fetch_status: statusCode,\n    fetched_at: new Date().toISOString(),\n    new_hash: contentHash,\n    old_hash: page.last_content_hash || null,\n    new_text: normalized,\n    old_text: prevText,\n    hash_changed: contentHash !== (page.last_content_hash || ''),\n    length_delta_pct: Math.round(lengthRatio * 1000) / 10,\n    fetch_ok: statusCode >= 200 && statusCode < 400 && rawBody.length > 200\n  }\n}];"
      },
      "id": "2d2d2d2d-0002-0000-0000-000000000006",
      "name": "Normalize + Hash",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [1340, 300]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "fetch-ok",
              "leftValue": "={{ $json.fetch_ok }}",
              "rightValue": true,
              "operator": { "type": "boolean", "operation": "equal" }
            },
            {
              "id": "hash-changed",
              "leftValue": "={{ $json.hash_changed }}",
              "rightValue": true,
              "operator": { "type": "boolean", "operation": "equal" }
            },
            {
              "id": "had-prior-snapshot",
              "leftValue": "={{ $json.old_text }}",
              "rightValue": "",
              "operator": { "type": "string", "operation": "notEmpty" }
            },
            {
              "id": "non-trivial-delta",
              "leftValue": "={{ $json.length_delta_pct }}",
              "rightValue": 0.5,
              "operator": { "type": "number", "operation": "gte" }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "2d2d2d2d-0002-0000-0000-000000000007",
      "name": "Material Change?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [1560, 300],
      "notesInFlow": true,
      "notes": "Four-part gate: fetch succeeded, hash differs, we have a prior snapshot to compare against, and length delta exceeds 0.5% (filters out single-character or whitespace-only edits)."
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.anthropic.com/v1/messages",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            { "name": "anthropic-version", "value": "2023-06-01" },
            { "name": "content-type", "value": "application/json" }
          ]
        },
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"model\": \"claude-sonnet-4-6\",\n  \"max_tokens\": 400,\n  \"system\": \"You compare two snapshots of a competitor's public web page and report what changed in a way that helps a B2B sales team. Output rules: (1) If the diff is cosmetic, navigation-only, footer-only, or you cannot identify a specific factual delta, return exactly the string NO_CHANGE on a single line. Nothing else. (2) Otherwise return two short sentences. Sentence one: what changed (a price, a feature, a target customer, a hire, a positioning shift). Sentence two: why a salesperson should care (a new objection to pre-empt, a new wedge to use, a new threat to flag). Do not invent details that are not in the diff. Do not speculate about strategy. Do not pad with generic commentary.\",\n  \"messages\": [\n    {\n      \"role\": \"user\",\n      \"content\": \"Competitor: {{ $json.competitor_name }}\\nPage type: {{ $json.page_type }}\\nURL: {{ $json.url }}\\n\\n--- PREVIOUS SNAPSHOT ---\\n{{ $json.old_text.slice(0, 6000) }}\\n\\n--- CURRENT SNAPSHOT ---\\n{{ $json.new_text.slice(0, 6000) }}\"\n    }\n  ]\n}",
        "options": {}
      },
      "id": "2d2d2d2d-0002-0000-0000-000000000008",
      "name": "Claude — Diff + Summarize",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [1780, 200],
      "credentials": {
        "httpHeaderAuth": {
          "id": "PLACEHOLDER_ANTHROPIC_CRED_ID",
          "name": "Anthropic — x-api-key"
        }
      },
      "notesInFlow": true,
      "notes": "Snapshots truncated to 6000 chars each — keeps input ≤ ~3k tokens per page. NO_CHANGE sentinel is the model's escape hatch when the diff is noisy."
    },
    {
      "parameters": {
        "jsCode": "// Pull the model's text out of the Anthropic response and decide whether to keep it.\nconst page = $('Material Change?').item.json;\nconst resp = $json;\nconst summary = (resp?.content?.[0]?.text || '').trim();\nconst isNoChange = summary === '' || summary === 'NO_CHANGE' || /^NO_CHANGE\\b/i.test(summary);\n\nreturn [{\n  json: {\n    page_id: page.page_id,\n    competitor_name: page.competitor_name,\n    page_type: page.page_type,\n    url: page.url,\n    new_hash: page.new_hash,\n    new_text: page.new_text,\n    summary,\n    is_material: !isNoChange,\n    summarized_at: new Date().toISOString(),\n    input_tokens: resp?.usage?.input_tokens || null,\n    output_tokens: resp?.usage?.output_tokens || null\n  }\n}];"
      },
      "id": "2d2d2d2d-0002-0000-0000-000000000009",
      "name": "Parse Claude Response",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [2000, 200]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "INSERT INTO competitor_change_log (\n  page_id, competitor_name, page_type, url,\n  content_hash, summary, is_material, detected_at\n) VALUES ($1, $2, $3, $4, $5, $6, $7, now())\nRETURNING id;\n\nUPDATE competitor_tracked_pages\nSET\n  last_content_hash = $5,\n  last_content_text = $8,\n  last_seen_at = now()\nWHERE page_id = $1;",
        "options": {
          "queryReplacement": "={{ $json.page_id }},{{ $json.competitor_name }},{{ $json.page_type }},{{ $json.url }},{{ $json.new_hash }},{{ JSON.stringify($json.summary) }},{{ $json.is_material }},{{ JSON.stringify($json.new_text) }}"
        }
      },
      "id": "2d2d2d2d-0002-0000-0000-00000000000a",
      "name": "Persist Change + Update Snapshot",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.4,
      "position": [2220, 200],
      "credentials": {
        "postgres": {
          "id": "PLACEHOLDER_POSTGRES_CRED_ID",
          "name": "Postgres — competitive-intel"
        }
      },
      "notesInFlow": true,
      "notes": "Two statements: append to the change log (audit trail), then advance the snapshot. is_material flag drives the weekly digest filter."
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "UPDATE competitor_tracked_pages\nSET\n  last_content_hash = COALESCE($2, last_content_hash),\n  last_content_text = COALESCE($3, last_content_text),\n  last_seen_at = now()\nWHERE page_id = $1;",
        "options": {
          "queryReplacement": "={{ $json.page_id }},{{ $json.fetch_ok ? $json.new_hash : null }},{{ $json.fetch_ok ? JSON.stringify($json.new_text) : null }}"
        }
      },
      "id": "2d2d2d2d-0002-0000-0000-00000000000b",
      "name": "Touch Snapshot (No Material Change)",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.4,
      "position": [1780, 400],
      "credentials": {
        "postgres": {
          "id": "PLACEHOLDER_POSTGRES_CRED_ID",
          "name": "Postgres — competitive-intel"
        }
      },
      "notesInFlow": true,
      "notes": "False branch: still advances the stored hash so the next run compares against the latest content, but does NOT spend a Claude call or write to the change log."
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "30 14 * * 1"
            }
          ]
        }
      },
      "id": "2d2d2d2d-0002-0000-0000-00000000000c",
      "name": "Weekly Digest Cron — Mon 14:30",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1,
      "position": [240, 700],
      "notesInFlow": true,
      "notes": "Independent trigger. Mondays at 14:30 in the workflow timezone — Tuesday morning for APAC, mid-morning for EU, breakfast for the US east coast."
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT\n  competitor_name,\n  json_agg(\n    json_build_object(\n      'page_type', page_type,\n      'url', url,\n      'summary', summary,\n      'detected_at', detected_at\n    ) ORDER BY detected_at DESC\n  ) AS changes\nFROM competitor_change_log\nWHERE is_material = true\n  AND detected_at >= now() - interval '7 days'\nGROUP BY competitor_name\nORDER BY competitor_name;",
        "options": {}
      },
      "id": "2d2d2d2d-0002-0000-0000-00000000000d",
      "name": "Aggregate Last 7 Days Of Material Changes",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.4,
      "position": [460, 700],
      "credentials": {
        "postgres": {
          "id": "PLACEHOLDER_POSTGRES_CRED_ID",
          "name": "Postgres — competitive-intel"
        }
      }
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "have-changes",
              "leftValue": "={{ $json.competitor_name }}",
              "rightValue": "",
              "operator": { "type": "string", "operation": "notEmpty" }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "2d2d2d2d-0002-0000-0000-00000000000e",
      "name": "Anything To Report?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [680, 700],
      "notesInFlow": true,
      "notes": "Silent weeks stay silent — no 'no updates this week' filler messages. The channel never fires unless there is something actually worth reading."
    },
    {
      "parameters": {
        "jsCode": "// Render one Slack Block Kit payload per competitor with material changes this week.\nconst c = $json;\nconst changes = c.changes || [];\nconst blocks = [\n  {\n    type: 'header',\n    text: { type: 'plain_text', text: `Competitor update — ${c.competitor_name}`, emoji: false }\n  },\n  {\n    type: 'context',\n    elements: [\n      { type: 'mrkdwn', text: `${changes.length} material change${changes.length === 1 ? '' : 's'} in the last 7 days` }\n    ]\n  },\n  { type: 'divider' }\n];\nfor (const ch of changes) {\n  blocks.push({\n    type: 'section',\n    text: {\n      type: 'mrkdwn',\n      text: `*${ch.page_type}* — <${ch.url}|view page>\\n${ch.summary}`\n    }\n  });\n}\nreturn [{\n  json: {\n    competitor_name: c.competitor_name,\n    blocks,\n    fallback_text: `Competitor update — ${c.competitor_name} (${changes.length} material change${changes.length === 1 ? '' : 's'} this week)`\n  }\n}];"
      },
      "id": "2d2d2d2d-0002-0000-0000-00000000000f",
      "name": "Compose Slack Blocks",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [900, 700]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://slack.com/api/chat.postMessage",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "httpHeaderAuth",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            { "name": "content-type", "value": "application/json; charset=utf-8" }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"channel\": \"#competitive-intel\",\n  \"text\": {{ JSON.stringify($json.fallback_text) }},\n  \"blocks\": {{ JSON.stringify($json.blocks) }},\n  \"unfurl_links\": false,\n  \"unfurl_media\": false\n}",
        "options": {}
      },
      "id": "2d2d2d2d-0002-0000-0000-000000000010",
      "name": "Slack — Post Weekly Digest",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [1120, 700],
      "credentials": {
        "httpHeaderAuth": {
          "id": "PLACEHOLDER_SLACK_CRED_ID",
          "name": "Slack — bot token"
        }
      },
      "notesInFlow": true,
      "notes": "One message per competitor, not one mega-post — sales reps mute long unbroken digests. Update channel name to your team's actual channel."
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "intel-on-demand",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "2d2d2d2d-0002-0000-0000-000000000011",
      "name": "On-Demand Webhook (Slack Slash Command)",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [240, 1100],
      "notesInFlow": true,
      "notes": "Wire a Slack slash command (e.g. /whatsnew acme) to this URL. Slack POSTs form-encoded body with text=<competitor query>."
    },
    {
      "parameters": {
        "jsCode": "// Parse Slack slash command payload, normalize the competitor name.\nconst body = $json.body || $json;\nconst raw = (body.text || '').trim();\nif (!raw) {\n  return [{ json: { error: 'Usage: /whatsnew <competitor>', _respond_immediately: true } }];\n}\nreturn [{ json: { query: raw.toLowerCase(), response_url: body.response_url || null } }];"
      },
      "id": "2d2d2d2d-0002-0000-0000-000000000012",
      "name": "Parse Slash Command",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [460, 1100]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT\n  competitor_name,\n  page_type,\n  url,\n  summary,\n  detected_at\nFROM competitor_change_log\nWHERE is_material = true\n  AND lower(competitor_name) LIKE '%' || $1 || '%'\n  AND detected_at >= now() - interval '90 days'\nORDER BY detected_at DESC\nLIMIT 10;",
        "options": {
          "queryReplacement": "={{ $json.query }}"
        }
      },
      "id": "2d2d2d2d-0002-0000-0000-000000000013",
      "name": "Fetch On-Demand History",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.4,
      "position": [680, 1100],
      "credentials": {
        "postgres": {
          "id": "PLACEHOLDER_POSTGRES_CRED_ID",
          "name": "Postgres — competitive-intel"
        }
      }
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={\n  \"response_type\": \"ephemeral\",\n  \"text\": {{ JSON.stringify(($input.all().length === 0 ? 'No material changes recorded in the last 90 days.' : 'Last ' + $input.all().length + ' material changes:')) }},\n  \"blocks\": {{ JSON.stringify($input.all().map(i => ({ type: 'section', text: { type: 'mrkdwn', text: '*' + i.json.competitor_name + ' — ' + i.json.page_type + '* (' + new Date(i.json.detected_at).toISOString().slice(0,10) + ')\\n' + i.json.summary + '\\n<' + i.json.url + '|view page>' } }))) }}\n}",
        "options": {}
      },
      "id": "2d2d2d2d-0002-0000-0000-000000000014",
      "name": "Respond To Slack",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [900, 1100]
    }
  ],
  "connections": {
    "Daily Cron — 5am UTC": {
      "main": [
        [{ "node": "Pull Tracked Pages", "type": "main", "index": 0 }]
      ]
    },
    "Pull Tracked Pages": {
      "main": [
        [{ "node": "Iterate One Page At A Time", "type": "main", "index": 0 }]
      ]
    },
    "Iterate One Page At A Time": {
      "main": [
        [{ "node": "Throttle — 4s Between Fetches", "type": "main", "index": 0 }]
      ]
    },
    "Throttle — 4s Between Fetches": {
      "main": [
        [{ "node": "Fetch Page HTML", "type": "main", "index": 0 }]
      ]
    },
    "Fetch Page HTML": {
      "main": [
        [{ "node": "Normalize + Hash", "type": "main", "index": 0 }]
      ]
    },
    "Normalize + Hash": {
      "main": [
        [{ "node": "Material Change?", "type": "main", "index": 0 }]
      ]
    },
    "Material Change?": {
      "main": [
        [{ "node": "Claude — Diff + Summarize", "type": "main", "index": 0 }],
        [{ "node": "Touch Snapshot (No Material Change)", "type": "main", "index": 0 }]
      ]
    },
    "Claude — Diff + Summarize": {
      "main": [
        [{ "node": "Parse Claude Response", "type": "main", "index": 0 }]
      ]
    },
    "Parse Claude Response": {
      "main": [
        [{ "node": "Persist Change + Update Snapshot", "type": "main", "index": 0 }]
      ]
    },
    "Persist Change + Update Snapshot": {
      "main": [
        [{ "node": "Iterate One Page At A Time", "type": "main", "index": 0 }]
      ]
    },
    "Touch Snapshot (No Material Change)": {
      "main": [
        [{ "node": "Iterate One Page At A Time", "type": "main", "index": 0 }]
      ]
    },
    "Weekly Digest Cron — Mon 14:30": {
      "main": [
        [{ "node": "Aggregate Last 7 Days Of Material Changes", "type": "main", "index": 0 }]
      ]
    },
    "Aggregate Last 7 Days Of Material Changes": {
      "main": [
        [{ "node": "Anything To Report?", "type": "main", "index": 0 }]
      ]
    },
    "Anything To Report?": {
      "main": [
        [{ "node": "Compose Slack Blocks", "type": "main", "index": 0 }],
        []
      ]
    },
    "Compose Slack Blocks": {
      "main": [
        [{ "node": "Slack — Post Weekly Digest", "type": "main", "index": 0 }]
      ]
    },
    "On-Demand Webhook (Slack Slash Command)": {
      "main": [
        [{ "node": "Parse Slash Command", "type": "main", "index": 0 }]
      ]
    },
    "Parse Slash Command": {
      "main": [
        [{ "node": "Fetch On-Demand History", "type": "main", "index": 0 }]
      ]
    },
    "Fetch On-Demand History": {
      "main": [
        [{ "node": "Respond To Slack", "type": "main", "index": 0 }]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1",
    "timezone": "Europe/London",
    "saveDataErrorExecution": "all",
    "saveDataSuccessExecution": "all",
    "saveManualExecutions": true
  },
  "versionId": "2d2d2d2d-0002-0000-0000-0000000000ff",
  "meta": {
    "templateCreatedBy": "ooligo",
    "instanceId": "ooligo-pilot"
  },
  "id": "competitive-intel-tracker",
  "tags": [
    { "name": "revops" },
    { "name": "competitive-intel" },
    { "name": "sales-enablement" }
  ]
}
