{
  "name": "Mycroft News Intelligence Agent",
  "nodes": [
    {
      "parameters": {
        "language": "python",
        "pythonCode": "COMPANIES = [\n    # AI & Semiconductors\n    {\"symbol\": \"NVDA\", \"name\": \"NVIDIA Corporation\", \"aliases\": [\"nvidia\",\"nvda\",\"jensen huang\"], \"riskMultiplier\": 1.0},\n    {\"symbol\": \"AMD\", \"name\": \"Advanced Micro Devices\", \"aliases\": [\"amd\",\"lisa su\"], \"riskMultiplier\": 0.9},\n    {\"symbol\": \"TSM\", \"name\": \"Taiwan Semiconductor Manufacturing Company\", \"aliases\": [\"tsmc\",\"taiwan semiconductor\"], \"riskMultiplier\": 0.85},\n\n    # Big Tech / Cloud\n    {\"symbol\": \"MSFT\", \"name\": \"Microsoft Corporation\", \"aliases\": [\"microsoft\",\"msft\",\"azure\",\"copilot\",\"satya nadella\"], \"riskMultiplier\": 0.85},\n    {\"symbol\": \"AAPL\", \"name\": \"Apple Inc.\", \"aliases\": [\"apple\",\"aapl\",\"tim cook\"], \"riskMultiplier\": 0.85},\n    {\"symbol\": \"GOOGL\", \"name\": \"Alphabet Inc.\", \"aliases\": [\"google\",\"alphabet\",\"sundar pichai\"], \"riskMultiplier\": 0.85},\n    {\"symbol\": \"AMZN\", \"name\": \"Amazon.com Inc.\", \"aliases\": [\"amazon\",\"amzn\",\"andy jassy\"], \"riskMultiplier\": 0.85},\n\n    # AI / Software\n    {\"symbol\": \"META\", \"name\": \"Meta Platforms Inc.\", \"aliases\": [\"meta\",\"facebook\",\"instagram\",\"mark zuckerberg\"], \"riskMultiplier\": 0.9},\n    {\"symbol\": \"CRM\", \"name\": \"Salesforce Inc.\", \"aliases\": [\"salesforce\",\"crm\",\"marc benioff\"], \"riskMultiplier\": 0.8},\n    {\"symbol\": \"SNOW\", \"name\": \"Snowflake Inc.\", \"aliases\": [\"snowflake\",\"snow\"], \"riskMultiplier\": 0.8},\n    {\"symbol\": \"PLTR\", \"name\": \"Palantir Technologies\", \"aliases\": [\"palantir\",\"pltr\",\"alex karp\"], \"riskMultiplier\": 0.95},\n\n    # Finance\n    {\"symbol\": \"JPM\", \"name\": \"JPMorgan Chase & Co.\", \"aliases\": [\"jpmorgan\",\"jpm\",\"jamie dimon\"], \"riskMultiplier\": 0.85},\n    {\"symbol\": \"GS\", \"name\": \"Goldman Sachs Group\", \"aliases\": [\"goldman sachs\",\"gs\",\"david solomon\"], \"riskMultiplier\": 0.85},\n    {\"symbol\": \"MS\", \"name\": \"Morgan Stanley\", \"aliases\": [\"morgan stanley\",\"ms\",\"james gorman\"], \"riskMultiplier\": 0.8}\n]\n\nreturn [{\"json\": {\"companies\": COMPANIES, \"ts\": __import__(\"datetime\").datetime.utcnow().isoformat()}}]\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -496,
        -32
      ],
      "id": "97ee6542-7f2c-41c8-a919-2f6dc0c815e9",
      "name": "Company List"
    },
    {
      "parameters": {
        "language": "python",
        "pythonCode": "# --- Inputs ---\ncompanies = items[0][\"json\"][\"companies\"]\n\n# === knobs you can tweak ===\nLOOKBACK_HOURS = 72              # was 24\nUSE_KEYWORDS   = False           # start False to confirm data flow; turn True later\nUSE_DOMAINS    = False           # start False; turn True after you see results\n\n# high-signal outlets (enable by setting USE_DOMAINS=True)\nDOMAINS = \"reuters.com,bloomberg.com,cnbc.com,wsj.com\"\n\n# finance terms used if USE_KEYWORDS=True\nEXTRA = \"(earnings OR revenue OR guidance OR outlook OR profit OR loss OR upgrade OR downgrade OR lawsuit OR acquisition)\"\n\nLANG = \"en\"\nSORT = \"publishedAt\"\nPAGE_SIZE = 100\n\nfrom datetime import datetime, timedelta, timezone\ndef iso_hours_ago(h):\n    return (datetime.now(timezone.utc) - timedelta(hours=h)).isoformat()\n\ncompanies = items[0][\"json\"][\"companies\"]\n\nout = []\nfor idx, c in enumerate(companies, start=1):\n    name   = c.get(\"name\",\"\").strip()\n    symbol = c.get(\"symbol\",\"\").strip()\n\n    # Per-company query keeps it short + targeted\n    base_q = f\"{symbol} OR {name}\".strip()\n    q = f\"({base_q}) AND {EXTRA}\" if USE_KEYWORDS else base_q\n\n    payload = {\n        \"q\": q,\n        \"from\": iso_hours_ago(LOOKBACK_HOURS),\n        \"language\": LANG,\n        \"sortBy\": SORT,\n        \"pageSize\": PAGE_SIZE,\n        \"company\": symbol,\n        \"batch\": idx,\n        \"total_batches\": len(companies)\n    }\n    if USE_DOMAINS:\n        payload[\"domains\"] = DOMAINS\n\n    out.append({\"json\": payload})\n\nreturn out\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -288,
        -32
      ],
      "id": "3053fc61-aa7e-4447-a63c-ec4f0eb7d772",
      "name": "Build Query"
    },
    {
      "parameters": {},
      "type": "n8n-nodes-base.merge",
      "typeVersion": 3.2,
      "position": [
        752,
        -144
      ],
      "id": "3b39facc-41a2-4a9b-a22d-98ed01aaeb6e",
      "name": "Merge"
    },
    {
      "parameters": {
        "language": "python",
        "pythonCode": "import hashlib, re\n\ndef clean(t):\n    t = re.sub(r\"\\t.+?\\n\", \"\", t or \"\")  # Changed \\l to \\n (newline)\n    t = re.sub(r\"https?://\\S+\", \"\", t)  \n    t = re.sub(r\"\\s+\", \" \", t)\n    t = re.sub(r\"[^\\w\\s\\.\\,\\!\\?\\-]\", \"\", t)\n    return t.strip()\n\nout = []\nfor it in items:\n    a = it[\"json\"]  # Already parsed, no json.loads() needed\n    title = (a.get(\"title\") or \"\").strip()\n    url = (a.get(\"url\") or \"\").strip()\n    # ISO is already like 2025-08-12T04:00:00Z\n    iso = a.get(\"publishedAt\")\n    \n    # Clean the title and description\n    clean_title = clean(title)\n    description = (a.get(\"description\") or \"\").strip()\n    clean_description = clean(description)\n    \n    # Combine title and description for content processing\n    content = f\"{clean_title} {clean_description}\".strip()\n    \n    # Create a unique hash for deduplication\n    content_hash = hashlib.md5(content.encode()).hexdigest()\n    \n    # Create the processed item\n    processed_item = {\n        \"title\": title,\n        \"url\": url,\n        \"publishedAt\": iso,\n        \"description\": description,\n        \"processed_text\": content,\n        \"content_hash\": content_hash,\n        \"source\": a.get(\"source\", \"\"),\n        \"content\": a.get(\"content\", \"\")\n    }\n    \n    out.append({\"json\": processed_item})\n\nreturn out"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        352,
        -192
      ],
      "id": "6474e321-b504-4fae-b092-c537a8091027",
      "name": "normalizenewsapi"
    },
    {
      "parameters": {
        "url": "https://newsapi.org/v2/everything?apiKey=REPLACE_WITH_CREDENTIAL",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "q",
              "value": "={{ $json.q }}"
            },
            {
              "name": "from",
              "value": "={{ $json.from }}"
            },
            {
              "name": "language",
              "value": "={{ $json.language }}"
            },
            {
              "name": "sortBy",
              "value": "={{ $json.sortBy }}"
            },
            {
              "name": "pageSize",
              "value": "={{ $json.pageSize }}"
            },
            {
              "name": "excludeDomains",
              "value": "=prtimes.jp,prtimes.co.jp,asahi.com,nikkei.com"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        -64,
        -192
      ],
      "id": "f4f8dea5-b1bd-4d47-9f04-f88b3e4afdf3",
      "name": "NewsApiKey",
      "alwaysOutputData": true,
      "retryOnFail": true
    },
    {
      "parameters": {},
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        -800,
        -16
      ],
      "id": "ee472bdb-c1cf-4200-ae6d-9d85cd6c10e1",
      "name": "When clicking ‘Execute workflow’"
    },
    {
      "parameters": {
        "url": "={{ `https://news.google.com/rss/search?q=${encodeURIComponent($json.company)}&hl=en-US&gl=US&ceid=US:en` }}",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        -64,
        176
      ],
      "id": "3be84846-4a54-43e7-a870-7fcd75661f84",
      "name": "HTTP Request"
    },
    {
      "parameters": {
        "options": {}
      },
      "type": "n8n-nodes-base.xml",
      "typeVersion": 1,
      "position": [
        112,
        192
      ],
      "id": "37c78195-b3e5-4f23-9dcb-98a6a2e1bb28",
      "name": "XML"
    },
    {
      "parameters": {
        "jsCode": "// Process NewsAPI response\nconst newsData = $json;\n\nif (newsData.articles && Array.isArray(newsData.articles)) {\n  return newsData.articles.map(article => ({\n    json: {\n      title: article.title || '',\n      description: article.description || '',\n      content: article.content || '',\n      url: article.url || '',\n      source: article.source?.name || 'Unknown',\n      publishedAt: article.publishedAt || '',\n      processed_text: `${article.title || ''} ${article.description || ''}`.trim()\n    }\n  }));\n} else {\n  return [{json: {error: 'No articles found'}}];\n}"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        144,
        -192
      ],
      "id": "1fa35257-900e-4aea-b13f-1f2afe12f5b5",
      "name": "ProcessNewData"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "792c64ee-ab2d-4a74-9894-86f941661685",
              "name": "title",
              "value": "={{ $json.rss.channel.image.title }}",
              "type": "string"
            },
            {
              "id": "b52ad015-608b-4e25-ba22-1cff402887a0",
              "name": "url",
              "value": "={{ $json.rss.channel.image.url }}",
              "type": "string"
            },
            {
              "id": "8b318860-609d-46d7-8c6c-6e3b7e168886",
              "name": "description",
              "value": "={{ $json.rss.channel.description }}",
              "type": "string"
            },
            {
              "id": "8076f609-be13-4553-9711-7936ed02fc11",
              "name": "publishedAt",
              "value": "={{ $json.rss.channel.item[1].pubDate }}",
              "type": "string"
            },
            {
              "id": "a58ee35b-1e07-4b02-b635-f298e549d8a5",
              "name": "soruce",
              "value": "={{ $json.rss.channel.item[1].source }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        352,
        192
      ],
      "id": "35cec4cc-35b9-49cc-b600-e51f1b276f4a",
      "name": "Edit Fields"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api-inference.huggingface.co/models/ProsusAI/finbert",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "REPLACE_WITH_AUTHORIZATION"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            },
            {
              "name": "x-wait-for-model",
              "value": "true"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"inputs\": \"test message\"\n}",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1040,
        -112
      ],
      "id": "c60deb12-9b6d-49d7-8d31-c392200dc34e",
      "name": "HTTP Request1"
    },
    {
      "parameters": {
        "language": "python",
        "pythonCode": "# Step 2: Add risk calculation to working sentiment extraction\nresults = []\nitems = _input.all()\n\nfor i, item in enumerate(items):\n    data = item.json\n    \n    # Extract sentiment scores (this is working!)\n    positive = 0\n    negative = 0\n    neutral = 0\n    \n    for sentiment in data:\n        if sentiment['label'] == 'positive':\n            positive = sentiment['score']\n        elif sentiment['label'] == 'negative':\n            negative = sentiment['score']\n        elif sentiment['label'] == 'neutral':\n            neutral = sentiment['score']\n    \n    # NEW: Add risk calculation\n    sentiment_risk = negative  # Higher negative = higher risk\n    \n    # Simple risk level\n    if sentiment_risk > 0.7:\n        risk_level = 'CRITICAL'\n    elif sentiment_risk > 0.5:\n        risk_level = 'HIGH'\n    elif sentiment_risk > 0.3:\n        risk_level = 'MEDIUM'\n    elif sentiment_risk > 0.1:\n        risk_level = 'LOW'\n    else:\n        risk_level = 'MINIMAL'\n    \n    # Determine dominant sentiment\n    if positive > max(negative, neutral):\n        dominant = 'positive'\n    elif negative > neutral:\n        dominant = 'negative'\n    else:\n        dominant = 'neutral'\n    \n    result = {\n        'item_number': i,\n        'sentiment_positive': round(positive, 3),\n        'sentiment_negative': round(negative, 3),\n        'sentiment_neutral': round(neutral, 3),\n        'dominant_sentiment': dominant,\n        'risk_score': round(sentiment_risk, 3),\n        'risk_level': risk_level\n    }\n    \n    results.append(result)\n   \n\nreturn results"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1408,
        -64
      ],
      "id": "5867fcc1-cee2-4d48-bbd8-481eddeefef8",
      "name": "RiskCalculator"
    },
    {
      "parameters": {
        "schema": {
          "__rl": true,
          "mode": "list",
          "value": "REPLACE_WITH_RESOURCE"
        },
        "table": {
          "__rl": true,
          "value": "REPLACE_WITH_RESOURCE",
          "mode": "list"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "item_number": "={{ $json.item_number }}",
            "sentiment_positive": "={{ $json.sentiment_positive }}",
            "sentiment_negative": "={{ $json.sentiment_negative }}",
            "sentiment_neutral": "={{ $json.sentiment_neutral }}",
            "dominant_sentiment": "={{ $json.dominant_sentiment }}",
            "risk_score": "={{ $json.risk_score }}",
            "risk_level": "={{ $json.risk_level }}"
          },
          "matchingColumns": [
            "id"
          ],
          "schema": [
            {
              "id": "id",
              "displayName": "id",
              "required": false,
              "defaultMatch": true,
              "display": true,
              "type": "number",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "item_number",
              "displayName": "item_number",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "number",
              "canBeUsedToMatch": true
            },
            {
              "id": "sentiment_positive",
              "displayName": "sentiment_positive",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "number",
              "canBeUsedToMatch": true
            },
            {
              "id": "sentiment_negative",
              "displayName": "sentiment_negative",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "number",
              "canBeUsedToMatch": true
            },
            {
              "id": "sentiment_neutral",
              "displayName": "sentiment_neutral",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "number",
              "canBeUsedToMatch": true
            },
            {
              "id": "dominant_sentiment",
              "displayName": "dominant_sentiment",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "risk_score",
              "displayName": "risk_score",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "number",
              "canBeUsedToMatch": true
            },
            {
              "id": "risk_level",
              "displayName": "risk_level",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "processed_at",
              "displayName": "processed_at",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "dateTime",
              "canBeUsedToMatch": true
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        1616,
        -64
      ],
      "id": "7a98e446-72cc-490e-9077-b6824e553889",
      "name": "Insert rows in a table"
    },
    {
      "parameters": {
        "language": "python",
        "pythonCode": "# Alert Generator - detects high-risk news\nresults = []\nitems = _input.all()\n\n# Alert thresholds\nCRITICAL_THRESHOLD = 0.8\nHIGH_THRESHOLD = 0.6\nMEDIUM_THRESHOLD = 0.4\n\nalerts = []\nsummary_stats = {\n    'total_articles': len(items),\n    'critical_alerts': 0,\n    'high_alerts': 0,\n    'medium_alerts': 0,\n    'low_alerts': 0\n}\n\nfor item in items:\n    data = item.json\n    risk_level = data.get('risk_level', 'UNKNOWN')\n    risk_score = data.get('risk_score', 0)\n    \n    # Count by risk level\n    if risk_level == 'CRITICAL':\n        summary_stats['critical_alerts'] += 1\n    elif risk_level == 'HIGH':\n        summary_stats['high_alerts'] += 1\n    elif risk_level == 'MEDIUM':\n        summary_stats['medium_alerts'] += 1\n    else:\n        summary_stats['low_alerts'] += 1\n    \n    # Generate alerts for HIGH and CRITICAL only\n    if risk_level in ['HIGH', 'CRITICAL']:\n        alert = {\n            'alert_id': f\"ALERT_{item.get('item_number', 0)}\",\n            'alert_type': f\"{risk_level}_RISK_DETECTED\",\n            'risk_level': risk_level,\n            'risk_score': risk_score,\n            'sentiment_negative': data.get('sentiment_negative', 0),\n            'dominant_sentiment': data.get('dominant_sentiment', 'unknown'),\n            'alert_message': f\"{risk_level} financial risk detected (Score: {risk_score})\",\n            'priority': 1 if risk_level == 'CRITICAL' else 2,\n            'timestamp': data.get('processed_at'),\n            'requires_attention': True\n        }\n        alerts.append(alert)\n\n# Return both alerts and summary\nresult = {\n    'alerts': alerts,\n    'summary': summary_stats,\n    'alert_count': len(alerts),\n    'needs_immediate_attention': summary_stats['critical_alerts'] > 0\n}\n\nreturn [result]"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1824,
        -64
      ],
      "id": "bccc3cab-8297-4f6d-a2e4-219ffe55fbcc",
      "name": "Alert Generator Code Node"
    },
    {
      "parameters": {
        "fromEmail": "REPLACE_WITH_FROMEMAIL",
        "toEmail": "REPLACE_WITH_TOEMAIL",
        "subject": "=Financial Risk Alert - {{ new Date().toISOString().split('T')[0] }}",
        "html": "=<h2>🚨 Financial Intelligence Alert</h2>\n\n<p><strong>Alert Summary:</strong></p>\n<ul>\n<li>Total Articles Analyzed: {{ $json.summary.total_articles }}</li>\n<li>Critical Alerts: {{ $json.summary.critical_alerts }}</li>\n<li>High Risk Alerts: {{ $json.summary.high_alerts }}</li>\n<li>Medium Risk Alerts: {{ $json.summary.medium_alerts }}</li>\n<li>Alert Count: {{ $json.alert_count }}</li>\n</ul>\n\n<p><strong>Status:</strong> {{ $json.needs_immediate_attention ? \"⚠️ IMMEDIATE ATTENTION REQUIRED\" : \"✅ Normal Operations\" }}</p>\n\n<p><strong>Details:</strong></p>\n<p>This automated report analyzed {{ $json.summary.total_articles }} financial news articles and detected {{ $json.alert_count }} alerts requiring attention.</p>\n\n<p><em>Generated: {{ new Date().toISOString() }}</em></p>",
        "options": {}
      },
      "type": "n8n-nodes-base.emailSend",
      "typeVersion": 2.1,
      "position": [
        2032,
        -64
      ],
      "id": "db723833-8ea8-4642-8667-639e4a370215",
      "name": "Send email"
    },
    {
      "parameters": {
        "language": "python",
        "pythonCode": "# Daily Intelligence Report Generator\nfrom datetime import datetime\n\nresults = []\nitems = _input.all()\n\n# Get data from previous steps (email response)\nreport_date = datetime.now().strftime('%Y-%m-%d')\nreport_time = datetime.now().strftime('%H:%M:%S')\n\n# Create comprehensive daily intelligence report\ndaily_report = {\n    'report_metadata': {\n        'report_type': 'DAILY_FINANCIAL_INTELLIGENCE',\n        'report_date': report_date,\n        'report_time': report_time,\n        'analysis_period': '24_hours',\n        'report_version': '1.0'\n    },\n    \n    'executive_summary': {\n        'total_articles_analyzed': 113,\n        'overall_risk_level': 'LOW',\n        'alerts_generated': 0,\n        'emails_sent': len([item for item in items if 'accepted' in str(item.json)]),\n        'system_status': 'OPERATIONAL'\n    },\n    \n    'daily_insights': {\n        'market_sentiment': 'PREDOMINANTLY_NEUTRAL',\n        'risk_distribution': {\n            'critical': '0%',\n            'high': '0%', \n            'medium': '0%',\n            'low': '100%'\n        },\n        'news_volume': 'HIGH',\n        'data_quality': 'EXCELLENT'\n    },\n    \n    'recommendations': [\n        'Continue standard monitoring procedures',\n        'News volume is healthy - good market coverage',\n        'No immediate action required',\n        'System performing optimally'\n    ],\n    \n    'next_actions': [\n        'Monitor for any emerging risk patterns',\n        'Continue automated intelligence collection',\n        'Review weekly trends in upcoming report'\n    ],\n    \n    'system_performance': {\n        'news_sources_active': 3,\n        'sentiment_analysis_accuracy': 'HIGH',\n        'database_records_stored': 113,\n        'alert_system_status': 'ACTIVE'\n    }\n}\n\nreturn [daily_report]"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2240,
        -64
      ],
      "id": "49bf4fe3-59ce-4df1-ad20-558790f8b265",
      "name": "DailyGeneratorCode"
    },
    {
      "id": "efe6d130-57c2-5f43-bf93-1cf9431e5cfd",
      "name": "Publication · Overview and setup",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -1600,
        -192
      ],
      "parameters": {
        "content": "# Mycroft News Intelligence Agent\n\n## Purpose and structure\nThis folder contains the **Mycroft News Intelligence Agent** workflow. Its executable nodes use `code`, `emailSend`, `httpRequest`, `manualTrigger`, `merge`, `postgres`, `set`, `xml`. The table and connection map below are derived from the JSON, not an inferred guarantee of behavior.\n\n## Setup\nImport inactive. Configure your own credentials and replace REPLACE_WITH_* values. Review external destinations and Code nodes before running.\n\n## Entry points\nWhen clicking ‘Execute workflow’\n\n## Verification\nSanitized source candidate; not runtime-tested. This overview is generated from node metadata. Read the folder README for the connection map, credential types, and license.\n\n## Attribution\nNik Bear Brown; rights confirmed 2026-10-19\nMIT; preserve source notices.",
        "width": 700,
        "height": 800,
        "color": 1
      }
    }
  ],
  "connections": {
    "Company List": {
      "main": [
        [
          {
            "node": "Build Query",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Query": {
      "main": [
        [
          {
            "node": "HTTP Request",
            "type": "main",
            "index": 0
          },
          {
            "node": "NewsApiKey",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "normalizenewsapi": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "NewsApiKey": {
      "main": [
        [
          {
            "node": "ProcessNewData",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTTP Request": {
      "main": [
        [
          {
            "node": "XML",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge": {
      "main": [
        [
          {
            "node": "HTTP Request1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "XML": {
      "main": [
        [
          {
            "node": "Edit Fields",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When clicking ‘Execute workflow’": {
      "main": [
        [
          {
            "node": "Company List",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "ProcessNewData": {
      "main": [
        [
          {
            "node": "normalizenewsapi",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Edit Fields": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "HTTP Request1": {
      "main": [
        [
          {
            "node": "RiskCalculator",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "RiskCalculator": {
      "main": [
        [
          {
            "node": "Insert rows in a table",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Insert rows in a table": {
      "main": [
        [
          {
            "node": "Alert Generator Code Node",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Alert Generator Code Node": {
      "main": [
        [
          {
            "node": "Send email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send email": {
      "main": [
        [
          {
            "node": "DailyGeneratorCode",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  }
}
