{
  "name": "DevOps Daily Digest RSS 2 Discord",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 6 * * *"
            }
          ]
        }
      },
      "id": "98201cd3-bb19-4a9c-9604-f3b9db9bf63b",
      "name": "Schedule Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "const sources = [\n  { name: 'InfoQ', url: 'https://www.infoq.com/RSS/articles/', color: '0x3B82F6' },\n  { name: 'The New Stack', url: 'https://thenewstack.io/feed/', color: '0x01aef4' },\n  { name: 'CNCF Blog', url: 'https://www.cncf.io/blog/feed/', color: '0x008080' },\n  { name: 'Kubernetes Blog', url: 'https://kubernetes.io/feed.xml', color: '0x326CE5' },\n  { name: 'The Register DevOps', url: 'https://www.theregister.com/devops/headlines.atom', color: '0xDC2626' },\n  { name: 'Ars Technica', url: 'https://feeds.arstechnica.com/arstechnica/index', color: '0x800080' },\n  { name: 'Cloudflare', url: 'https://developers.cloudflare.com/changelog/rss/index.xml', color: '0xF59E0B' },\n  { name: 'AWS Containers', url: 'https://aws.amazon.com/blogs/containers/feed/', color: '0xFF9900' },\n  { name: 'AWS News', url: 'https://aws.amazon.com/blogs/aws/feed/', color: '0xFF9900' },\n  { name: 'AWS AI', url: 'https://aws.amazon.com/blogs/machine-learning/feed/', color: '0xFF9900' },\n  { name: 'Bleeping Computer', url: 'https://www.bleepingcomputer.com/feed/', color: '0x03F4FC' },\n  { name: 'The Hacker News', url: 'https://feeds.feedburner.com/TheHackersNews', color: '0x3B35AE' }\n];\n\nreturn sources.map(source => ({ json: source }));"
      },
      "id": "c5fae01c-a2e4-4c0d-b5f6-ba3d19043625",
      "name": "Source List",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        240,
        0
      ]
    },
    {
      "parameters": {
        "url": "={{ $json.url }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "User-Agent",
              "value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"
            },
            {
              "name": "Accept",
              "value": "application/rss+xml, application/atom+xml, application/xml, text/xml;q=0.9, */*;q=0.8"
            },
            {
              "name": "Accept-Language",
              "value": "en-US,en;q=0.9"
            }
          ]
        },
        "options": {}
      },
      "id": "573305e8-32d4-4ce8-8433-9948f7a7267f",
      "name": "Fetch Feed XML",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        480,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "const decodeEntities = (s) => String(s || '')\n  .replace(/<!\\[CDATA\\[([\\s\\S]*?)\\]\\]>/g, '$1')\n  .replace(/&#(\\d+);/g, (_, n) => String.fromCharCode(Number(n)))\n  .replace(/&#x([0-9a-fA-F]+);/g, (_, h) => String.fromCharCode(parseInt(h, 16)))\n  .replace(/&nbsp;/g, ' ')\n  .replace(/&lt;/g, '<')\n  .replace(/&gt;/g, '>')\n  .replace(/&amp;/g, '&')\n  .replace(/&quot;/g, '\"')\n  .replace(/&#39;/g, \"'\");\n\nconst decode = (s) => decodeEntities(s)\n  .replace(/<[^>]*>/g, ' ')\n  .replace(/\\s+/g, ' ')\n  .trim();\n\nconst getTag = (block, tag) => {\n  const re = new RegExp(`<${tag}[^>]*>([\\\\s\\\\S]*?)<\\\\/${tag}>`, 'i');\n  const m = block.match(re);\n  return m ? decode(m[1]) : '';\n};\n\nconst getAttr = (block, tag, attr) => {\n  const re = new RegExp(`<${tag}[^>]*\\\\s${attr}=(['\"])(.*?)\\\\1[^>]*\\\\/?>`, 'i');\n  const m = block.match(re);\n  return m ? m[2].trim() : '';\n};\n\nconst getAttrs = (block, tag, attr) => {\n  const re = new RegExp(`<${tag}[^>]*\\\\s${attr}=(['\"])(.*?)\\\\1[^>]*\\\\/?>`, 'gi');\n  return [...block.matchAll(re)]\n    .map(match => match[2].trim())\n    .filter(Boolean);\n};\n\nconst normalizeLink = (raw) => {\n  try {\n    const u = new URL(String(raw || '').trim());\n    const dropParams = [\n      'utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n      'utm_id', 'utm_name', 'utm_cid', 'utm_reader', 'utm_referrer',\n      'fbclid', 'gclid', 'mc_cid', 'mc_eid', 'igshid'\n    ];\n    for (const p of dropParams) u.searchParams.delete(p);\n    u.hash = '';\n    return u.toString();\n  } catch (e) {\n    return String(raw || '').trim();\n  }\n};\n\nconst normalizeAssetUrl = (raw, base) => {\n  try {\n    const value = String(raw || '').trim();\n    if (!value) return '';\n    const u = base ? new URL(value, String(base || '').trim()) : new URL(value);\n    u.hash = '';\n    return u.toString();\n  } catch (e) {\n    return String(raw || '').trim();\n  }\n};\n\nconst parseColor = (value) => {\n  if (Number.isInteger(value)) {\n    return value;\n  }\n\n  const raw = String(value || '').trim();\n  if (!raw) {\n    return null;\n  }\n\n  if (/^0x[0-9a-fA-F]+$/i.test(raw)) {\n    return Number.parseInt(raw.slice(2), 16);\n  }\n\n  const parsed = Number(raw);\n  return Number.isInteger(parsed) ? parsed : null;\n};\n\nconst extractImage = (block, link, sourceUrl) => {\n  const candidates = [\n    ...getAttrs(block, 'media:thumbnail', 'url'),\n    ...getAttrs(block, 'media:content', 'url'),\n    ...getAttrs(block, 'enclosure', 'url')\n  ];\n\n  const atomEnclosureMatch = block.match(/<link\\b[^>]*rel=(['\"])[^'\"]*enclosure[^'\"]*\\1[^>]*href=(['\"])(.*?)\\2[^>]*>/i);\n  if (atomEnclosureMatch) {\n    candidates.push(atomEnclosureMatch[3].trim());\n  }\n\n  const htmlish = decodeEntities(block);\n  const imgMatches = [...htmlish.matchAll(/<img[^>]*\\ssrc=(['\"])(.*?)\\1[^>]*>/gi)]\n    .map(match => match[2].trim())\n    .filter(Boolean);\n  candidates.push(...imgMatches);\n\n  for (const candidate of candidates) {\n    const url = normalizeAssetUrl(candidate, link || sourceUrl);\n    if (/^https?:\\/\\//i.test(url)) {\n      return url;\n    }\n  }\n\n  return '';\n};\n\nconst sourceItems = $('Source List').all().map(item => item.json || {});\nconst out = [];\n\nfor (const [index, item] of items.entries()) {\n  const j = item.json || {};\n  const sourceMeta = sourceItems[index] || {};\n  const source = String(sourceMeta.name || j.name || 'Unknown Source').trim();\n  const sourceUrl = String(sourceMeta.url || j.url || '').trim();\n  const sourceColor = parseColor(sourceMeta.color);\n\n  let xml = '';\n  if (typeof j === 'string') {\n    xml = j;\n  } else if (typeof j.body === 'string') {\n    xml = j.body;\n  } else if (typeof j.data === 'string') {\n    xml = j.data;\n  } else if (typeof j.response === 'string') {\n    xml = j.response;\n  } else {\n    const firstXmlish = Object.values(j).find(v => typeof v === 'string' && v.trim().startsWith('<'));\n    xml = firstXmlish || '';\n  }\n\n  if (!xml) continue;\n\n  const atomMatches = xml.match(/<entry[\\s\\S]*?<\\/entry>/gi) || [];\n  for (const block of atomMatches) {\n    const title = getTag(block, 'title');\n    const rawLink = getAttr(block, 'link', 'href') || getTag(block, 'id');\n    const link = normalizeLink(rawLink);\n    const pubDate = getTag(block, 'updated') || getTag(block, 'published');\n    const summary = getTag(block, 'summary') || getTag(block, 'content');\n    const rawId = getTag(block, 'id') || rawLink || `${title}|${pubDate}`;\n    const id = normalizeLink(rawId);\n    const thumbnail = extractImage(block, link, sourceUrl);\n\n    if (title && link) {\n      out.push({\n        json: {\n          id,\n          title,\n          link,\n          pubDate,\n          summary: summary.slice(0, 500),\n          source,\n          sourceColor,\n          thumbnail,\n          category: ''\n        }\n      });\n    }\n  }\n\n  const rssMatches = xml.match(/<item[\\s\\S]*?<\\/item>/gi) || [];\n  for (const block of rssMatches) {\n    const title = getTag(block, 'title');\n    const rawLink = getTag(block, 'link');\n    const link = normalizeLink(rawLink);\n    const pubDate = getTag(block, 'pubDate') || getTag(block, 'dc:date');\n    const summary = getTag(block, 'description') || getTag(block, 'content:encoded') || getTag(block, 'content');\n    const guid = getTag(block, 'guid');\n    const category = (block.match(/<category[^>]*>([\\s\\S]*?)<\\/category>/gi) || [])\n      .map(x => decode(x.replace(/<category[^>]*>/i, '').replace(/<\\/category>/i, '')))\n      .join(', ');\n    const rawId = guid || rawLink || `${title}|${pubDate}`;\n    const id = normalizeLink(rawId);\n    const thumbnail = extractImage(block, link, sourceUrl);\n\n    if (title && link) {\n      out.push({\n        json: {\n          id,\n          title,\n          link,\n          pubDate,\n          summary: summary.slice(0, 500),\n          source,\n          sourceColor,\n          thumbnail,\n          category\n        }\n      });\n    }\n  }\n}\n\nreturn out;"
      },
      "id": "4ff0dc28-1421-49d5-bce0-b64ebda14336",
      "name": "Parse Feeds",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        720,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "const now = Date.now();\nconst cutoff = now - (24 * 60 * 60 * 1000);\n\nreturn items.filter(item => {\n  const rawDate = item.json.pubDate || '';\n  if (!rawDate) return false;\n\n  const timestamp = new Date(rawDate).getTime();\n  if (Number.isNaN(timestamp)) return false;\n\n  return timestamp >= cutoff && timestamp <= now;\n});"
      },
      "id": "af8ae0c4-ef74-43bc-a23a-34089f3d36cf",
      "name": "Last 24h Filter",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        960,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "const keywords = [\n  'devops', 'sre', 'platform', 'kubernetes', 'k8s', 'docker', 'container',\n  'terraform', 'grafana', 'prometheus', 'loki', 'tempo', 'cloudflare',\n  'cloud native', 'observability', 'monitoring', 'security', 'cve',\n  'supply chain', 'ci/cd', 'github actions', 'deployment', 'release',\n  'cluster', 'runtime', 'ingress', 'proxy', 'postgres', 'nginx', 'caddy',\n  'AI', 'ML', 'machine learning', 'artificial intelligence', 'genai',\n  'generative AI', 'LLM', 'LLMOps', 'MLOps', 'RAG', 'MCP', 'GPT',\n  'ChatGPT', 'Claude', 'OpenAI', 'Anthropic', 'Gemini', 'DeepMind',\n  'Llama', 'Mistral', 'Hugging Face', 'HuggingFace', 'LangChain',\n  'LlamaIndex', 'vLLM', 'Ollama', 'Bedrock', 'SageMaker', 'Vertex AI',\n  'PyTorch', 'TensorFlow', 'CUDA', 'fine-tuning', 'fine tuning',\n  'embedding', 'embeddings', 'vector database', 'vector db', 'multimodal',\n  'prompt engineering', 'AI agent', 'AI agents', 'agentic', 'copilot',\n  'cybersecurity', 'infosec', 'application security', 'appsec', 'secops',\n  'devsecops', 'cloud security', 'network security', 'endpoint security',\n  'identity', 'iam', 'sso', 'oauth', 'oidc', 'saml', 'zero trust',\n  'authentication', 'authorization', 'secrets management', 'vault',\n  'encryption', 'phishing', 'malware', 'ransomware', 'botnet', 'breach',\n  'data breach', 'incident response', 'threat intel', 'threat intelligence',\n  'threat detection', 'threat hunting', 'ioc', 'siem', 'soar', 'xdr', 'edr',\n  'waf', 'ddos', 'exploit', 'zero-day', 'zero day', 'vulnerability',\n  'vulnerabilities', 'cvss', 'patch management', 'sast', 'dast', 'sca',\n  'pentest', 'penetration testing', 'red team', 'blue team', 'owasp',\n  'container security', 'kubernetes security', 'runtime security',\n  'supply chain security', 'software bill of materials', 'sbom'\n];\n\nconst excludedKeywords = [\n  'deals','certified', 'discount', 'microsoft'];\n// Microsoft? Because - https://tenor.com/view/programming-code-vibe-coding-program-gif-3049533733041174981\nconst escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\nconst buildKeywordPatterns = values => values.map(value => {\n  const pattern = escapeRegExp(String(value).toLowerCase().trim()).replace(/\\s+/g, '\\\\s+');\n  return new RegExp(`\\\\b${pattern}\\\\b`, 'i');\n});\nconst keywordPatterns = buildKeywordPatterns(keywords);\nconst excludedKeywordPatterns = buildKeywordPatterns(excludedKeywords);\n\nreturn items.filter(item => {\n  const text = `${item.json.title || ''} ${item.json.summary || ''} ${item.json.category || ''}`.toLowerCase();\n  const hasExcludedKeyword = excludedKeywordPatterns.some(pattern => pattern.test(text));\n  if (hasExcludedKeyword) {\n    return false;\n  }\n\n  const score = keywordPatterns.reduce((n, pattern) => n + (pattern.test(text) ? 1 : 0), 0);\n  item.json.score = score;\n  return score >= 1;\n});"
      },
      "id": "c1032ce0-9878-4bbf-8cdf-f518103adc19",
      "name": "Relevance Filter",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1200,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "const sorted = [...items].sort((a, b) => {\n  const ta = new Date(a.json.pubDate || 0).getTime() || 0;\n  const tb = new Date(b.json.pubDate || 0).getTime() || 0;\n  return tb - ta;\n});\n\nreturn sorted.slice(0, 50);"
      },
      "id": "142d1f2d-3fe3-4d6d-9e9e-6dad796ed23f",
      "name": "Limit 50",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1440,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "const seen = new Set();\nconst out = [];\n\nfor (const item of items) {\n  const id = String(item.json.id || '').trim();\n  if (!id) continue;\n  if (seen.has(id)) continue;\n  seen.add(id);\n  out.push(item);\n}\n\nreturn out;"
      },
      "id": "e96feccc-5b35-4636-be21-f2ad6a1cfcb9",
      "name": "In-Run Dedup",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1680,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "const ids = items.map(item => String(item.json.id || '').trim()).filter(Boolean);\n\nreturn [\n  {\n    json: {\n      ids,\n      originalItems: items.map(item => item.json)\n    }\n  }\n];"
      },
      "id": "dc0f6e65-9a35-4f32-bee3-6eeeff761131",
      "name": "Collect IDs",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1920,
        0
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT COALESCE(array_agg(item_id), ARRAY[]::text[]) AS existing_ids\nFROM rss_seen_items\nWHERE item_id = ANY($1::text[]);",
        "options": {
          "queryReplacement": "={{ [$json.ids] }}"
        }
      },
      "id": "211e5dc7-aba7-4996-a714-6cb7268f151e",
      "name": "Postgres - Get Existing IDs",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        2160,
        0
      ],
      "credentials": {
        "postgres": {
          "id": "ZrfpTDC6ozlV0sIB",
          "name": "LocalPGSQL"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const source = $('Collect IDs').first().json;\nconst originalItems = Array.isArray(source.originalItems) ? source.originalItems : [];\nconst existingIds = new Set(Array.isArray(items[0]?.json?.existing_ids) ? items[0].json.existing_ids : []);\n\nreturn originalItems\n  .filter(item => {\n    const id = String(item.id || '').trim();\n    return id && !existingIds.has(id);\n  })\n  .map(item => ({ json: item }));"
      },
      "id": "9297ed66-5494-417d-8afc-32210d281a9a",
      "name": "Filter Unseen",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2400,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "const clean = (s, max) => {\n  const v = String(s || '').replace(/\\s+/g, ' ').trim();\n  return v.length > max ? v.slice(0, max - 1) + '…' : v;\n};\n\nconst normalizeThumbnailUrl = (value) => {\n  const raw = String(value || '').trim();\n  if (!raw) {\n    return '';\n  }\n\n  const lower = raw.toLowerCase();\n  if (!lower.startsWith('http://') && !lower.startsWith('https://')) {\n    return '';\n  }\n\n  try {\n    return encodeURI(raw);\n  } catch (e) {\n    return raw;\n  }\n};\n\nconst sorted = [...items].sort((a, b) => {\n  const ta = new Date(a.json.pubDate || 0).getTime() || 0;\n  const tb = new Date(b.json.pubDate || 0).getTime() || 0;\n  return tb - ta;\n});\n\nif (sorted.length === 0) {\n  return [];\n}\n\nconst date = new Date().toISOString().slice(0, 10);\nconst batches = [];\n\nfor (let i = 0; i < sorted.length; i += 10) {\n  const batch = sorted.slice(i, i + 10);\n  const embeds = batch.map((item, idx) => {\n    const embed = {\n      title: `${i + idx + 1}. ${clean(item.json.title, 240)}`,\n      url: item.json.link,\n      description: clean(item.json.summary || item.json.category || item.json.link, 350),\n      footer: {\n        text: clean(item.json.source, 120)\n      }\n    };\n\n    const color = Number(item.json.sourceColor);\n    if (Number.isInteger(color)) {\n      embed.color = color;\n    }\n\n    const thumbnail = normalizeThumbnailUrl(item.json.thumbnail);\n    if (thumbnail) {\n      embed.image = { url: thumbnail };\n    }\n\n    return embed;\n  });\n\n  const payload = {\n    content: i === 0\n      ? `**DevOps Digest - ${date}**`\n      : `**DevOps Digest - ${date}** (continued)`,\n    embeds\n  };\n\n  batches.push({\n    json: {\n      payload,\n      sentItems: batch.map(x => x.json)\n    }\n  });\n}\n\nreturn batches;"
      },
      "id": "54e6d521-e53d-47c1-b3f0-a91c5f3ce76e",
      "name": "Build Discord Embed Payloads",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2640,
        0
      ]
    },
    {
      "parameters": {
        "options": {}
      },
      "id": "7672c2dc-dbcd-4c12-bb5d-ede8bd223dfa",
      "name": "Loop Over Batches",
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 3,
      "position": [
        2880,
        0
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://discord.com/api/webhooks/1482672959700074530/m5-V8Tn2x7hudmqOMZoWtBBEJPSK1N1NLjRP9BrpzlJo1BNNJS4GXrCptEhLdpYb0K5q",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ $json.payload }}",
        "options": {}
      },
      "id": "f61f4fae-1615-4eb1-81d7-94f2dc717dfa",
      "name": "Post to Discord Webhook",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        3120,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "const batch = $('Loop Over Batches').item.json || {};\nconst batchItems = Array.isArray(batch.sentItems) ? batch.sentItems : [];\n\nreturn batchItems\n  .map(item => ({\n    json: {\n      item_id: String(item.id || '').trim(),\n      link: String(item.link || '').trim(),\n      source: String(item.source || '').trim(),\n      pub_date: String(item.pubDate || '').trim()\n    }\n  }))\n  .filter(item => item.json.item_id);"
      },
      "id": "d4a7f469-f59d-472b-b563-43d2ff62cb96",
      "name": "Prepare Seen Rows",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3360,
        0
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "INSERT INTO rss_seen_items (item_id, link, source, pub_date)\nVALUES ($1, $2, $3, NULLIF($4, '')::timestamptz)\nON CONFLICT (item_id) DO NOTHING;",
        "options": {
          "queryReplacement": "={{ [$json.item_id, $json.link, $json.source, $json.pub_date] }}"
        }
      },
      "id": "11f19275-682f-403e-b0a6-d822dfed234d",
      "name": "Postgres - Insert Seen Rows",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        3600,
        0
      ],
      "credentials": {
        "postgres": {
          "id": "ZrfpTDC6ozlV0sIB",
          "name": "LocalPGSQL"
        }
      }
    },
    {
      "parameters": {
        "amount": 2
      },
      "id": "cba2027f-044f-4afb-8711-856d09096503",
      "name": "Wait 2 Seconds",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [
        3840,
        0
      ],
      "webhookId": "74fd667a-33ce-4fd6-8a31-2211ed2a9877"
    }
  ],
  "pinData": {},
  "connections": {
    "Schedule Trigger": {
      "main": [
        [
          {
            "node": "Source List",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Source List": {
      "main": [
        [
          {
            "node": "Fetch Feed XML",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Feed XML": {
      "main": [
        [
          {
            "node": "Parse Feeds",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Feeds": {
      "main": [
        [
          {
            "node": "Last 24h Filter",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Last 24h Filter": {
      "main": [
        [
          {
            "node": "Relevance Filter",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Relevance Filter": {
      "main": [
        [
          {
            "node": "Limit 50",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "In-Run Dedup": {
      "main": [
        [
          {
            "node": "Collect IDs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Collect IDs": {
      "main": [
        [
          {
            "node": "Postgres - Get Existing IDs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Postgres - Get Existing IDs": {
      "main": [
        [
          {
            "node": "Filter Unseen",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter Unseen": {
      "main": [
        [
          {
            "node": "Build Discord Embed Payloads",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Discord Embed Payloads": {
      "main": [
        [
          {
            "node": "Loop Over Batches",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Over Batches": {
      "main": [
        [],
        [
          {
            "node": "Post to Discord Webhook",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Post to Discord Webhook": {
      "main": [
        [
          {
            "node": "Prepare Seen Rows",
            "type": "main",
            "index": 0
          },
          {
            "node": "Wait 2 Seconds",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Seen Rows": {
      "main": [
        [
          {
            "node": "Postgres - Insert Seen Rows",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait 2 Seconds": {
      "main": [
        [
          {
            "node": "Loop Over Batches",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Limit 50": {
      "main": [
        [
          {
            "node": "In-Run Dedup",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "a7087a01-cbca-4e6a-b4ab-64e3fff30342",
  "meta": {
    "templateCredsSetupCompleted": true,
    "instanceId": "826d977a1f81442269bc5eaada3539569e634026f4d88dc268e79caba7166643"
  },
  "id": "0BVF1GSFapJDpkLX",
  "tags": []
}