{
  "name": "SignalForge-v4.1-KeywordWorker",
  "nodes": [
    {
      "id": "b1b2c3d4-0000-4000-8000-000000000001",
      "name": "Execute Workflow Trigger",
      "type": "n8n-nodes-base.executeWorkflowTrigger",
      "typeVersion": 1,
      "position": [
        240,
        300
      ],
      "parameters": {}
    },
    {
      "id": "b1b2c3d4-0000-4000-8000-000000000002",
      "name": "Cache Gate",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        440,
        300
      ],
      "parameters": {
        "jsCode": "const input = $input.first().json;\nconst st = $getWorkflowStaticData('global');\nif (!st.cache) st.cache = {};\n\nconst keyword = input.keyword;\nconst config = input.config;\nconst ttlMs = (config.cache_ttl_minutes || 30) * 60 * 1000;\nconst now = Date.now();\n\nconst cached = st.cache[keyword];\n\nif (cached && (now - cached.ts_epoch_ms < ttlMs)) {\n  return [{\n    json: {\n      keyword,\n      config,\n      cache_hit: true,\n      cached_items: cached.items || [],\n      cached_hits: 1,\n      api_calls: 0,\n      failed_calls: 0\n    }\n  }];\n}\n\nreturn [{\n  json: {\n    keyword,\n    config,\n    cache_hit: false,\n    cached_hits: 0,\n    api_calls: 0,\n    failed_calls: 0\n  }\n}];"
      }
    },
    {
      "id": "b1b2c3d4-0000-4000-8000-000000000003",
      "name": "IF Cache Hit",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        640,
        300
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true
          },
          "conditions": [
            {
              "leftValue": "={{ $json.cache_hit }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        }
      }
    },
    {
      "id": "b1b2c3d4-0000-4000-8000-000000000004",
      "name": "Emit Cached Items",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        840,
        200
      ],
      "parameters": {
        "jsCode": "const data = $json;\nif (!data.cached_items || data.cached_items.length === 0) {\n  return [{ json: { __empty: true, __stream: 'cache' } }];\n}\nreturn data.cached_items.map(i => ({\n  json: { ...i, meta: { ...i.meta, cached: true, api_calls: 0, failed_calls: 0 } }\n}));"
      }
    },
    {
      "id": "b1b2c3d4-0000-4000-8000-000000000005",
      "name": "Fetch HN",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        840,
        400
      ],
      "parameters": {
        "method": "GET",
        "url": "={{ 'https://hn.algolia.com/api/v1/search?query=' + encodeURIComponent($('Cache Gate').first().json.keyword) + '&tags=story&hitsPerPage=5' }}",
        "options": {
          "continueOnFail": true
        }
      }
    },
    {
      "id": "b1b2c3d4-0000-4000-8000-000000000006",
      "name": "Fetch Wikipedia",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        840,
        500
      ],
      "parameters": {
        "method": "GET",
        "url": "={{ 'https://en.wikipedia.org/w/api.php?action=query&list=search&srsearch=' + encodeURIComponent($('Cache Gate').first().json.keyword) + '&format=json&utf8=1&srlimit=5' }}",
        "options": {
          "continueOnFail": true
        }
      }
    },
    {
      "id": "b1b2c3d4-0000-4000-8000-000000000007",
      "name": "Fetch Crossref",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        840,
        600
      ],
      "parameters": {
        "method": "GET",
        "url": "={{ 'https://api.crossref.org/works?query=' + encodeURIComponent($('Cache Gate').first().json.keyword) + '&rows=5' }}",
        "options": {
          "continueOnFail": true
        }
      }
    },
    {
      "id": "b1b2c3d4-0000-4000-8000-000000000008",
      "name": "Normalize HN",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1040,
        400
      ],
      "parameters": {
        "jsCode": "const cacheGate = $('Cache Gate').first().json;\nconst keyword = cacheGate.keyword;\nconst data = $json;\n\nif (!data || !data.hits || data.error) {\n  return [{ json: { __empty: true, __src: 'hn', meta: { failed_calls: 1 } } }];\n}\n\nreturn data.hits.map(h => ({\n  json: {\n    source: 'hn',\n    keyword,\n    title: h.title,\n    url: h.url || `https://news.ycombinator.com/item?id=${h.objectID}`,\n    published_at: h.created_at ? new Date(h.created_at).toISOString() : null,\n    raw_score: (h.points || 0) + (h.num_comments || 0),\n    score: (h.points || 0) + (h.num_comments || 0),\n    summary_raw: null,\n    summary: null,\n    meta: { author: h.author, venue: 'Hacker News', cached: false, api_calls: 0, failed_calls: 0 }\n  }\n}));"
      }
    },
    {
      "id": "b1b2c3d4-0000-4000-8000-000000000009",
      "name": "Normalize Wikipedia",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1040,
        500
      ],
      "parameters": {
        "jsCode": "const cacheGate = $('Cache Gate').first().json;\nconst keyword = cacheGate.keyword;\nconst data = $json;\n\nif (!data || !data.query || !data.query.search || data.error) {\n  return [{ json: { __empty: true, __src: 'wikipedia', meta: { failed_calls: 1 } } }];\n}\n\nreturn data.query.search.map(hit => ({\n  json: {\n    source: 'wikipedia',\n    keyword,\n    title: hit.title,\n    url: `https://en.wikipedia.org/wiki/${encodeURIComponent(hit.title.replace(/ /g, '_'))}`,\n    published_at: null,\n    raw_score: Math.floor((hit.size || 0) / 100),\n    score: Math.floor((hit.size || 0) / 100),\n    summary_raw: hit.snippet || null,\n    summary: null,\n    meta: { author: null, venue: 'Wikipedia', cached: false, api_calls: 0, failed_calls: 0 }\n  }\n}));"
      }
    },
    {
      "id": "b1b2c3d4-0000-4000-8000-000000000010",
      "name": "Normalize Crossref",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1040,
        600
      ],
      "parameters": {
        "jsCode": "const cacheGate = $('Cache Gate').first().json;\nconst keyword = cacheGate.keyword;\nconst data = $json;\n\nif (!data || !data.message || !data.message.items || data.error) {\n  return [{ json: { __empty: true, __src: 'crossref', meta: { failed_calls: 1 } } }];\n}\n\nreturn data.message.items.map(item => ({\n  json: {\n    source: 'crossref',\n    keyword,\n    title: (item.title && item.title[0]) ? item.title[0] : 'Untitled',\n    url: item.URL || '',\n    published_at: (item['published-print'] && item['published-print']['date-time']) ? item['published-print']['date-time'] : null,\n    raw_score: 20 + (item['is-referenced-by-count'] || 0),\n    score: 20 + (item['is-referenced-by-count'] || 0),\n    summary_raw: null,\n    summary: null,\n    meta: {\n      author: item.author ? item.author[0].family : null,\n      venue: item['container-title'] ? item['container-title'][0] : 'Crossref',\n      cached: false,\n      api_calls: 0,\n      failed_calls: 0\n    }\n  }\n}));"
      }
    },
    {
      "id": "b1b2c3d4-0000-4000-8000-000000000011",
      "name": "Merge HN Wiki",
      "type": "n8n-nodes-base.merge",
      "typeVersion": 2.1,
      "position": [
        1240,
        450
      ],
      "parameters": {
        "mode": "append",
        "options": {}
      }
    },
    {
      "id": "b1b2c3d4-0000-4000-8000-000000000012",
      "name": "Merge All",
      "type": "n8n-nodes-base.merge",
      "typeVersion": 2.1,
      "position": [
        1440,
        500
      ],
      "parameters": {
        "mode": "append",
        "options": {}
      }
    },
    {
      "id": "b1b2c3d4-0000-4000-8000-000000000013",
      "name": "Cache Save",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1640,
        400
      ],
      "parameters": {
        "jsCode": "const cacheGate = $('Cache Gate').first().json;\nconst keyword = cacheGate.keyword;\nconst st = $getWorkflowStaticData('global');\nconst allItems = $input.all();\n\n// Count failed calls\nlet failedCalls = 0;\nconst validItems = allItems\n  .map(i => i.json)\n  .filter(i => {\n    if (i.__empty) {\n      if (i.meta?.failed_calls) failedCalls += i.meta.failed_calls;\n      else failedCalls++;\n      return false;\n    }\n    return true;\n  });\n\nst.cache[keyword] = {\n  ts_epoch_ms: Date.now(),\n  items: validItems\n};\n\nif (validItems.length === 0) {\n  return [{ json: { __empty: true, keyword, meta: { cached: false, api_calls: 3, failed_calls: failedCalls || 3 } } }];\n}\n\nreturn validItems.map(item => ({\n  json: {\n    ...item,\n    meta: { ...item.meta, cached: false, api_calls: 3, failed_calls: 0 }\n  }\n}));"
      }
    },
    {
      "id": "b1b2c3d4-0000-4000-8000-000000000014",
      "name": "Merge Cached",
      "type": "n8n-nodes-base.merge",
      "typeVersion": 2.1,
      "position": [
        1640,
        300
      ],
      "parameters": {
        "mode": "append",
        "options": {}
      }
    }
  ],
  "connections": {
    "Execute Workflow Trigger": {
      "main": [
        [
          {
            "node": "Cache Gate",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Cache Gate": {
      "main": [
        [
          {
            "node": "IF Cache Hit",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF Cache Hit": {
      "main": [
        [
          {
            "node": "Emit Cached Items",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Fetch HN",
            "type": "main",
            "index": 0
          },
          {
            "node": "Fetch Wikipedia",
            "type": "main",
            "index": 0
          },
          {
            "node": "Fetch Crossref",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Emit Cached Items": {
      "main": [
        [
          {
            "node": "Merge Cached",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch HN": {
      "main": [
        [
          {
            "node": "Normalize HN",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Wikipedia": {
      "main": [
        [
          {
            "node": "Normalize Wikipedia",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Crossref": {
      "main": [
        [
          {
            "node": "Normalize Crossref",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize HN": {
      "main": [
        [
          {
            "node": "Merge HN Wiki",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize Wikipedia": {
      "main": [
        [
          {
            "node": "Merge HN Wiki",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Merge HN Wiki": {
      "main": [
        [
          {
            "node": "Merge All",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize Crossref": {
      "main": [
        [
          {
            "node": "Merge All",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Merge All": {
      "main": [
        [
          {
            "node": "Cache Save",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Cache Save": {
      "main": [
        [
          {
            "node": "Merge Cached",
            "type": "main",
            "index": 1
          }
        ]
      ]
    }
  }
}