AutomationFlowsAI & RAG › Market Intelligence Engine with AI Sentiment Detection & Competitor Analysis

Market Intelligence Engine with AI Sentiment Detection & Competitor Analysis

ByCheng Siong Chin @cschin on n8n.io

A scheduled process aggregates content from eight distinct data sources and standardizes all inputs into a unified format. AI models perform sentiment scoring, detect conspiracy or misinformation signals, and generate trend analyses across domains. An MCDN routing model…

Cron / scheduled trigger★★★★★ complexityAI-powered46 nodesHTTP RequestOpenAIPostgresSlackGmail
AI & RAG Trigger: Cron / scheduled Nodes: 46 Complexity: ★★★★★ AI nodes: yes Added:
Market Intelligence Engine with AI Sentiment Detection & Competitor Analysis — n8n workflow card showing HTTP Request, OpenAI, Postgres integration

This workflow corresponds to n8n.io template #10765 — we link there as the canonical source.

This workflow follows the Gmail → HTTP Request recipe pattern — see all workflows that pair these two integrations.

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "id": "vmiRUwq7lpvxIcfE",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Market Intelligence Engine with AI Sentiment Detection & Competitor Analysis",
  "tags": [],
  "nodes": [
    {
      "id": "64a37127-3701-4fe2-98ac-3f0ec859594b",
      "name": "Schedule Data Collection",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        400,
        384
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "hours",
              "hoursInterval": 6
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "dc522946-aee3-4c76-92f7-ddeaeb7b7d6e",
      "name": "Workflow Configuration",
      "type": "n8n-nodes-base.set",
      "position": [
        608,
        384
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "id-1",
              "name": "newsApiUrl",
              "type": "string",
              "value": "<__PLACEHOLDER_VALUE__News API endpoint URL__>"
            },
            {
              "id": "id-2",
              "name": "blogApiUrl",
              "type": "string",
              "value": "<__PLACEHOLDER_VALUE__Blog API endpoint URL__>"
            },
            {
              "id": "id-3",
              "name": "socialApiUrl",
              "type": "string",
              "value": "<__PLACEHOLDER_VALUE__Social media API endpoint URL__>"
            },
            {
              "id": "id-4",
              "name": "academicApiUrl",
              "type": "string",
              "value": "<__PLACEHOLDER_VALUE__Academic papers API endpoint URL (e.g., arXiv, Semantic Scholar)__>"
            },
            {
              "id": "id-5",
              "name": "githubApiUrl",
              "type": "string",
              "value": "<__PLACEHOLDER_VALUE__GitHub API endpoint URL__>"
            },
            {
              "id": "id-6",
              "name": "forumApiUrl",
              "type": "string",
              "value": "<__PLACEHOLDER_VALUE__Forum API endpoint URL (e.g., Reddit, HackerNews)__>"
            },
            {
              "id": "id-7",
              "name": "docsApiUrl",
              "type": "string",
              "value": "<__PLACEHOLDER_VALUE__Product docs API endpoint URL__>"
            },
            {
              "id": "id-8",
              "name": "alertThreshold",
              "type": "number",
              "value": 0.75
            },
            {
              "id": "id-9",
              "name": "dbTable",
              "type": "string",
              "value": "ai_trends"
            },
            {
              "id": "id-10",
              "name": "dashboardTable",
              "type": "string",
              "value": "trend_dashboard"
            },
            {
              "id": "id-11",
              "name": "kpiTable",
              "type": "string",
              "value": "trend_kpis"
            },
            {
              "id": "id-12",
              "name": "trainingTable",
              "type": "string",
              "value": "training_data"
            }
          ]
        },
        "includeOtherFields": true
      },
      "typeVersion": 3.4
    },
    {
      "id": "299667ff-10fe-4937-afb4-02f7f6153a1a",
      "name": "Fetch News Articles",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        864,
        448
      ],
      "parameters": {
        "url": "={{ $('Workflow Configuration').first().json.newsApiUrl }}",
        "options": {},
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "e33ccf59-9898-42e6-8f78-43eff861121f",
      "name": "Fetch Blog Posts",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        864,
        592
      ],
      "parameters": {
        "url": "={{ $('Workflow Configuration').first().json.blogApiUrl }}",
        "options": {},
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "78c33619-26a6-4ae6-9392-890e32c8f19e",
      "name": "Fetch Social Media",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        864,
        736
      ],
      "parameters": {
        "url": "={{ $('Workflow Configuration').first().json.socialApiUrl }}",
        "options": {},
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "6c17736f-5bb5-462a-bab7-fee420850f96",
      "name": "Fetch Academic Papers",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        864,
        928
      ],
      "parameters": {
        "url": "={{ $('Workflow Configuration').first().json.academicApiUrl }}",
        "options": {},
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "02727f99-066e-4778-a62d-7d5af023ef91",
      "name": "Fetch Code Repos",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        864,
        1120
      ],
      "parameters": {
        "url": "={{ $('Workflow Configuration').first().json.githubApiUrl }}",
        "options": {},
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "336e1343-34e8-4ace-8d3b-27fb12d35064",
      "name": "Fetch Forum Discussions",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        864,
        1312
      ],
      "parameters": {
        "url": "={{ $('Workflow Configuration').first().json.forumApiUrl }}",
        "options": {},
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "8e536043-0b4b-496b-9ff7-fda8a088de31",
      "name": "Fetch Product Docs",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        864,
        1504
      ],
      "parameters": {
        "url": "={{ $('Workflow Configuration').first().json.docsApiUrl }}",
        "options": {},
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "42637df4-a528-4614-ba7d-f2bac87b1ade",
      "name": "Merge All Sources",
      "type": "n8n-nodes-base.merge",
      "position": [
        1136,
        528
      ],
      "parameters": {
        "numberInputs": 7
      },
      "typeVersion": 3.2
    },
    {
      "id": "dab13587-245a-46a7-88ab-fb19cc495d25",
      "name": "Normalize Content Schema",
      "type": "n8n-nodes-base.code",
      "position": [
        1344,
        416
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Normalize all incoming items into a unified schema\nconst item = $input.item.json;\n\n// Helper function to safely extract nested values\nfunction safeGet(obj, path, defaultValue = null) {\n  try {\n    return path.split('.').reduce((acc, part) => acc && acc[part], obj) || defaultValue;\n  } catch (e) {\n    return defaultValue;\n  }\n}\n\n// Helper function to generate ID if not present\nfunction generateId(item) {\n  if (item.id) return item.id;\n  if (item.url) return Buffer.from(item.url).toString('base64').substring(0, 32);\n  return `item_${Date.now()}_${Math.random().toString(36).substring(7)}`;\n}\n\n// Helper function to parse date\nfunction parseDate(dateValue) {\n  if (!dateValue) return new Date().toISOString();\n  try {\n    const date = new Date(dateValue);\n    return isNaN(date.getTime()) ? new Date().toISOString() : date.toISOString();\n  } catch (e) {\n    return new Date().toISOString();\n  }\n}\n\n// Helper function to extract tags\nfunction extractTags(item) {\n  const tags = [];\n  \n  if (item.tags && Array.isArray(item.tags)) {\n    tags.push(...item.tags);\n  } else if (item.keywords && Array.isArray(item.keywords)) {\n    tags.push(...item.keywords);\n  } else if (item.categories && Array.isArray(item.categories)) {\n    tags.push(...item.categories);\n  } else if (item.hashtags && Array.isArray(item.hashtags)) {\n    tags.push(...item.hashtags);\n  }\n  \n  return tags.filter(tag => tag && typeof tag === 'string');\n}\n\n// Normalize the item\nconst normalizedItem = {\n  id: generateId(item),\n  title: safeGet(item, 'title') || safeGet(item, 'headline') || safeGet(item, 'name') || 'Untitled',\n  content: safeGet(item, 'content') || safeGet(item, 'body') || safeGet(item, 'description') || safeGet(item, 'text') || '',\n  url: safeGet(item, 'url') || safeGet(item, 'link') || safeGet(item, 'permalink') || '',\n  source: safeGet(item, 'source') || safeGet(item, 'sourceName') || safeGet(item, 'publisher') || 'Unknown',\n  publishedDate: parseDate(safeGet(item, 'publishedDate') || safeGet(item, 'published_at') || safeGet(item, 'createdAt') || safeGet(item, 'date')),\n  author: safeGet(item, 'author') || safeGet(item, 'creator') || safeGet(item, 'username') || 'Unknown',\n  tags: extractTags(item),\n  metadata: {\n    originalSource: safeGet(item, 'source'),\n    language: safeGet(item, 'language') || safeGet(item, 'lang'),\n    score: safeGet(item, 'score') || safeGet(item, 'rating'),\n    engagement: {\n      likes: safeGet(item, 'likes') || safeGet(item, 'upvotes') || 0,\n      comments: safeGet(item, 'comments') || safeGet(item, 'replies') || 0,\n      shares: safeGet(item, 'shares') || safeGet(item, 'retweets') || 0\n    },\n    raw: item\n  }\n};\n\nreturn normalizedItem;"
      },
      "typeVersion": 2
    },
    {
      "id": "a6aae8b4-fbf3-458c-8784-561b1fc9f728",
      "name": "Deduplicate Content",
      "type": "n8n-nodes-base.removeDuplicates",
      "position": [
        1520,
        416
      ],
      "parameters": {
        "compare": "selectedFields",
        "options": {},
        "fieldsToCompare": "url"
      },
      "typeVersion": 2
    },
    {
      "id": "176067e6-a064-4d9e-ad03-3760320b08ca",
      "name": "Extract Entities & Topics",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "position": [
        1728,
        416
      ],
      "parameters": {
        "operation": "message"
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "1cf1c8a9-03bf-4805-ac7e-b098607f2282",
      "name": "Parse Technical Signals",
      "type": "n8n-nodes-base.code",
      "position": [
        2080,
        416
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Parse Technical Signals from Content\n// Extract: AI models, datasets, benchmarks, libraries/frameworks, version numbers\n\nconst item = $input.item.json;\nconst content = item.content || item.text || item.body || '';\n\n// Initialize signals object\nconst signals = {\n  aiModels: [],\n  datasets: [],\n  benchmarks: [],\n  libraries: [],\n  versions: []\n};\n\n// AI Models patterns (common model names)\nconst modelPatterns = [\n  /GPT-?\\d+(\\.\\d+)?/gi,\n  /Claude[\\s-]?\\d*/gi,\n  /LLaMA[\\s-]?\\d*/gi,\n  /PaLM[\\s-]?\\d*/gi,\n  /BERT/gi,\n  /T5/gi,\n  /Gemini[\\s-]?\\d*/gi,\n  /Mistral[\\s-]?\\d*/gi,\n  /Falcon[\\s-]?\\d*/gi,\n  /Stable\\s*Diffusion[\\s-]?\\d*/gi,\n  /DALL-?E[\\s-]?\\d*/gi,\n  /Midjourney/gi,\n  /Whisper/gi,\n  /CodeLlama/gi,\n  /Vicuna/gi,\n  /Alpaca/gi\n];\n\n// Dataset patterns\nconst datasetPatterns = [\n  /ImageNet/gi,\n  /COCO/gi,\n  /MNIST/gi,\n  /CIFAR-?\\d+/gi,\n  /SQuAD/gi,\n  /GLUE/gi,\n  /SuperGLUE/gi,\n  /Common\\s*Crawl/gi,\n  /WebText/gi,\n  /The\\s*Pile/gi,\n  /RedPajama/gi,\n  /C4/gi,\n  /BookCorpus/gi,\n  /WikiText/gi\n];\n\n// Benchmark patterns\nconst benchmarkPatterns = [\n  /MMLU/gi,\n  /HumanEval/gi,\n  /HellaSwag/gi,\n  /TruthfulQA/gi,\n  /GSM8K/gi,\n  /MATH/gi,\n  /BBH/gi,\n  /ARC[\\s-]?(?:Easy|Challenge)?/gi,\n  /WinoGrande/gi,\n  /LAMBADA/gi,\n  /BIG-?Bench/gi\n];\n\n// Library/Framework patterns\nconst libraryPatterns = [\n  /PyTorch/gi,\n  /TensorFlow/gi,\n  /JAX/gi,\n  /Keras/gi,\n  /Hugging\\s*Face/gi,\n  /Transformers/gi,\n  /LangChain/gi,\n  /LlamaIndex/gi,\n  /scikit-learn/gi,\n  /OpenAI/gi,\n  /Anthropic/gi,\n  /vLLM/gi,\n  /DeepSpeed/gi,\n  /Megatron/gi,\n  /PEFT/gi,\n  /LoRA/gi,\n  /QLoRA/gi\n];\n\n// Version number pattern (e.g., v1.2.3, version 2.0, 3.5.1)\nconst versionPattern = /(?:v(?:ersion)?\\s*)?\\d+\\.\\d+(?:\\.\\d+)?(?:-(?:alpha|beta|rc)\\d*)?/gi;\n\n// Extract AI Models\nmodelPatterns.forEach(pattern => {\n  const matches = content.match(pattern);\n  if (matches) {\n    signals.aiModels.push(...matches);\n  }\n});\n\n// Extract Datasets\ndatasetPatterns.forEach(pattern => {\n  const matches = content.match(pattern);\n  if (matches) {\n    signals.datasets.push(...matches);\n  }\n});\n\n// Extract Benchmarks\nbenchmarkPatterns.forEach(pattern => {\n  const matches = content.match(pattern);\n  if (matches) {\n    signals.benchmarks.push(...matches);\n  }\n});\n\n// Extract Libraries/Frameworks\nlibraryPatterns.forEach(pattern => {\n  const matches = content.match(pattern);\n  if (matches) {\n    signals.libraries.push(...matches);\n  }\n});\n\n// Extract Version Numbers\nconst versionMatches = content.match(versionPattern);\nif (versionMatches) {\n  signals.versions.push(...versionMatches);\n}\n\n// Deduplicate and normalize\nsignals.aiModels = [...new Set(signals.aiModels.map(m => m.trim()))];\nsignals.datasets = [...new Set(signals.datasets.map(d => d.trim()))];\nsignals.benchmarks = [...new Set(signals.benchmarks.map(b => b.trim()))];\nsignals.libraries = [...new Set(signals.libraries.map(l => l.trim()))];\nsignals.versions = [...new Set(signals.versions.map(v => v.trim()))];\n\n// Return enriched item with technical signals\nreturn {\n  ...item,\n  technicalSignals: signals,\n  signalCount: {\n    models: signals.aiModels.length,\n    datasets: signals.datasets.length,\n    benchmarks: signals.benchmarks.length,\n    libraries: signals.libraries.length,\n    versions: signals.versions.length,\n    total: signals.aiModels.length + signals.datasets.length + signals.benchmarks.length + signals.libraries.length + signals.versions.length\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "0f28612a-98f1-442b-af18-a2f21a3e5873",
      "name": "Store Raw Data",
      "type": "n8n-nodes-base.postgres",
      "position": [
        2304,
        416
      ],
      "parameters": {
        "table": {
          "__rl": true,
          "mode": "name",
          "value": "={{ $('Workflow Configuration').first().json.dbTable }}"
        },
        "schema": {
          "__rl": true,
          "mode": "list",
          "value": "public"
        },
        "columns": {
          "value": null,
          "mappingMode": "autoMapInputData"
        },
        "options": {}
      },
      "typeVersion": 2.6
    },
    {
      "id": "6064ed22-fc1b-43d7-85a4-18dc578429f8",
      "name": "Time-Series Analytics",
      "type": "n8n-nodes-base.code",
      "position": [
        2544,
        304
      ],
      "parameters": {
        "jsCode": "// Time-Series Analytics: Calculate mention frequency, growth rates, and trending patterns\n\nconst items = $input.all();\n\n// Helper function to parse dates\nfunction parseDate(dateStr) {\n  return new Date(dateStr);\n}\n\n// Helper function to get time window key\nfunction getTimeWindowKey(date, window) {\n  const d = new Date(date);\n  switch(window) {\n    case 'daily':\n      return d.toISOString().split('T')[0];\n    case 'weekly':\n      const weekStart = new Date(d);\n      weekStart.setDate(d.getDate() - d.getDay());\n      return weekStart.toISOString().split('T')[0];\n    case 'monthly':\n      return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`;\n    default:\n      return d.toISOString().split('T')[0];\n  }\n}\n\n// Group data by entity/topic and time windows\nconst timeSeriesData = {};\n\nfor (const item of items) {\n  const entities = item.json.entities || [];\n  const topics = item.json.topics || [];\n  const timestamp = item.json.timestamp || item.json.publishedDate || new Date().toISOString();\n  \n  // Process entities\n  for (const entity of entities) {\n    if (!timeSeriesData[entity]) {\n      timeSeriesData[entity] = {\n        daily: {},\n        weekly: {},\n        monthly: {}\n      };\n    }\n    \n    ['daily', 'weekly', 'monthly'].forEach(window => {\n      const key = getTimeWindowKey(timestamp, window);\n      if (!timeSeriesData[entity][window][key]) {\n        timeSeriesData[entity][window][key] = 0;\n      }\n      timeSeriesData[entity][window][key]++;\n    });\n  }\n  \n  // Process topics\n  for (const topic of topics) {\n    if (!timeSeriesData[topic]) {\n      timeSeriesData[topic] = {\n        daily: {},\n        weekly: {},\n        monthly: {}\n      };\n    }\n    \n    ['daily', 'weekly', 'monthly'].forEach(window => {\n      const key = getTimeWindowKey(timestamp, window);\n      if (!timeSeriesData[topic][window][key]) {\n        timeSeriesData[topic][window][key] = 0;\n      }\n      timeSeriesData[topic][window][key]++;\n    });\n  }\n}\n\n// Calculate growth rates and moving averages\nfunction calculateMetrics(timeSeries, windowSize = 7) {\n  const sortedDates = Object.keys(timeSeries).sort();\n  const values = sortedDates.map(date => timeSeries[date]);\n  \n  // Calculate growth rate\n  const growthRates = [];\n  for (let i = 1; i < values.length; i++) {\n    const rate = values[i-1] === 0 ? 0 : ((values[i] - values[i-1]) / values[i-1]) * 100;\n    growthRates.push(rate);\n  }\n  \n  // Calculate moving average\n  const movingAverages = [];\n  for (let i = 0; i < values.length; i++) {\n    const start = Math.max(0, i - windowSize + 1);\n    const window = values.slice(start, i + 1);\n    const avg = window.reduce((sum, val) => sum + val, 0) / window.length;\n    movingAverages.push(avg);\n  }\n  \n  // Identify trending pattern\n  const recentGrowth = growthRates.slice(-3).reduce((sum, val) => sum + val, 0) / Math.min(3, growthRates.length);\n  let trend = 'stable';\n  if (recentGrowth > 20) trend = 'rising';\n  else if (recentGrowth < -20) trend = 'declining';\n  \n  return {\n    totalMentions: values.reduce((sum, val) => sum + val, 0),\n    averageGrowthRate: growthRates.length > 0 ? growthRates.reduce((sum, val) => sum + val, 0) / growthRates.length : 0,\n    recentGrowthRate: recentGrowth,\n    movingAverage: movingAverages[movingAverages.length - 1] || 0,\n    trend: trend,\n    dataPoints: sortedDates.length\n  };\n}\n\n// Generate output with analytics for each entity/topic\nconst results = [];\n\nfor (const [entityOrTopic, windows] of Object.entries(timeSeriesData)) {\n  const result = {\n    entity: entityOrTopic,\n    daily: calculateMetrics(windows.daily, 7),\n    weekly: calculateMetrics(windows.weekly, 4),\n    monthly: calculateMetrics(windows.monthly, 3),\n    rawTimeSeries: windows\n  };\n  \n  results.push(result);\n}\n\n// Sort by total mentions (daily) to prioritize high-volume entities\nresults.sort((a, b) => b.daily.totalMentions - a.daily.totalMentions);\n\nreturn results.map(result => ({ json: result }));"
      },
      "typeVersion": 2
    },
    {
      "id": "9c24ddb9-c02f-4015-b602-7af191f4e085",
      "name": "Topic Modeling & Velocity",
      "type": "n8n-nodes-base.code",
      "position": [
        2544,
        480
      ],
      "parameters": {
        "jsCode": "// Topic Modeling & Velocity Analysis using TF-IDF\n// Analyzes topic emergence, growth rate, and lifecycle stage\n\nconst items = $input.all();\n\n// Helper function to tokenize text\nfunction tokenize(text) {\n  return text.toLowerCase()\n    .replace(/[^a-z0-9\\s]/g, ' ')\n    .split(/\\s+/)\n    .filter(word => word.length > 3);\n}\n\n// Calculate TF (Term Frequency)\nfunction calculateTF(tokens) {\n  const tf = {};\n  const totalTokens = tokens.length;\n  \n  tokens.forEach(token => {\n    tf[token] = (tf[token] || 0) + 1;\n  });\n  \n  // Normalize by total tokens\n  Object.keys(tf).forEach(token => {\n    tf[token] = tf[token] / totalTokens;\n  });\n  \n  return tf;\n}\n\n// Calculate IDF (Inverse Document Frequency)\nfunction calculateIDF(documents) {\n  const idf = {};\n  const totalDocs = documents.length;\n  \n  // Count document frequency for each term\n  documents.forEach(doc => {\n    const uniqueTokens = [...new Set(doc.tokens)];\n    uniqueTokens.forEach(token => {\n      idf[token] = (idf[token] || 0) + 1;\n    });\n  });\n  \n  // Calculate IDF\n  Object.keys(idf).forEach(token => {\n    idf[token] = Math.log(totalDocs / idf[token]);\n  });\n  \n  return idf;\n}\n\n// Calculate TF-IDF scores\nfunction calculateTFIDF(tf, idf) {\n  const tfidf = {};\n  \n  Object.keys(tf).forEach(token => {\n    tfidf[token] = tf[token] * (idf[token] || 0);\n  });\n  \n  return tfidf;\n}\n\n// Extract topics from documents\nfunction extractTopics(documents, topN = 10) {\n  const allTFIDF = {};\n  \n  documents.forEach(doc => {\n    Object.keys(doc.tfidf).forEach(token => {\n      allTFIDF[token] = (allTFIDF[token] || 0) + doc.tfidf[token];\n    });\n  });\n  \n  // Sort by TF-IDF score and get top N\n  const sortedTopics = Object.entries(allTFIDF)\n    .sort((a, b) => b[1] - a[1])\n    .slice(0, topN)\n    .map(([topic, score]) => ({ topic, score }));\n  \n  return sortedTopics;\n}\n\n// Calculate topic velocity metrics\nfunction calculateTopicVelocity(documents, topic) {\n  // Group documents by time period (e.g., daily)\n  const timeGroups = {};\n  \n  documents.forEach(doc => {\n    const timestamp = new Date(doc.timestamp || doc.publishedAt || Date.now());\n    const dateKey = timestamp.toISOString().split('T')[0]; // Daily grouping\n    \n    if (!timeGroups[dateKey]) {\n      timeGroups[dateKey] = [];\n    }\n    \n    // Check if document contains the topic\n    if (doc.tokens.includes(topic)) {\n      timeGroups[dateKey].push(doc);\n    }\n  });\n  \n  // Calculate mention counts over time\n  const sortedDates = Object.keys(timeGroups).sort();\n  const mentionCounts = sortedDates.map(date => timeGroups[date].length);\n  \n  // Calculate velocity (rate of change)\n  const velocity = [];\n  for (let i = 1; i < mentionCounts.length; i++) {\n    velocity.push(mentionCounts[i] - mentionCounts[i - 1]);\n  }\n  \n  // Calculate acceleration (rate of change of velocity)\n  const acceleration = [];\n  for (let i = 1; i < velocity.length; i++) {\n    acceleration.push(velocity[i] - velocity[i - 1]);\n  }\n  \n  // Determine lifecycle stage\n  const avgVelocity = velocity.length > 0 ? velocity.reduce((a, b) => a + b, 0) / velocity.length : 0;\n  const avgAcceleration = acceleration.length > 0 ? acceleration.reduce((a, b) => a + b, 0) / acceleration.length : 0;\n  \n  let lifecycleStage = 'stable';\n  if (avgVelocity > 5 && avgAcceleration > 0) {\n    lifecycleStage = 'emerging';\n  } else if (avgVelocity > 0 && avgAcceleration < 0) {\n    lifecycleStage = 'peaking';\n  } else if (avgVelocity < -2) {\n    lifecycleStage = 'declining';\n  } else if (avgVelocity > 0) {\n    lifecycleStage = 'growing';\n  }\n  \n  return {\n    topic,\n    totalMentions: mentionCounts.reduce((a, b) => a + b, 0),\n    mentionsByDate: sortedDates.map((date, i) => ({ date, count: mentionCounts[i] })),\n    avgVelocity,\n    avgAcceleration,\n    lifecycleStage,\n    trendDirection: avgVelocity > 0 ? 'up' : avgVelocity < 0 ? 'down' : 'flat'\n  };\n}\n\n// Process documents\nconst documents = items.map(item => {\n  const content = item.json.content || item.json.text || item.json.title || '';\n  const tokens = tokenize(content);\n  \n  return {\n    ...item.json,\n    tokens,\n    timestamp: item.json.timestamp || item.json.publishedAt || item.json.createdAt || new Date().toISOString()\n  };\n});\n\n// Calculate TF for each document\ndocuments.forEach(doc => {\n  doc.tf = calculateTF(doc.tokens);\n});\n\n// Calculate IDF across all documents\nconst idf = calculateIDF(documents);\n\n// Calculate TF-IDF for each document\ndocuments.forEach(doc => {\n  doc.tfidf = calculateTFIDF(doc.tf, idf);\n});\n\n// Extract top topics\nconst topTopics = extractTopics(documents, 20);\n\n// Calculate velocity for each topic\nconst topicVelocityAnalysis = topTopics.map(({ topic, score }) => {\n  const velocityMetrics = calculateTopicVelocity(documents, topic);\n  return {\n    ...velocityMetrics,\n    tfidfScore: score\n  };\n});\n\n// Sort by emerging topics (high velocity and acceleration)\nconst emergingTopics = topicVelocityAnalysis\n  .filter(t => t.lifecycleStage === 'emerging' || t.lifecycleStage === 'growing')\n  .sort((a, b) => b.avgVelocity - a.avgVelocity);\n\n// Return analysis results\nreturn [\n  {\n    json: {\n      analysisType: 'topic_modeling_velocity',\n      timestamp: new Date().toISOString(),\n      totalDocuments: documents.length,\n      topTopics: topicVelocityAnalysis,\n      emergingTopics,\n      summary: {\n        totalTopicsAnalyzed: topTopics.length,\n        emergingCount: emergingTopics.length,\n        growingCount: topicVelocityAnalysis.filter(t => t.lifecycleStage === 'growing').length,\n        decliningCount: topicVelocityAnalysis.filter(t => t.lifecycleStage === 'declining').length,\n        stableCount: topicVelocityAnalysis.filter(t => t.lifecycleStage === 'stable').length\n      }\n    }\n  }\n];"
      },
      "typeVersion": 2
    },
    {
      "id": "4a6ed491-8a1f-4a68-9341-b0c241752fce",
      "name": "Sentiment Analysis",
      "type": "n8n-nodes-base.code",
      "position": [
        2544,
        672
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Sentiment Analysis using lexicon-based approach\n// Analyzes sentiment scores (-1 to 1) and tracks trends over time\n\nconst item = $input.item.json;\n\n// Simple sentiment lexicon (can be expanded)\nconst positiveWords = [\n  'good', 'great', 'excellent', 'amazing', 'wonderful', 'fantastic', 'awesome',\n  'love', 'best', 'perfect', 'outstanding', 'brilliant', 'innovative', 'revolutionary',\n  'breakthrough', 'success', 'powerful', 'efficient', 'improved', 'better',\n  'exciting', 'impressive', 'superior', 'advanced', 'cutting-edge', 'promising'\n];\n\nconst negativeWords = [\n  'bad', 'terrible', 'awful', 'horrible', 'poor', 'worst', 'hate', 'disappointing',\n  'failed', 'failure', 'broken', 'bug', 'issue', 'problem', 'slow', 'difficult',\n  'complicated', 'confusing', 'outdated', 'deprecated', 'vulnerable', 'insecure',\n  'unstable', 'unreliable', 'inferior', 'lacking'\n];\n\nconst intensifiers = ['very', 'extremely', 'highly', 'incredibly', 'absolutely'];\nconst negations = ['not', 'no', 'never', 'neither', 'nobody', 'nothing', \"don't\", \"doesn't\", \"didn't\"];\n\n// Extract text content from item\nconst text = (item.content || item.text || item.title || '').toLowerCase();\nconst words = text.split(/\\W+/);\n\n// Calculate sentiment score\nlet sentimentScore = 0;\nlet positiveCount = 0;\nlet negativeCount = 0;\nlet totalWords = words.length;\n\nfor (let i = 0; i < words.length; i++) {\n  const word = words[i];\n  const prevWord = i > 0 ? words[i - 1] : '';\n  \n  // Check for intensifiers\n  const intensifier = intensifiers.includes(prevWord) ? 1.5 : 1.0;\n  \n  // Check for negations\n  const negated = negations.includes(prevWord) ? -1 : 1;\n  \n  if (positiveWords.includes(word)) {\n    const score = 1 * intensifier * negated;\n    sentimentScore += score;\n    if (score > 0) positiveCount++;\n    else negativeCount++;\n  } else if (negativeWords.includes(word)) {\n    const score = -1 * intensifier * negated;\n    sentimentScore += score;\n    if (score < 0) negativeCount++;\n    else positiveCount++;\n  }\n}\n\n// Normalize sentiment score to -1 to 1 range\nconst maxPossibleScore = totalWords;\nconst normalizedScore = maxPossibleScore > 0 ? \n  Math.max(-1, Math.min(1, sentimentScore / (maxPossibleScore * 0.1))) : 0;\n\n// Classify sentiment\nlet sentimentLabel = 'neutral';\nif (normalizedScore > 0.2) sentimentLabel = 'positive';\nelse if (normalizedScore < -0.2) sentimentLabel = 'negative';\n\n// Calculate sentiment confidence\nconst sentimentWordCount = positiveCount + negativeCount;\nconst confidence = totalWords > 0 ? sentimentWordCount / totalWords : 0;\n\n// Track sentiment trends (calculate moving average if timestamp available)\nconst timestamp = item.timestamp || item.publishedAt || item.createdAt || new Date().toISOString();\nconst date = new Date(timestamp);\n\n// Return enriched item with sentiment analysis\nreturn {\n  json: {\n    ...item,\n    sentiment: {\n      score: parseFloat(normalizedScore.toFixed(3)),\n      label: sentimentLabel,\n      confidence: parseFloat(confidence.toFixed(3)),\n      positiveWords: positiveCount,\n      negativeWords: negativeCount,\n      totalWords: totalWords,\n      timestamp: timestamp,\n      date: date.toISOString().split('T')[0],\n      hour: date.getHours(),\n      dayOfWeek: date.getDay()\n    },\n    sentimentTrend: {\n      score: normalizedScore,\n      timestamp: timestamp,\n      source: item.source || 'unknown',\n      topic: item.topic || item.category || 'general'\n    }\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "f54f9bba-b2ef-4a5e-8a2f-e898b4280852",
      "name": "Changepoint Detection",
      "type": "n8n-nodes-base.code",
      "position": [
        2544,
        800
      ],
      "parameters": {
        "jsCode": "// Changepoint Detection using CUSUM and Bayesian methods\n// Detects sudden shifts in trend momentum from time-series data\n\nconst items = $input.all();\n\n// Helper function: Calculate mean\nfunction mean(arr) {\n  return arr.reduce((sum, val) => sum + val, 0) / arr.length;\n}\n\n// Helper function: Calculate standard deviation\nfunction stdDev(arr) {\n  const avg = mean(arr);\n  const squareDiffs = arr.map(val => Math.pow(val - avg, 2));\n  return Math.sqrt(mean(squareDiffs));\n}\n\n// CUSUM (Cumulative Sum) Changepoint Detection\nfunction cusumChangepoints(values, threshold = 3) {\n  const avg = mean(values);\n  const std = stdDev(values);\n  \n  let cumSum = 0;\n  let changepoints = [];\n  \n  for (let i = 0; i < values.length; i++) {\n    cumSum += (values[i] - avg) / std;\n    \n    if (Math.abs(cumSum) > threshold) {\n      changepoints.push({\n        index: i,\n        value: values[i],\n        cusum: cumSum,\n        type: cumSum > 0 ? 'upward_shift' : 'downward_shift'\n      });\n      cumSum = 0; // Reset after detection\n    }\n  }\n  \n  return changepoints;\n}\n\n// Bayesian Changepoint Detection (simplified)\nfunction bayesianChangepoints(values, prior = 0.01) {\n  const changepoints = [];\n  const n = values.length;\n  \n  for (let t = 10; t < n - 10; t++) {\n    const before = values.slice(Math.max(0, t - 10), t);\n    const after = values.slice(t, Math.min(n, t + 10));\n    \n    const meanBefore = mean(before);\n    const meanAfter = mean(after);\n    const stdBefore = stdDev(before);\n    const stdAfter = stdDev(after);\n    \n    // Calculate probability of changepoint\n    const meanDiff = Math.abs(meanAfter - meanBefore);\n    const pooledStd = Math.sqrt((stdBefore ** 2 + stdAfter ** 2) / 2);\n    \n    if (pooledStd > 0) {\n      const zScore = meanDiff / pooledStd;\n      \n      if (zScore > 2.5) { // Significant change\n        changepoints.push({\n          index: t,\n          value: values[t],\n          zScore: zScore,\n          meanBefore: meanBefore,\n          meanAfter: meanAfter,\n          type: meanAfter > meanBefore ? 'acceleration' : 'deceleration'\n        });\n      }\n    }\n  }\n  \n  return changepoints;\n}\n\n// Process each item's time-series data\nconst results = items.map(item => {\n  const data = item.json;\n  \n  // Extract time-series values (adjust field names based on your data structure)\n  const timeSeriesField = data.timeSeries || data.values || data.metrics || [];\n  const values = Array.isArray(timeSeriesField) \n    ? timeSeriesField.map(v => typeof v === 'object' ? (v.value || v.count || 0) : v)\n    : [];\n  \n  if (values.length < 20) {\n    return {\n      json: {\n        ...data,\n        changepoints: [],\n        changepointDetection: {\n          status: 'insufficient_data',\n          message: 'Need at least 20 data points for changepoint detection'\n        }\n      }\n    };\n  }\n  \n  // Run both detection methods\n  const cusumResults = cusumChangepoints(values, 3);\n  const bayesianResults = bayesianChangepoints(values, 0.01);\n  \n  // Combine and deduplicate changepoints\n  const allChangepoints = [...cusumResults, ...bayesianResults];\n  const uniqueChangepoints = [];\n  const seen = new Set();\n  \n  allChangepoints.forEach(cp => {\n    const key = `${cp.index}-${cp.type}`;\n    if (!seen.has(key)) {\n      seen.add(key);\n      uniqueChangepoints.push(cp);\n    }\n  });\n  \n  // Sort by index\n  uniqueChangepoints.sort((a, b) => a.index - b.index);\n  \n  // Calculate changepoint statistics\n  const changepointStats = {\n    totalChangepoints: uniqueChangepoints.length,\n    upwardShifts: uniqueChangepoints.filter(cp => cp.type === 'upward_shift' || cp.type === 'acceleration').length,\n    downwardShifts: uniqueChangepoints.filter(cp => cp.type === 'downward_shift' || cp.type === 'deceleration').length,\n    changepointDensity: uniqueChangepoints.length / values.length,\n    mostRecentChangepoint: uniqueChangepoints.length > 0 ? uniqueChangepoints[uniqueChangepoints.length - 1] : null\n  };\n  \n  // Determine trend momentum status\n  let momentumStatus = 'stable';\n  if (changepointStats.mostRecentChangepoint) {\n    const recentIndex = changepointStats.mostRecentChangepoint.index;\n    const recency = (values.length - recentIndex) / values.length;\n    \n    if (recency < 0.2) { // Recent changepoint (within last 20%)\n      momentumStatus = changepointStats.mostRecentChangepoint.type.includes('upward') || \n                       changepointStats.mostRecentChangepoint.type === 'acceleration' \n                       ? 'accelerating' : 'decelerating';\n    }\n  }\n  \n  return {\n    json: {\n      ...data,\n      changepoints: uniqueChangepoints,\n      changepointDetection: {\n        status: 'success',\n        stats: changepointStats,\n        momentumStatus: momentumStatus,\n        detectionMethods: ['cusum', 'bayesian'],\n        timestamp: new Date().toISOString()\n      }\n    }\n  };\n});\n\nreturn results;"
      },
      "typeVersion": 2
    },
    {
      "id": "8659195a-233c-4cfd-8697-26c3fee9f13a",
      "name": "Novelty Scoring",
      "type": "n8n-nodes-base.code",
      "position": [
        2544,
        992
      ],
      "parameters": {
        "jsCode": "// Novelty Scoring: Calculate uniqueness of content against historical baseline\n// Measures how novel current topics, entities, and signals are compared to past data\n\nconst items = $input.all();\nconst outputItems = [];\n\n// Helper function to calculate Jaccard similarity\nfunction jaccardSimilarity(set1, set2) {\n  const intersection = set1.filter(x => set2.includes(x)).length;\n  const union = new Set([...set1, ...set2]).size;\n  return union === 0 ? 0 : intersection / union;\n}\n\n// Helper function to calculate cosine similarity for numeric vectors\nfunction cosineSimilarity(vec1, vec2) {\n  if (vec1.length !== vec2.length) return 0;\n  \n  let dotProduct = 0;\n  let mag1 = 0;\n  let mag2 = 0;\n  \n  for (let i = 0; i < vec1.length; i++) {\n    dotProduct += vec1[i] * vec2[i];\n    mag1 += vec1[i] * vec1[i];\n    mag2 += vec2[i] * vec2[i];\n  }\n  \n  const magnitude = Math.sqrt(mag1) * Math.sqrt(mag2);\n  return magnitude === 0 ? 0 : dotProduct / magnitude;\n}\n\n// Process each item\nfor (const item of items) {\n  const data = item.json;\n  \n  // Extract current features\n  const currentTopics = data.topics || [];\n  const currentEntities = data.entities || [];\n  const currentSignals = data.technical_signals || [];\n  const currentKeywords = data.keywords || [];\n  \n  // Historical baseline (in production, fetch from database)\n  // For now, use a rolling window from previous items\n  const historicalWindow = items.slice(0, Math.max(0, items.indexOf(item)));\n  \n  if (historicalWindow.length === 0) {\n    // First item - assign high novelty\n    outputItems.push({\n      json: {\n        ...data,\n        novelty_score: 0.95,\n        topic_novelty: 0.95,\n        entity_novelty: 0.95,\n        signal_novelty: 0.95,\n        keyword_novelty: 0.95,\n        novelty_explanation: 'First item in dataset - baseline established',\n        is_novel: true\n      }\n    });\n    continue;\n  }\n  \n  // Calculate topic novelty\n  let topicSimilarities = [];\n  for (const histItem of historicalWindow) {\n    const histTopics = histItem.json.topics || [];\n    if (histTopics.length > 0 && currentTopics.length > 0) {\n      topicSimilarities.push(jaccardSimilarity(currentTopics, histTopics));\n    }\n  }\n  const avgTopicSimilarity = topicSimilarities.length > 0 \n    ? topicSimilarities.reduce((a, b) => a + b, 0) / topicSimilarities.length \n    : 0;\n  const topicNovelty = 1 - avgTopicSimilarity;\n  \n  // Calculate entity novelty\n  let entitySimilarities = [];\n  for (const histItem of historicalWindow) {\n    const histEntities = histItem.json.entities || [];\n    if (histEntities.length > 0 && currentEntities.length > 0) {\n      entitySimilarities.push(jaccardSimilarity(currentEntities, histEntities));\n    }\n  }\n  const avgEntitySimilarity = entitySimilarities.length > 0 \n    ? entitySimilarities.reduce((a, b) => a + b, 0) / entitySimilarities.length \n    : 0;\n  const entityNovelty = 1 - avgEntitySimilarity;\n  \n  // Calculate signal novelty\n  let signalSimilarities = [];\n  for (const histItem of historicalWindow) {\n    const histSignals = histItem.json.technical_signals || [];\n    if (histSignals.length > 0 && currentSignals.length > 0) {\n      signalSimilarities.push(jaccardSimilarity(currentSignals, histSignals));\n    }\n  }\n  const avgSignalSimilarity = signalSimilarities.length > 0 \n    ? signalSimilarities.reduce((a, b) => a + b, 0) / signalSimilarities.length \n    : 0;\n  const signalNovelty = 1 - avgSignalSimilarity;\n  \n  // Calculate keyword novelty\n  let keywordSimilarities = [];\n  for (const histItem of historicalWindow) {\n    const histKeywords = histItem.json.keywords || [];\n    if (histKeywords.length > 0 && currentKeywords.length > 0) {\n      keywordSimilarities.push(jaccardSimilarity(currentKeywords, histKeywords));\n    }\n  }\n  const avgKeywordSimilarity = keywordSimilarities.length > 0 \n    ? keywordSimilarities.reduce((a, b) => a + b, 0) / keywordSimilarities.length \n    : 0;\n  const keywordNovelty = 1 - avgKeywordSimilarity;\n  \n  // Composite novelty score (weighted average)\n  const weights = {\n    topic: 0.35,\n    entity: 0.25,\n    signal: 0.25,\n    keyword: 0.15\n  };\n  \n  const noveltyScore = (\n    topicNovelty * weights.topic +\n    entityNovelty * weights.entity +\n    signalNovelty * weights.signal +\n    keywordNovelty * weights.keyword\n  );\n  \n  // Determine if content is novel (threshold: 0.6)\n  const isNovel = noveltyScore >= 0.6;\n  \n  // Generate explanation\n  let explanation = [];\n  if (topicNovelty > 0.7) explanation.push('highly novel topics');\n  if (entityNovelty > 0.7) explanation.push('new entities');\n  if (signalNovelty > 0.7) explanation.push('unique technical signals');\n  if (keywordNovelty > 0.7) explanation.push('fresh keywords');\n  \n  const noveltyExplanation = explanation.length > 0 \n    ? `Novel content detected: ${explanation.join(', ')}` \n    : 'Content similar to historical baseline';\n  \n  outputItems.push({\n    json: {\n      ...data,\n      novelty_score: Math.round(noveltyScore * 1000) / 1000,\n      topic_novelty: Math.round(topicNovelty * 1000) / 1000,\n      entity_novelty: Math.round(entityNovelty * 1000) / 1000,\n      signal_novelty: Math.round(signalNovelty * 1000) / 1000,\n      keyword_novelty: Math.round(keywordNovelty * 1000) / 1000,\n      novelty_explanation: noveltyExplanation,\n      is_novel: isNovel,\n      historical_window_size: historicalWindow.length\n    }\n  });\n}\n\nreturn outputItems;"
      },
      "typeVersion": 2
    },
    {
      "id": "1c4677d9-f0d4-4fc6-808a-841d0da2a8f3",
      "name": "Merge Analytics Signals",
      "type": "n8n-nodes-base.merge",
      "position": [
        2816,
        464
      ],
      "parameters": {
        "numberInputs": 5
      },
      "typeVersion": 3.2
    },
    {
      "id": "cae688d8-5104-492a-b764-cfc5c80df974",
      "name": "MCDM Signal Fusion",
      "type": "n8n-nodes-base.code",
      "position": [
        3008,
        320
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// MCDM Signal Fusion using TOPSIS method\n// Combines multiple analytics signals: velocity, sentiment, changepoint, novelty, time-series growth\n\nconst item = $input.item.json;\n\n// Extract signals from merged analytics data\nconst signals = {\n  velocity: item.velocity || 0,\n  sentiment: item.sentiment || 0,\n  changepointMagnitude: item.changepointMagnitude || 0,\n  novelty: item.novelty || 0,\n  timeSeriesGrowth: item.timeSeriesGrowth || 0\n};\n\n// Define weights for each criterion (sum = 1)\nconst weights = {\n  velocity: 0.25,\n  sentiment: 0.15,\n  changepointMagnitude: 0.25,\n  novelty: 0.20,\n  timeSeriesGrowth: 0.15\n};\n\n// Normalize signals to [0, 1] range\nfunction normalize(value, min = 0, max = 100) {\n  if (max === min) return 0;\n  return (value - min) / (max - min);\n}\n\n// Normalize each signal\nconst normalized = {\n  velocity: normalize(signals.velocity, 0, 100),\n  sentiment: normalize(signals.sentiment, -1, 1),\n  changepointMagnitude: normalize(signals.changepointMagnitude, 0, 100),\n  novelty: normalize(signals.novelty, 0, 100),\n  timeSeriesGrowth: normalize(signals.timeSeriesGrowth, -50, 50)\n};\n\n// Calculate weighted normalized values\nconst weighted = {\n  velocity: normalized.velocity * weights.velocity,\n  sentiment: normalized.sentiment * weights.sentiment,\n  changepointMagnitude: normalized.changepointMagnitude * weights.changepointMagnitude,\n  novelty: normalized.novelty * weights.novelty,\n  timeSeriesGrowth: normalized.timeSeriesGrowth * weights.timeSeriesGrowth\n};\n\n// TOPSIS: Calculate ideal best and worst solutions\nconst idealBest = {\n  velocity: 1 * weights.velocity,\n  sentiment: 1 * weights.sentiment,\n  changepointMagnitude: 1 * weights.changepointMagnitude,\n  novelty: 1 * weights.novelty,\n  timeSeriesGrowth: 1 * weights.timeSeriesGrowth\n};\n\nconst idealWorst = {\n  velocity: 0,\n  sentiment: 0,\n  changepointMagnitude: 0,\n  novelty: 0,\n  timeSeriesGrowth: 0\n};\n\n// Calculate Euclidean distance to ideal best and worst\nfunction euclideanDistance(point1, point2) {\n  const keys = Object.keys(point1);\n  const sumSquares = keys.reduce((sum, key) => {\n    return sum + Math.pow(point1[key] - point2[key], 2);\n  }, 0);\n  return Math.sqrt(sumSquares);\n}\n\nconst distanceToBest = euclideanDistance(weighted, idealBest);\nconst distanceToWorst = euclideanDistance(weighted, idealWorst);\n\n// Calculate TOPSIS score (closeness coefficient)\nconst topsisScore = distanceToWorst / (distanceToBest + distanceToWorst);\n\n// Alternative: Weighted Sum Method score\nconst weightedSumScore = Object.values(weighted).reduce((sum, val) => sum + val, 0);\n\n// Calculate composite impact score (0-100 scale)\nconst impactScore = Math.round(topsisScore * 100);\n\n// Determine trend strength category\nlet trendStrength = 'Low';\nif (impactScore >= 75) trendStrength = 'Critical';\nelse if (impactScore >= 60) trendStrength = 'High';\nelse if (impactScore >= 40) trendStrength = 'Medium';\n\n// Return enriched data with MCDM scores\nreturn {\n  json: {\n    ...item,\n    mcdm: {\n      topsisScore: Math.round(topsisScore * 1000) / 1000,\n      weightedSumScore: Math.round(weightedSumScore * 1000) / 1000,\n      impactScore: impactScore,\n      trendStrength: trendStrength,\n      signalBreakdown: {\n        velocity: signals.velocity,\n        sentiment: signals.sentiment,\n        changepointMagnitude: signals.changepointMagnitude,\n        novelty: signals.novelty,\n        timeSeriesGrowth: signals.timeSeriesGrowth\n      },\n      normalizedSignals: normalized,\n      weightedSignals: weighted,\n      distanceToBest: Math.round(distanceToBest * 1000) / 1000,\n      distanceToWorst: Math.round(distanceToWorst * 1000) / 1000\n    }\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "65b3005f-d59a-4fbb-a126-0e5cf81e43a6",
      "name": "Rank Trends by Impact",
      "type": "n8n-nodes-base.code",
      "position": [
        3232,
        320
      ],
      "parameters": {
        "jsCode": "// Rank trends by combined impact score, relevance, and maturity stage\n\nconst items = $input.all();\n\n// Process and rank trends\nconst rankedTrends = items.map((item, index) => {\n  const data = item.json;\n  \n  // Extract signals from merged analytics\n  const impactScore = data.impactScore || 0;\n  const velocityScore = data.velocityScore || 0;\n  const sentimentScore = data.sentimentScore || 0;\n  const noveltyScore = data.noveltyScore || 0;\n  const changepointScore = data.changepointScore || 0;\n  \n  // Calculate relevance to AI domain (0-1 scale)\n  const aiKeywords = ['ai', 'artificial intelligence', 'machine learning', 'ml', 'deep learning', \n                      'neural network', 'llm', 'gpt', 'transformer', 'nlp', 'computer vision',\n                      'reinforcement learning', 'generative', 'diffusion', 'embedding'];\n  \n  const topic = (data.topic || '').toLowerCase();\n  const content = (data.content || '').toLowerCase();\n  const combinedText = topic + ' ' + content;\n  \n  const relevanceScore = aiKeywords.reduce((score, keyword) => {\n    return score + (combinedText.includes(keyword) ? 1 : 0);\n  }, 0) / aiKeywords.length;\n  \n  // Determine maturity stage based on velocity and time metrics\n  let maturityStage = 'emerging';\n  const mentionCount = data.mentionCount || 0;\n  const growthRate = data.growthRate || 0;\n  \n  if (mentionCount > 100 && growthRate < 0.1) {\n    maturityStage = 'mature';\n  } else if (mentionCount > 100 && growthRate < 0) {\n    maturityStage = 'declining';\n  } else if (mentionCount > 20 && growthRate > 0.2) {\n    maturityStage = 'growing';\n  }\n  \n  // Calculate combined impact score with weighted factors\n  const combinedImpact = (\n    impactScore * 0.3 +\n    velocityScore * 0.2 +\n    sentimentScore * 0.15 +\n    noveltyScore * 0.2 +\n    changepointScore * 0.15\n  ) * relevanceScore; // Multiply by relevance to prioritize AI-related trends\n  \n  return {\n    json: {\n      ...data,\n      relevanceScore,\n      maturityStage,\n      combinedImpact,\n      rankingMetrics: {\n        impactScore,\n        velocityScore,\n        sentimentScore,\n        noveltyScore,\n        changepointScore,\n        relevanceScore,\n        maturityStage\n      }\n    }\n  };\n});\n\n// Sort by combined impact score (descending)\nrankedTrends.sort((a, b) => b.json.combinedImpact - a.json.combinedImpact);\n\n// Add rank position\nrankedTrends.forEach((item, index) => {\n  item.json.rank = index + 1;\n  item.json.rankedAt = new Date().toISOString();\n});\n\nreturn rankedTrends;"
      },
      "typeVersion": 2
    },
    {
      "id": "9740d6f2-8b8e-4451-90f3-503fe625816f",
      "name": "Store Trend Rankings",
      "type": "n8n-nodes-base.postgres",
      "position": [
        3456,
        320
      ],
      "parameters": {
        "table": {
          "__rl": true,
          "mode": "name",
          "value": "={{ $('Workflow Configuration').first().json.dbTable }}_rankings"
        },
        "schema": {
          "__rl": true,
          "mode": "list",
          "value": "public"
        },
        "columns": {
          "value": {
            "rank": "={{ $json.rank }}",
            "score": "={{ $json.score }}",
            "topic": "={{ $json.topic }}",
            "signals": "={{ $json.signals }}",
            "timestamp": "={{ $json.timestamp }}"
          },
          "mappingMode": "defineBelow"
        },
        "options": {}
      },
      "typeVersion": 2.6
    },
    {
      "id": "e3c252bb-b9bc-4939-8ee2-d332b47ded1d",
      "name": "Check Alert Threshold",
      "type": "n8n-nodes-base.if",
      "position": [
        3680,
        416
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "leftValue": "",
            "caseSensitive": false,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "id-1",
              "operator": {
                "type": "number",
                "operation": "gt"
              },
              "leftValue": "={{ $('Store Trend Rankings').item.json.impactScore }}",
              "rightValue": "={{ $('Workflow Configuration').first().json.alertThreshold }}"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "937b6195-1477-425c-8222-3e69ad6965f7",
      "name": "Generate Trend Summary",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "position": [
        3904,
        320
      ],
      "parameters": {
        "operation": "message"
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "d62439d4-c362-41e6-a191-a47fcb47f5cb",
      "name": "Send Slack Alert",
      "type": "n8n-nodes-base.slack",
      "position": [
        4240,
        416
      ],
      "parameters": {
        "text": "=\ud83d\udea8 *New AI Trend Alert*\n\n{{ $json.message }}",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "id",
          "value": "<__PLACEHOLDER_VALUE__Slack channel ID or name__>"
        },
        "otherOptions": {},
        "authentication": "oAuth2"
      },
      "credentials": {
        "slackOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "9475aae7-ee6e-45df-ba41-2328cfe280d7",
      "name": "Send Email Report",
      "type": "n8n-nodes-base.gmail",
      "position": [
        4240,
        608
      ],
      "parameters": {
        "sendTo": "<__PLACEHOLDER_VALUE__Recipient email address__>",
        "message": "={{ $json.message }}",
        "options": {},
        "subject": "=AI Trend Alert: {{ $json.trendName }}"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "b13c795d-ad6b-4b2e-874f-38037130a5f2",
      "name": "Trigger Workflow Actions",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        4240,
        800
      ],
      "parameters": {
        "url": "<__PLACEHOLDER_VALUE__Workflow action API endpoint URL__>",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ $json }}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "341af4d2-7b57-4001-878f-4163c00450f9",
      "name": "Store Dashboard Data",
      "type": "n8n-nodes-base.postgres",
      "position": [
        3680,
        608
      ],
      "parameters": {
        "table": {
          "__rl": true,
          "mode": "name",
          "value": "={{ $('Workflow Configuration').first().json.dashboardTable }}"
        },
        "schema": {
          "__rl": true,
          "mode": "list",
          "value": "public"
        },
        "columns": {
          "value": {
            "trend_id": "={{ $json.trend_id }}",
            "chart_data": "={{ $json.chart_data }}",
            "chart_type": "={{ $json.chart_type }}",
            "created_at": "={{ $now }}",
            "timeline_data": "={{ $json.timeline_data }}",
            "leaderboard_data": "={{ $json.leaderboard_data }}",
            "visualization_config": "={{ $json.visualization_config }}"
          },
          "mappingMode": "defineBelow"
        },
        "options": {}
      },
      "typeVersion": 2.6
    },
    {
      "id": "c7f65782-b356-4181-bc32-9efa4e60d32f",
      "name": "Track KPIs",
      "type": "n8n-nodes-base.postgres",
      "position": [
        3680,
        800
      ],
      "parameters": {
        "table": {
          "__rl": true,
          "mode": "name",
          "value": "={{ $('Workflow Configuration').first().json.kpiTable }}"
        },
        "schema": {
          "__rl": true,
          "mode": "list",
          "value": "public"
        },
        "columns": {
          "value": {
            "recall": "={{ $json.recall }}",
            "trend_id": "={{ $json.trend_id }}",
            "precision": "={{ $json.precision }}",
            "timestamp": "={{ $now.toISO() }}",
            "alert_accuracy": "={{ $json.alert_accuracy }}",
            "trend_lead_time": "={{ $json.trend_lead_time }}",
            "detection_latency": "={{ $json.detection_latency }}",
            "downstream_impact": "={{ $json.downstream_impact }}",
            "false_positive_rate": "={{ $json.false_positive_rate }}"
          },
          "schema": [],
          "mappingMode": "defineBelow",
          "matchingColumns": []
        },
        "options": {}
      },
      "typeVersion": 2.6
    },
    {
      "id": "9300c075-225d-4c5d-b840-44538a321c82",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        336,
        -48
      ],
      "parameters": {
        "width": 464,
        "height": 240,
        "content": "## How It Works\nA scheduled process aggregates content from eight distinct data sources and standardizes all inputs into a unified format. AI models perform sentiment scoring, detect conspiracy or misinformation signals, and generate trend analyses across domains. An MCDN routing model prioritizes and channels insights to the appropriate workflows. Dashboards visualize real-time analytics, trigger KPIs based on thresholds, and compile comprehensive market-intelligence reports for stakeholders."
      },
      "typeVersion": 1
    },
    {
      "id": "1d58006c-94ed-410c-9238-aef234cffd12",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        816,
        -48
      ],
      "parameters": {
        "width": 480,
        "height": 256,
        "content": "## Setup Steps\n1. **Data Sources:** Connect news APIs, social media platforms, academic databases, code repositories, and documentation feeds.\n2. **AI Analysis:** Configure OpenAI models for sentiment analysis, conspiracy detection, and trend scoring.\n3. **Dashboards:** Integrate analytics platforms and enable automated email or reporting outputs.\n4. **Storage:** Configure a database for historical records, trend archives, and competitive-intelligence storage."
      },
      "typeVersion": 1
    },
    {
      "id": "e1c5bcad-64e9-4084-9cd9-aeee207a2fbf",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2000,
        -32
      ],
      "parameters": {
        "color": 4,
        "width": 432,
        "height": 224,
        "content": "## Customization\nAdjust sentiment thresholds; add/remove data sources; modify analysis rules; extend AI models\n\n## Benefits\nReduces research time 80%; consolidates market intelligence; improves decision accuracy"
      },
      "typeVersion": 1
    },
    {
      "id": "fbe7866c-2701-49a9-b458-9a2506b8b592",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1312,
        -48
      ],
      "parameters": {
        "color": 6,
        "width": 656,
        "height": 240,
        "content": "## Prerequisites\nMulti-source API credentials; OpenAI API key; dashboard platform access; email service; code repository access; academic database credentials\n\n## Use Cases\nCompetitive intelligence monitoring; market trend analysis; technology landscape tracking; product strategy research; misinformation filtering"
      },
      "typeVersion": 1
    },
    {
      "id": "fe5669b8-0743-4e76-a568-0527267b29a9",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        352,
        224
      ],
      "parameters": {
        "color": 7,
        "width": 416,
        "height": 336,
        "content": "## Schedule Daily Collection\nTriggers workflow to fetch news, blogs, social media, academic papers, code, docs\nWhy: Daily cadence captures emerging trends before competitors react"
      },
      "typeVersion": 1
    },
    {
      "id": "0ee5a556-a74b-46ad-8a59-46724f008b65",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        784,
        224
      ],
      "parameters": {
        "color": 7,
        "height": 1456,
        "content": "## Fetch News & Blog Content\nRetrieves articles from tech news sites, industry publications\nWhy: News breaks market shifts earliest\n"
      },
      "typeVersion": 1
    },
    {
      "id": "74636813-6e90-4a32-a3e5-cbdea263fc6d",
      "name": "Sticky Note6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1040,
        224
      ],
      "parameters": {
        "color": 7,
        "width": 272,
        "height": 736,
        "content": "## Aggregate Market Discussions\nCollects earnings calls, investor presentations\nWhy: Direct stakeholder commentary reveals strategy and positioning"
      },
      "typeVersion": 1
    },
    {
      "id": "bfd645a5-7624-4a29-b54a-5fa5747a0aba",
      "name": "Sticky Note7",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1680,
        240
      ],
      "parameters": {
        "color": 7,
        "width": 736,
        "height": 432,
        "content": "## Extract Content & Parse Structure, Storage\nConverts PDFs, web pages, videos, code into machine-readable text with metadata\nWhy: Unified parsing enables cross-source analysis; structures data surfaces patterns\n"
      },
      "typeVersion": 1
    },
    {
      "id": "3d99cba6-840d-4626-bdfb-65ac763cd3ab",
      "name": "Sticky Note8",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1328,
        224
      ],
      "parameters": {
        "color": 7,
        "width": 336,
        "height": 448,
        "content": "\n## Normalize All Formats and Duplicate\nStandardizes timestamps, authorship, hierarch

Credentials you'll need

Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.

Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

A scheduled process aggregates content from eight distinct data sources and standardizes all inputs into a unified format. AI models perform sentiment scoring, detect conspiracy or misinformation signals, and generate trend analyses across domains. An MCDN routing model…

Source: https://n8n.io/workflows/10765/ — original creator credit. Request a take-down →

More AI & RAG workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

AI & RAG

Imagine a dedicated financial expert tirelessly working behind the scenes, sifting through every transaction, every investment move, and every accounting entry. That's exactly what this automated syst

HTTP Request, Google Sheets, OpenAI +3
AI & RAG

Grain Reputation Management Suite v1. Uses httpRequest, openAi, slack, gmail. Scheduled trigger; 14 nodes.

HTTP Request, OpenAI, Slack +1
AI & RAG

User Signup & Verification: The workflow starts when a user signs up. It generates a verification code and sends it via SMS using Twilio. Code Validation: The user replies with the code. The workflow

Postgres, HTTP Request, OpenAI +2
AI & RAG

This workflow monitors filesystem sync and backup jobs by validating their execution logs, not by running or inspecting the jobs themselves.

Google Cloud Storage, Gmail, GitHub +2
AI & RAG

Stop wasting billable hours on manual time-tracking. AutoTimesheet Pro uses AI to collect emails, meetings, and GitHub work, then writes a clean timesheet straight into Google Sheets. Perfect for deve

Google Calendar, Gmail, GitHub +3