AutomationFlowsAI & RAG › Llms_query_links_stabilisation (my Sql)

Llms_query_links_stabilisation (my Sql)

LLMS_Query_Links_stabilisation. Uses mySql, lmChatOpenAi, executeCommand, chainLlm. Webhook trigger; 95 nodes.

Webhook trigger★★★★★ complexityAI-powered95 nodesMySQLOpenAI ChatExecute CommandChain LlmOpenRouter ChatN8N Nodes Openai LangfuseEmail SendOpenAI
AI & RAG Trigger: Webhook Nodes: 95 Complexity: ★★★★★ AI nodes: yes Added:
Llms_query_links_stabilisation (my Sql) — n8n workflow card showing MySQL, OpenAI Chat, Execute Command integration

This workflow follows the Chainllm → Emailsend 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
{
  "updatedAt": "2026-01-06T12:50:54.731Z",
  "createdAt": "2025-11-26T14:54:28.156Z",
  "id": "TmIV9kJgU673IsL3",
  "name": "LLMS_Query_Links_stabilisation",
  "description": null,
  "active": false,
  "isArchived": true,
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "77b4d30b-cf0b-4297-8206-be86d0d4d52e",
        "options": {}
      },
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        -656,
        240
      ],
      "id": "7040b11b-5941-4bc6-9aae-c63db2baf001",
      "name": "Waiting for Topic"
    },
    {
      "parameters": {
        "jsCode": "// This runs for every incoming item\nreturn items.map(item => {\n  const text = $input.first().json.body.topic; // replace with the actual field name\n\n  // Split off the \"Example Vendors\" part\n  const [mainPart, vendorsPart = ''] = text.split('|').map(s => s.trim());\n\n  // Extract topic and definition\n  const [topic = '', definition = ''] = mainPart.split(' - ').map(s => s.trim());\n\n  // Clean up and split the vendors list\n  const exampleVendors = vendorsPart\n    .replace(/^Example Vendors:\\s*/i, '')\n    .split(',')\n    .map(v => v.trim())\n    .filter(v => v);\n\n  return {\n    json: {\n      topic,\n      definition,\n      exampleVendors\n    }\n  };\n});\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -320,
        240
      ],
      "id": "097f7e6b-50c8-45f6-9143-514d7a55cd95",
      "name": "Code"
    },
    {
      "parameters": {
        "table": {
          "__rl": true,
          "value": "Topic",
          "mode": "list",
          "cachedResultName": "Topic"
        },
        "dataMode": "defineBelow",
        "valuesToSend": {
          "values": [
            {
              "column": "name",
              "value": "={{ $('Code').item.json.topic }}"
            },
            {
              "column": "frequency",
              "value": "={{ $('Waiting for Topic').item.json.body.frequency }}"
            },
            {
              "column": "search_top_x",
              "value": "={{ $('Waiting for Topic').item.json.body.searchTopX }}"
            },
            {
              "column": "user_id",
              "value": "={{ $('Waiting for Topic').item.json.body.userId }}"
            },
            {
              "column": "last_execution",
              "value": "={{ $('Waiting for Topic').item.json.body.search_datetime }}"
            },
            {
              "column": "is_newsletter",
              "value": "0"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.mySql",
      "typeVersion": 2.4,
      "position": [
        320,
        368
      ],
      "id": "4164d0aa-6ca4-4d73-b7c2-e016ea20643f",
      "name": "Inserting New Topic to Database",
      "credentials": {
        "mySql": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "operation": "select",
        "table": {
          "__rl": true,
          "value": "Topic",
          "mode": "list",
          "cachedResultName": "Topic"
        },
        "limit": 1,
        "where": {
          "values": [
            {
              "column": "name",
              "value": "={{ $('Code').item.json.topic }}"
            },
            {
              "column": "user_id",
              "value": "={{ $('Waiting for Topic').item.json.body.userId }}"
            },
            {
              "column": "last_execution",
              "value": "={{ $('Waiting for Topic').item.json.body.search_datetime }}"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.mySql",
      "typeVersion": 2.4,
      "position": [
        560,
        368
      ],
      "id": "e032cb58-1b32-4c8a-adec-2cc11aa026fb",
      "name": "Retrieving New Topic ID",
      "credentials": {
        "mySql": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Access the first item from the input\nconst webhookData = $('Waiting for Topic').first().json.body;\nconst idtopic     = $input.first().json.id;\n\n// Frequency mapping function\nfunction mapFrequency(freq) {\n  if (freq.toLowerCase() === \"daily\")   return \"d\";\n  if (freq.toLowerCase() === \"weekly\")  return \"w\";\n  if (freq.toLowerCase() === \"monthly\") return \"m\";\n  return freq; // Return original if not matched\n}\n\n// Extracting values\nconst user_id           = webhookData.user_id;\nconst topic             = $('Code').first().json.topic;\nconst topicDescription  = $('Code').first().json.definition;  // \u2190 new\nconst email             = webhookData.email;\nconst top_k             = webhookData.top_k;\nconst frequency         = mapFrequency(webhookData.frequency);\nconst searchDatetime    = webhookData.search_datetime;\n\n// Return these values so they can be used by subsequent nodes\nreturn [\n  {\n    json: {\n      user_id,\n      topic,\n      topicDescription,   // \u2190 included here\n      email,\n      top_k,\n      frequency,\n      searchDatetime,\n      idtopic,\n    },\n  },\n];\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        800,
        368
      ],
      "id": "7a138697-0a96-48c7-aead-e66639a711d8",
      "name": "Preparing New Input variables"
    },
    {
      "parameters": {
        "fieldsToAggregate": {
          "fieldToAggregate": [
            {
              "fieldToAggregate": "topic"
            },
            {
              "fieldToAggregate": "email"
            },
            {
              "fieldToAggregate": "frequency"
            },
            {
              "fieldToAggregate": "searchDatetime"
            },
            {
              "fieldToAggregate": "idtopic"
            },
            {
              "fieldToAggregate": "topicDescription"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.aggregate",
      "typeVersion": 1,
      "position": [
        992,
        272
      ],
      "id": "fe63cffe-e919-43c7-a69d-cc29a883edc4",
      "name": "Aggregating Input Variables"
    },
    {
      "parameters": {
        "operation": "select",
        "table": {
          "__rl": true,
          "value": "Topic",
          "mode": "list",
          "cachedResultName": "Topic"
        },
        "limit": 1,
        "where": {
          "values": [
            {
              "column": "user_id",
              "value": "={{ $('Waiting for Topic').item.json.body.userId }}"
            },
            {
              "column": "name",
              "value": "={{ $('Code').item.json.topic }}"
            }
          ]
        },
        "options": {
          "detailedOutput": true
        }
      },
      "type": "n8n-nodes-base.mySql",
      "typeVersion": 2.4,
      "position": [
        -48,
        240
      ],
      "id": "a38f33b9-14bd-45a7-884e-3d9ffb68ef9b",
      "name": "Check Existence of the Topic",
      "credentials": {
        "mySql": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "cc270b1f-4867-4312-87ed-677d9e35aa5e",
              "leftValue": "={{ $json.data }}",
              "rightValue": "",
              "operator": {
                "type": "array",
                "operation": "notEmpty",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        144,
        240
      ],
      "id": "9b783d52-9776-4cf7-9277-f7d243276e1b",
      "name": "Check existence of the Received Topic"
    },
    {
      "parameters": {
        "jsCode": "// Access the first item from the input\nconst webhookData = $('Waiting for Topic').first().json.body;\nconst idtopic     = $input.first().json.data[0].id;\n\n// Frequency mapping function\nfunction mapFrequency(freq) {\n  if (freq.toLowerCase() === \"daily\")   return \"d\";\n  if (freq.toLowerCase() === \"weekly\")  return \"w\";\n  if (freq.toLowerCase() === \"monthly\") return \"m\";\n  return freq; // Return original if not matched\n}\n\n// Extracting values\nconst user_id           = webhookData.user_id;\nconst topic             = $('Code').first().json.topic;\nconst topicDescription  = $('Code').first().json.definition;   // \u2190 new\nconst email             = webhookData.email;\nconst top_k             = webhookData.top_k;\nconst frequency         = mapFrequency(webhookData.frequency);\nconst searchDatetime    = webhookData.search_datetime;\n\n// Return these values so they can be used by subsequent nodes\nreturn [\n  {\n    json: {\n      user_id,\n      topic,\n      topicDescription,   // \u2190 included here\n      email,\n      top_k,\n      frequency,\n      searchDatetime,\n      idtopic,\n    },\n  },\n];\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        544,
        32
      ],
      "id": "55aad3aa-bca3-41e1-998a-031d403b4efd",
      "name": "Preparing Input Variables"
    },
    {
      "parameters": {
        "model": {
          "__rl": true,
          "value": "gpt-5.1",
          "mode": "list",
          "cachedResultName": "gpt-5.1"
        },
        "options": {
          "timeout": 600000
        }
      },
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "typeVersion": 1.2,
      "position": [
        2016,
        -272
      ],
      "id": "92912fe1-2b4b-434f-8337-46cdd1886e16",
      "name": "OpenAI Chat Model",
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "command": "=python3 /home/node/.n8n/scripts/searcher_link.py '{{ $json.text.replace(/\"/g, \"\").trim() }}'\n"
      },
      "type": "n8n-nodes-base.executeCommand",
      "typeVersion": 1,
      "position": [
        2512,
        -560
      ],
      "id": "04bd60c1-6886-4f4f-96e7-29c9df1f2673",
      "name": "Searching Links",
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "// Replace \"stdout\" with your actual field name containing the JSON array of { link, title } objects\nconst jsonString = $input.first().json.stdout;\nif (!jsonString) {\n  return [];\n}\n\nlet parsedData;\ntry {\n  parsedData = JSON.parse(jsonString);\n} catch (error) {\n  // If it's not valid JSON or fails to parse, return an empty array\n  return [];\n}\n\n// Ensure parsedData is an array before we iterate\nif (!Array.isArray(parsedData)) {\n  return [];\n}\n\n// Create a new array of items, each with a \"url\" and \"title\"\nreturn parsedData.map(item => {\n  return {\n    json: {\n      url: item.link || \"\",\n      title: item.title || \"\"\n    }\n  };\n});\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2800,
        -400
      ],
      "id": "cc70bc19-4de1-47eb-a556-05b706d68e1b",
      "name": "Preparing Links to Looping"
    },
    {
      "parameters": {
        "options": {}
      },
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 3,
      "position": [
        1888,
        -592
      ],
      "id": "cdcfc2ab-ff82-4a1e-9c91-8eef78f28be2",
      "name": "Loop Over Items"
    },
    {
      "parameters": {},
      "type": "n8n-nodes-base.noOp",
      "name": "Replace Me",
      "typeVersion": 1,
      "position": [
        3280,
        -208
      ],
      "id": "77d991a2-6909-4e0a-b84e-ab21a4d8f242"
    },
    {
      "parameters": {
        "aggregate": "aggregateAllItemData",
        "destinationFieldName": "={{ $json.url }}\n{{ $json.title }}",
        "options": {}
      },
      "type": "n8n-nodes-base.aggregate",
      "typeVersion": 1,
      "position": [
        3072,
        -656
      ],
      "id": "812319fd-2b02-4038-8937-a1e5cd392da3",
      "name": "Aggregate1"
    },
    {
      "parameters": {
        "promptType": "define",
        "text": "=\nUser Goal: {{ $json.topic }} \n\nQueries:\n(Provide 6-10 queries below, following the guidelines above. Each should naturally include mentions of \u201cAI\u201d or \u201cgenerative AI,\u201d and focus on finding suppliers or vendors in the given domain.)\nthe answer must respect this format : \"\"Query1,Query2,Query3,..,QueryN\"\"\n\nrule :  you response must be  between  only 2 quotes not 3\nMust : the format of the output must be like  \"\"Queries\"\"",
        "messages": {
          "messageValues": [
            {
              "message": "=You are an expert in crafting highly optimized search queries for Google's SERP API.  Your task is to generate 10 search queries to collect information related to suppliers or vendors offering solutions powered by AI or generative AI related to the user\u2019s specified topic.  \ud83d\udd39 Instructions & Best Practices: User\u2019s Topic -> The user will provide :     a topic that either matches an IT domain recognized by leading IT analysts such as Gartner group (e.g., Intelligent Document Processing (IDP), etc.)  Or a  topic that  corresponds to more detailed IT function  (example : \u2018Audit CCTP\u2019   Focus: The queries must revolve around finding suppliers offering AI-driven or generative AI solutions in that topic area.  Natural Language: Ensure the queries read like typical Google searches (no advanced operators like site:, intitle:, inurl:, etc.).  Diversity: Provide queries that explore different angles\u2014such as solution providers value proposition, client case studies and implementation strategies, solutions benchmark in term of pricing and functions, , , or industry trends.  Concise & Precise: Keep each query short (max 10 words)  yet descriptive for optimal search results.  \ud83d\udd39Example :  #Input :  query for topic that matches an IT domain recognized by leading IT analysts such as Gartner group such as \u00ab Intelligent Document Processing (IDP), etc.) \u00bb   \u00ab Les solutions d\u2019Intelligent Document Processing. \u00bb  #Output :   Query 1 : software providers Intelligent Document Processing AI Query 2 : client success story Intelligent Document Processing AI Query 3 : benchmark Intelligent Document Processing AI Query 4 : implementation Intelligent Document Processing AI   \ud83d\udd39 Generate  10  Structured Search Queries  rule :  you response must be  between  only 2 quotes not 3"
            }
          ]
        },
        "batching": {}
      },
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        2176,
        -496
      ],
      "id": "bb109c43-f4a4-4db1-8f51-36f962864d28",
      "name": "Generating Queries GPT-5.0"
    },
    {
      "parameters": {
        "jsCode": "// R\u00e9cup\u00e9rer tous les items\nconst allItems = $input.all();\n\nconsole.log('Total items re\u00e7us:', allItems.length);\n\n// Afficher la structure de chaque item pour debug\nallItems.forEach((item, index) => {\n  console.log(`Item ${index}:`, JSON.stringify(item.json, null, 2));\n});\n\nconst allUrls = [];\n\n// Essayer diff\u00e9rentes structures possibles\nfor (const item of allItems) {\n  console.log('Keys dans item.json:', Object.keys(item.json));\n  \n  // Structure 1: url directement\n  if (item.json.url) {\n    allUrls.push({\n      url: item.json.url,\n      title: item.json.title || ''\n    });\n  }\n  // Structure 2: tableau d'URLs\n  else if (Array.isArray(item.json)) {\n    item.json.forEach(urlItem => {\n      if (urlItem.url) {\n        allUrls.push({\n          url: urlItem.url,\n          title: urlItem.title || ''\n        });\n      }\n    });\n  }\n  // Structure 3: objet avec des propri\u00e9t\u00e9s URL\n  else {\n    // Parcourir toutes les propri\u00e9t\u00e9s de l'objet\n    Object.entries(item.json).forEach(([key, value]) => {\n      // Si la valeur ressemble \u00e0 une URL\n      if (typeof value === 'string' && value.startsWith('http')) {\n        allUrls.push({\n          url: value,\n          title: key\n        });\n      }\n      // Si c'est un tableau\n      else if (Array.isArray(value)) {\n        value.forEach(v => {\n          if (v && v.url) {\n            allUrls.push({\n              url: v.url,\n              title: v.title || ''\n            });\n          }\n        });\n      }\n    });\n  }\n}\n\nconsole.log('URLs trouv\u00e9es:', allUrls.length);\n\nreturn [{\n  json: {\n    totalUrls: allUrls.length,\n    urls: allUrls,\n    debugInfo: allItems.map(i => Object.keys(i.json))\n  }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2304,
        -800
      ],
      "id": "4a9d1545-6735-4956-8082-d4a4396c3aab",
      "name": "All Links"
    },
    {
      "parameters": {
        "model": {
          "__rl": true,
          "value": "gpt-5.1",
          "mode": "list",
          "cachedResultName": "gpt-5.1"
        },
        "options": {
          "timeout": 600000
        }
      },
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "typeVersion": 1.2,
      "position": [
        2288,
        592
      ],
      "id": "bca6130c-f77a-4823-9025-ace750a30bd5",
      "name": "OpenAI Chat Model1",
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "command": "=python3 /home/node/.n8n/scripts/searcher_link.py '{{ $json.text.replace(/\"/g, \"\").trim() }}'\n"
      },
      "type": "n8n-nodes-base.executeCommand",
      "typeVersion": 1,
      "position": [
        2496,
        304
      ],
      "id": "b6cfd852-9c49-477f-af66-4badbec6573c",
      "name": "Searching Links1"
    },
    {
      "parameters": {
        "jsCode": "// Replace \"stdout\" with your actual field name containing the JSON array of { link, title } objects\nconst jsonString = $input.first().json.stdout;\nif (!jsonString) {\n  return [];\n}\n\nlet parsedData;\ntry {\n  parsedData = JSON.parse(jsonString);\n} catch (error) {\n  // If it's not valid JSON or fails to parse, return an empty array\n  return [];\n}\n\n// Ensure parsedData is an array before we iterate\nif (!Array.isArray(parsedData)) {\n  return [];\n}\n\n// Create a new array of items, each with a \"url\" and \"title\"\nreturn parsedData.map(item => {\n  return {\n    json: {\n      url: item.link || \"\",\n      title: item.title || \"\"\n    }\n  };\n});\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2784,
        464
      ],
      "id": "89ddaa02-c834-4b68-ac3e-36ca9a6562b1",
      "name": "Preparing Links to Looping1"
    },
    {
      "parameters": {
        "options": {}
      },
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 3,
      "position": [
        1872,
        272
      ],
      "id": "87a81439-f29e-4bb9-8c5e-4f9fa70a779a",
      "name": "Loop Over Items1"
    },
    {
      "parameters": {},
      "type": "n8n-nodes-base.noOp",
      "name": "Replace Me1",
      "typeVersion": 1,
      "position": [
        3264,
        656
      ],
      "id": "6a11cbe0-f1b1-4e9c-a1a0-5f13fa059d7c"
    },
    {
      "parameters": {
        "aggregate": "aggregateAllItemData",
        "destinationFieldName": "={{ $json.url }}\n{{ $json.title }}",
        "options": {}
      },
      "type": "n8n-nodes-base.aggregate",
      "typeVersion": 1,
      "position": [
        3056,
        208
      ],
      "id": "fe2027a6-ea3d-4c4d-bf9a-66c937751c41",
      "name": "Aggregate"
    },
    {
      "parameters": {
        "jsCode": "// R\u00e9cup\u00e9rer tous les items\nconst allItems = $input.all();\n\nconsole.log('Total items re\u00e7us:', allItems.length);\n\n// Afficher la structure de chaque item pour debug\nallItems.forEach((item, index) => {\n  console.log(`Item ${index}:`, JSON.stringify(item.json, null, 2));\n});\n\nconst allUrls = [];\n\n// Essayer diff\u00e9rentes structures possibles\nfor (const item of allItems) {\n  console.log('Keys dans item.json:', Object.keys(item.json));\n  \n  // Structure 1: url directement\n  if (item.json.url) {\n    allUrls.push({\n      url: item.json.url,\n      title: item.json.title || ''\n    });\n  }\n  // Structure 2: tableau d'URLs\n  else if (Array.isArray(item.json)) {\n    item.json.forEach(urlItem => {\n      if (urlItem.url) {\n        allUrls.push({\n          url: urlItem.url,\n          title: urlItem.title || ''\n        });\n      }\n    });\n  }\n  // Structure 3: objet avec des propri\u00e9t\u00e9s URL\n  else {\n    // Parcourir toutes les propri\u00e9t\u00e9s de l'objet\n    Object.entries(item.json).forEach(([key, value]) => {\n      // Si la valeur ressemble \u00e0 une URL\n      if (typeof value === 'string' && value.startsWith('http')) {\n        allUrls.push({\n          url: value,\n          title: key\n        });\n      }\n      // Si c'est un tableau\n      else if (Array.isArray(value)) {\n        value.forEach(v => {\n          if (v && v.url) {\n            allUrls.push({\n              url: v.url,\n              title: v.title || ''\n            });\n          }\n        });\n      }\n    });\n  }\n}\n\nconsole.log('URLs trouv\u00e9es:', allUrls.length);\n\nreturn [{\n  json: {\n    totalUrls: allUrls.length,\n    urls: allUrls,\n    debugInfo: allItems.map(i => Object.keys(i.json))\n  }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2288,
        64
      ],
      "id": "10bce842-418c-44a5-ab1d-434acbd72354",
      "name": "All Links1"
    },
    {
      "parameters": {
        "command": "=python3 /home/node/.n8n/scripts/searcher_link.py '{{ $json.text.replace(/\"/g, \"\").trim() }}'\n"
      },
      "type": "n8n-nodes-base.executeCommand",
      "typeVersion": 1,
      "position": [
        2480,
        1200
      ],
      "id": "02e5c772-c0e0-485f-9969-5b4e0da3c444",
      "name": "Searching Links2"
    },
    {
      "parameters": {
        "jsCode": "// Replace \"stdout\" with your actual field name containing the JSON array of { link, title } objects\nconst jsonString = $input.first().json.stdout;\nif (!jsonString) {\n  return [];\n}\n\nlet parsedData;\ntry {\n  parsedData = JSON.parse(jsonString);\n} catch (error) {\n  // If it's not valid JSON or fails to parse, return an empty array\n  return [];\n}\n\n// Ensure parsedData is an array before we iterate\nif (!Array.isArray(parsedData)) {\n  return [];\n}\n\n// Create a new array of items, each with a \"url\" and \"title\"\nreturn parsedData.map(item => {\n  return {\n    json: {\n      url: item.link || \"\",\n      title: item.title || \"\"\n    }\n  };\n});\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2768,
        1360
      ],
      "id": "39a31b18-9895-4ac2-83ef-213c571a7ad6",
      "name": "Preparing Links to Looping2"
    },
    {
      "parameters": {
        "options": {}
      },
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 3,
      "position": [
        1856,
        1168
      ],
      "id": "a0e38d9d-97eb-4116-9c4a-a54a8d691d7c",
      "name": "Loop Over Items2",
      "disabled": true
    },
    {
      "parameters": {},
      "type": "n8n-nodes-base.noOp",
      "name": "Replace Me2",
      "typeVersion": 1,
      "position": [
        3248,
        1552
      ],
      "id": "70ed9f4e-09ff-4ec4-bf3a-2f47acf0e74c"
    },
    {
      "parameters": {
        "aggregate": "aggregateAllItemData",
        "destinationFieldName": "={{ $json.url }}\n{{ $json.title }}",
        "options": {}
      },
      "type": "n8n-nodes-base.aggregate",
      "typeVersion": 1,
      "position": [
        3040,
        1104
      ],
      "id": "a2ba0609-3c52-448f-9d1c-5a125c8a2cbb",
      "name": "Aggregate2"
    },
    {
      "parameters": {
        "jsCode": "// R\u00e9cup\u00e9rer tous les items\nconst allItems = $input.all();\n\nconsole.log('Total items re\u00e7us:', allItems.length);\n\n// Afficher la structure de chaque item pour debug\nallItems.forEach((item, index) => {\n  console.log(`Item ${index}:`, JSON.stringify(item.json, null, 2));\n});\n\nconst allUrls = [];\n\n// Essayer diff\u00e9rentes structures possibles\nfor (const item of allItems) {\n  console.log('Keys dans item.json:', Object.keys(item.json));\n  \n  // Structure 1: url directement\n  if (item.json.url) {\n    allUrls.push({\n      url: item.json.url,\n      title: item.json.title || ''\n    });\n  }\n  // Structure 2: tableau d'URLs\n  else if (Array.isArray(item.json)) {\n    item.json.forEach(urlItem => {\n      if (urlItem.url) {\n        allUrls.push({\n          url: urlItem.url,\n          title: urlItem.title || ''\n        });\n      }\n    });\n  }\n  // Structure 3: objet avec des propri\u00e9t\u00e9s URL\n  else {\n    // Parcourir toutes les propri\u00e9t\u00e9s de l'objet\n    Object.entries(item.json).forEach(([key, value]) => {\n      // Si la valeur ressemble \u00e0 une URL\n      if (typeof value === 'string' && value.startsWith('http')) {\n        allUrls.push({\n          url: value,\n          title: key\n        });\n      }\n      // Si c'est un tableau\n      else if (Array.isArray(value)) {\n        value.forEach(v => {\n          if (v && v.url) {\n            allUrls.push({\n              url: v.url,\n              title: v.title || ''\n            });\n          }\n        });\n      }\n    });\n  }\n}\n\nconsole.log('URLs trouv\u00e9es:', allUrls.length);\n\nreturn [{\n  json: {\n    totalUrls: allUrls.length,\n    urls: allUrls,\n    debugInfo: allItems.map(i => Object.keys(i.json))\n  }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2272,
        960
      ],
      "id": "34dc5f55-9d3d-4772-b831-1210bd2bb51a",
      "name": "All Links2"
    },
    {
      "parameters": {
        "promptType": "define",
        "text": "=\nUser Goal: {{ $json.topic }} \n\nQueries:\n(Provide 6-10 queries below, following the guidelines above. Each should naturally include mentions of \u201cAI\u201d or \u201cgenerative AI,\u201d and focus on finding suppliers or vendors in the given domain.)\nthe answer must respect this format : \"\"Query1,Query2,Query3,..,QueryN\"\"\n\nrule :  you response must be  between  only 2 quotes not 3\nMust : the format of the output must be like  \"\"Queries\"\"",
        "messages": {
          "messageValues": [
            {
              "message": "=You are an expert in crafting highly optimized search queries for Google's SERP API.  Your task is to generate 10 search queries to collect information related to suppliers or vendors offering solutions powered by AI or generative AI related to the user\u2019s specified topic.  \ud83d\udd39 Instructions & Best Practices: User\u2019s Topic -> The user will provide :     a topic that either matches an IT domain recognized by leading IT analysts such as Gartner group (e.g., Intelligent Document Processing (IDP), etc.)  Or a  topic that  corresponds to more detailed IT function  (example : \u2018Audit CCTP\u2019   Focus: The queries must revolve around finding suppliers offering AI-driven or generative AI solutions in that topic area.  Natural Language: Ensure the queries read like typical Google searches (no advanced operators like site:, intitle:, inurl:, etc.).  Diversity: Provide queries that explore different angles\u2014such as solution providers value proposition, client case studies and implementation strategies, solutions benchmark in term of pricing and functions, , , or industry trends.  Concise & Precise: Keep each query short (max 10 words)  yet descriptive for optimal search results.  \ud83d\udd39Example :  #Input :  query for topic that matches an IT domain recognized by leading IT analysts such as Gartner group such as \u00ab Intelligent Document Processing (IDP), etc.) \u00bb   \u00ab Les solutions d\u2019Intelligent Document Processing. \u00bb  #Output :   Query 1 : software providers Intelligent Document Processing AI Query 2 : client success story Intelligent Document Processing AI Query 3 : benchmark Intelligent Document Processing AI Query 4 : implementation Intelligent Document Processing AI   \ud83d\udd39 Generate  10  Structured Search Queries  rule :  you response must be  between  only 2 quotes not 3"
            }
          ]
        },
        "batching": {}
      },
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        2144,
        1264
      ],
      "id": "34d26350-4b53-4da1-977c-3ac8ddc50cf6",
      "name": "Generating Queries GEMINI-3"
    },
    {
      "parameters": {
        "model": "google/gemini-3-pro-preview",
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
      "typeVersion": 1,
      "position": [
        2000,
        1488
      ],
      "id": "cd54869c-bf59-49c6-a19e-20b35bd72f6b",
      "name": "OpenRouter Chat Model",
      "credentials": {
        "openRouterApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "numberInputs": 3
      },
      "type": "n8n-nodes-base.merge",
      "typeVersion": 3.2,
      "position": [
        4112,
        240
      ],
      "id": "499497d4-904d-4b50-9cca-6f870dd48fbe",
      "name": "Merge",
      "disabled": true
    },
    {
      "parameters": {
        "jsCode": "// R\u00e9cup\u00e9rer tous les items des n\u0153uds All Links\nconst allItems = $input.all();\n\nconsole.log('Total items re\u00e7us:', allItems.length);\n\n// Variable pour compter le nombre total d'URLs BRUTES (avec doublons)\nlet totalRawUrls = 0; \n\n// Map pour compter les occurrences : url -> {count, title}\nconst urlCounts = new Map();\n\n// Fonction utilitaire pour ajouter une URL au compteur\nconst countUrl = (urlItem) => {\n    if (urlItem && urlItem.url) {\n        // Incr\u00e9menter le compteur BRUT avant toute d\u00e9duplication\n        totalRawUrls++; \n        \n        // Normaliser l'URL (par exemple, supprimer le slash final pour \u00e9viter les doublons si non significatif)\n        let url = urlItem.url.trim().replace(/\\/$/, \"\"); \n        \n        const title = urlItem.title || '';\n\n        // Compter les occurrences UNIQUES\n        if (urlCounts.has(url)) {\n            urlCounts.get(url).count++;\n        } else {\n            urlCounts.set(url, { count: 1, title: title });\n        }\n    }\n};\n\n// Parcourir tous les items\nfor (const item of allItems) {\n    const data = item.json;\n    // ... (Logique de parcours des structures d'entr\u00e9e inchang\u00e9e)\n    if (data.url && typeof data.url === 'string') {\n        countUrl(data);\n    } else if (Array.isArray(data)) {\n        data.forEach(countUrl);\n    } else if (typeof data === 'object' && data !== null) {\n        for (const [key, value] of Object.entries(data)) {\n            if (Array.isArray(value)) {\n                value.forEach(countUrl);\n            } else if (value && typeof value === 'object' && value.url) {\n                 countUrl(value);\n            }\n        }\n    }\n}\n\nconsole.log('Total URLs uniques trouv\u00e9es (d\u00e9doublonn\u00e9es):', urlCounts.size);\nconsole.log('Total URLs brutes trouv\u00e9es:', totalRawUrls); // Nouveau log\n\n// --- LOGIQUE POUR CALCULER LES STATISTIQUES COMPL\u00c8TES ---\nconst allSortedUrls = Array.from(urlCounts.entries())\n    .map(([url, data]) => ({\n        url: url,\n        title: data.title,\n        occurrences: data.count\n    }))\n    .sort((a, b) => b.occurrences - a.occurrences);\n\nconst top80Urls = allSortedUrls.slice(0, 80); \n\nconst totalUrlsAfterTop80 = allSortedUrls.length - top80Urls.length;\nconst occurrenceAfterTop80 = allSortedUrls[80]?.occurrences || 0;\n\n// Retourner la liste des 80 URLs les plus fr\u00e9quentes PLUS les stats\nreturn [{\n    json: {\n        totalUrlsSelected: top80Urls.length,\n        totalUniqueUrls: allSortedUrls.length,\n        totalRawUrls: totalRawUrls, // AJOUT DE LA VALEUR BRUTE\n        urls: top80Urls\n    }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        4656,
        272
      ],
      "id": "15038fec-04bd-4e6b-9ae8-38e3bdd7e5b8",
      "name": "Code1",
      "disabled": true
    },
    {
      "parameters": {
        "promptType": "define",
        "text": "=\nUser Goal: {{ $json.topic }} \n\nQueries:\n(Provide 6-10 queries below, following the guidelines above. Each should naturally include mentions of \u201cAI\u201d or \u201cgenerative AI,\u201d and focus on finding suppliers or vendors in the given domain.)\nthe answer must respect this format : \"\"Query1,Query2,Query3,..,QueryN\"\"\n\nrule :  you response must be  between  only 2 quotes not 3\nMust : the format of the output must be like  \"\"Queries\"\"",
        "messages": {
          "messageValues": [
            {
              "message": "=You are an expert in crafting highly optimized search queries for Google's SERP API.  Your task is to generate 10 search queries to collect information related to suppliers or vendors offering solutions powered by AI or generative AI related to the user\u2019s specified topic.  \ud83d\udd39 Instructions & Best Practices: User\u2019s Topic -> The user will provide :     a topic that either matches an IT domain recognized by leading IT analysts such as Gartner group (e.g., Intelligent Document Processing (IDP), etc.)  Or a  topic that  corresponds to more detailed IT function  (example : \u2018Audit CCTP\u2019   Focus: The queries must revolve around finding suppliers offering AI-driven or generative AI solutions in that topic area.  Natural Language: Ensure the queries read like typical Google searches (no advanced operators like site:, intitle:, inurl:, etc.).  Diversity: Provide queries that explore different angles\u2014such as solution providers value proposition, client case studies and implementation strategies, solutions benchmark in term of pricing and functions, , , or industry trends.  Concise & Precise: Keep each query short (max 10 words)  yet descriptive for optimal search results.  \ud83d\udd39Example :  #Input :  query for topic that matches an IT domain recognized by leading IT analysts such as Gartner group such as \u00ab Intelligent Document Processing (IDP), etc.) \u00bb   \u00ab Les solutions d\u2019Intelligent Document Processing. \u00bb  #Output :   Query 1 : software providers Intelligent Document Processing AI Query 2 : client success story Intelligent Document Processing AI Query 3 : benchmark Intelligent Document Processing AI Query 4 : implementation Intelligent Document Processing AI   \ud83d\udd39 Generate  10  Structured Search Queries  rule :  you response must be  between  only 2 quotes not 3"
            }
          ]
        },
        "batching": {}
      },
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        2160,
        368
      ],
      "id": "358a45e5-b904-4de5-bfa9-9c1d450be3e8",
      "name": "Generating Queries GPT-5.1"
    },
    {
      "parameters": {
        "model": "mistralai/mistral-large-2411",
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
      "typeVersion": 1,
      "position": [
        2256,
        -256
      ],
      "id": "19bb2907-19e2-4eaa-84d4-fb9aaed667b5",
      "name": "OpenRouter Chat Model1",
      "credentials": {
        "openRouterApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "model": "moonshotai/kimi-k2",
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
      "typeVersion": 1,
      "position": [
        2480,
        576
      ],
      "id": "d670beb6-defe-420c-a9ae-89f60a3e0610",
      "name": "OpenRouter Chat Model2",
      "credentials": {
        "openRouterApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "model": "deepseek/deepseek-v3.2-exp",
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
      "typeVersion": 1,
      "position": [
        2464,
        1456
      ],
      "id": "488e65f0-9f7b-439f-84b2-1fb1fe001945",
      "name": "OpenRouter Chat Model3",
      "credentials": {
        "openRouterApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "langfuseMetadata": {},
        "model": {
          "__rl": true,
          "value": "google/gemini-3-pro-preview",
          "mode": "list",
          "cachedResultName": "google/gemini-3-pro-preview"
        },
        "options": {}
      },
      "type": "n8n-nodes-openai-langfuse.lmChatOpenAiLangfuse",
      "typeVersion": 3,
      "position": [
        1952,
        608
      ],
      "id": "97737fa4-3732-451c-a0d0-8a4a690ba47e",
      "name": "OpenAI Chat Model with Langfuse",
      "credentials": {
        "openAiApiWithLangfuseApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "langfuseMetadata": {},
        "model": {
          "__rl": true,
          "value": "deepseek/deepseek-chat-v3.1",
          "mode": "list",
          "cachedResultName": "deepseek/deepseek-chat-v3.1"
        },
        "options": {}
      },
      "type": "n8n-nodes-openai-langfuse.lmChatOpenAiLangfuse",
      "typeVersion": 3,
      "position": [
        2240,
        1472
      ],
      "id": "c172bdf3-664c-4894-b703-f0e24713da55",
      "name": "OpenAI Chat Model with Langfuse1",
      "credentials": {
        "openAiApiWithLangfuseApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "langfuseMetadata": {},
        "model": {
          "__rl": true,
          "value": "deepseek/deepseek-chat-v3.1",
          "mode": "list",
          "cachedResultName": "deepseek/deepseek-chat-v3.1"
        },
        "options": {}
      },
      "type": "n8n-nodes-openai-langfuse.lmChatOpenAiLangfuse",
      "typeVersion": 3,
      "position": [
        2432,
        -256
      ],
      "id": "7b33da97-f5e4-471c-8f87-2f17b08f23c2",
      "name": "OpenAI Chat Model with Langfuse2",
      "credentials": {
        "openAiApiWithLangfuseApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// R\u00e9cup\u00e9rer tous les items des n\u0153uds All Links\nconst allItems = $input.all();\n\nconsole.log('Total items re\u00e7us:', allItems.length);\n\n// Variable pour compter le nombre total d'URLs BRUTES (avec doublons)\nlet totalRawUrls = 0; \n\n// Map pour compter les occurrences : url -> {count, title}\nconst urlCounts = new Map();\n\n// Fonction utilitaire pour ajouter une URL au compteur\nconst countUrl = (urlItem) => {\n    if (urlItem && urlItem.url) {\n        // Incr\u00e9menter le compteur BRUT avant toute d\u00e9duplication\n        totalRawUrls++; \n        \n        // Normaliser l'URL (par exemple, supprimer le slash final pour \u00e9viter les doublons si non significatif)\n        let url = urlItem.url.trim().replace(/\\/$/, \"\"); \n        \n        const title = urlItem.title || '';\n\n        // Compter les occurrences UNIQUES\n        if (urlCounts.has(url)) {\n            urlCounts.get(url).count++;\n        } else {\n            urlCounts.set(url, { count: 1, title: title });\n        }\n    }\n};\n\n// Parcourir tous les items\nfor (const item of allItems) {\n    const data = item.json;\n    // ... (Logique de parcours des structures d'entr\u00e9e inchang\u00e9e)\n    if (data.url && typeof data.url === 'string') {\n        countUrl(data);\n    } else if (Array.isArray(data)) {\n        data.forEach(countUrl);\n    } else if (typeof data === 'object' && data !== null) {\n        for (const [key, value] of Object.entries(data)) {\n            if (Array.isArray(value)) {\n                value.forEach(countUrl);\n            } else if (value && typeof value === 'object' && value.url) {\n                 countUrl(value);\n            }\n        }\n    }\n}\n\nconsole.log('Total URLs uniques trouv\u00e9es (d\u00e9doublonn\u00e9es):', urlCounts.size);\nconsole.log('Total URLs brutes trouv\u00e9es:', totalRawUrls); // Nouveau log\n\n// --- LOGIQUE POUR CALCULER LES STATISTIQUES COMPL\u00c8TES ---\nconst allSortedUrls = Array.from(urlCounts.entries())\n    .map(([url, data]) => ({\n        url: url,\n        title: data.title,\n        occurrences: data.count\n    }))\n    .sort((a, b) => b.occurrences - a.occurrences);\n\nconst top80Urls = allSortedUrls.slice(0, 80); \n\nconst totalUrlsAfterTop80 = allSortedUrls.length - top80Urls.length;\nconst occurrenceAfterTop80 = allSortedUrls[80]?.occurrences || 0;\n\n// Retourner la liste des 80 URLs les plus fr\u00e9quentes PLUS les stats\nreturn [{\n    json: {\n        totalUrlsSelected: top80Urls.length,\n        totalUniqueUrls: allSortedUrls.length,\n        totalRawUrls: totalRawUrls, // AJOUT DE LA VALEUR BRUTE\n        urls: top80Urls\n    }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3952,
        -800
      ],
      "id": "bac57d9b-b396-4f57-8f3b-0125e3819c6d",
      "name": "Code2"
    },
    {
      "parameters": {
        "jsCode": "// R\u00e9cup\u00e9rer tous les items des n\u0153uds All Links\nconst allItems = $input.all();\n\nconsole.log('Total items re\u00e7us:', allItems.length);\n\n// Variable pour compter le nombre total d'URLs BRUTES (avec doublons)\nlet totalRawUrls = 0; \n\n// Map pour compter les occurrences : url -> {count, title}\nconst urlCounts = new Map();\n\n// Fonction utilitaire pour ajouter une URL au compteur\nconst countUrl = (urlItem) => {\n    if (urlItem && urlItem.url) {\n        // Incr\u00e9menter le compteur BRUT avant toute d\u00e9duplication\n        totalRawUrls++; \n        \n        // Normaliser l'URL (par exemple, supprimer le slash final pour \u00e9viter les doublons si non significatif)\n        let url = urlItem.url.trim().replace(/\\/$/, \"\"); \n        \n        const title = urlItem.title || '';\n\n        // Compter les occurrences UNIQUES\n        if (urlCounts.has(url)) {\n            urlCounts.get(url).count++;\n        } else {\n            urlCounts.set(url, { count: 1, title: title });\n        }\n    }\n};\n\n// Parcourir tous les items\nfor (const item of allItems) {\n    const data = item.json;\n    // ... (Logique de parcours des structures d'entr\u00e9e inchang\u00e9e)\n    if (data.url && typeof data.url === 'string') {\n        countUrl(data);\n    } else if (Array.isArray(data)) {\n        data.forEach(countUrl);\n    } else if (typeof data === 'object' && data !== null) {\n        for (const [key, value] of Object.entries(data)) {\n            if (Array.isArray(value)) {\n                value.forEach(countUrl);\n            } else if (value && typeof value === 'object' && value.url) {\n                 countUrl(value);\n            }\n        }\n    }\n}\n\nconsole.log('Total URLs uniques trouv\u00e9es (d\u00e9doublonn\u00e9es):', urlCounts.size);\nconsole.log('Total URLs brutes trouv\u00e9es:', totalRawUrls); // Nouveau log\n\n// --- LOGIQUE POUR CALCULER LES STATISTIQUES COMPL\u00c8TES ---\nconst allSortedUrls = Array.from(urlCounts.entries())\n    .map(([url, data]) => ({\n        url: url,\n        title: data.title,\n        occurrences: data.count\n    }))\n    .sort((a, b) => b.occurrences - a.occurrences);\n\nconst top80Urls = allSortedUrls.slice(0, 80); \n\nconst totalUrlsAfterTop80 = allSortedUrls.length - top80Urls.length;\nconst occurrenceAfterTop80 = allSortedUrls[80]?.occurrences || 0;\n\n// Retourner la liste des 80 URLs les plus fr\u00e9quentes PLUS les stats\nreturn [{\n    json: {\n        totalUrlsSelected: top80Urls.length,\n        totalUniqueUrls: allSortedUrls.length,\n        totalRawUrls: totalRawUrls, // AJOUT DE LA VALEUR BRUTE\n        urls: top80Urls\n    }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3984,
        64
      ],
      "id": "86180a9b-ddd0-4001-a38c-e6967027fe9c",
      "name": "Code3"
    },
    {
      "parameters": {},
      "type": "n8n-nodes-base.merge",
      "typeVersion": 3.2,
      "position": [
        4400,
        -512
      ],
      "id": "5822994f-59b3-4cda-8db2-45b92a21dd05",
      "name": "Merge1"
    },
    {
      "parameters": {
        "jsCode": "const items = [];\nfor (let i = 0; i < 70; i++) {\n  items.push({\n    json: {\n      iteration: i + 1,\n      topic: $input.first().json.topic,\n      email: $input.first().json.email,\n      // copie les autres donn\u00e9es dont tu as besoin\n    }\n  });\n}\nreturn items;"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1264,
        16
      ],
      "id": "e5faf7f4-e3e8-472c-98d4-314758e37703",
      "name": "iteration_number_DeepSeek"
    },
    {
      "parameters": {
        "jsCode": "const items = [];\nfor (let i = 0; i < 60; i++) {\n  items.push({\n    json: {\n      iteration: i + 1,\n      topic: $input.first().json.topic,\n      email: $input.first().json.email,\n      // copie les autres donn\u00e9es dont tu as besoin\n    }\n  });\n}\nreturn items;"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1280,
        512
      ],
      "id": "62caab96-bfdb-4c25-abd4-a2a91702ffd9",
      "name": "iteration_number_For_Gemini-3"
    },
    {
      "parameters": {
        "jsCode": "// n8n Code node (Node.js)\n\n// Helper: normalize URL to avoid duplicates like\n// \"https://site.com\" vs \"https://site.com/\"\nfunction normalizeUrl(url) {\n  if (typeof url !== 'string') return '';\n  return url.trim().replace(/\\/+$/, '').toLowerCase(); // remove trailing slash(es) + lowercase\n}\n\n// R\u00e9cup\u00e8re tous les items en entr\u00e9e (chacun contient totalUrlsSelected, urls, etc.)\nconst inputItems = $input.all();\n\nconst seen = new Set();\nconst output = [];\n\nfor (const item of inputItems) {\n  const data = item.json;\n\n  // On s'attend \u00e0 un tableau \"urls\" dans chaque item\n  const urlsArray = Array.isArray(data.urls) ? data.urls : [];\n\n  for (const entry of urlsArray) {\n    const url = entry.url;\n    const title = entry.title;\n\n    if (!url || !title) continue;\n\n    const key = normalizeUrl(url);\n    if (!key) continue;\n\n    // Si d\u00e9j\u00e0 vu, on saute\n    if (seen.has(key)) continue;\n    seen.add(key);\n\n    // On renvoie un item par URL unique, avec seulement url + title\n    output.push({\n      json: {\n        url,\n        title,\n        // Si tu veux garder occurrences :\n        // occurrences: entry.occurrences,\n      },\n    });\n  }\n}\n\nreturn output;\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        4608,
        -512
      ],
      "id": "41ca9793-b2ee-4085-bae8-f3a1f7d21a20",
      "name": "Code4"
    },
    {
      "parameters": {
        "options": {}
      },
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 3,
      "position": [
        5120,
        -528
      ],
      "id": "39e4fe05-0f9b-4b54-9ee6-f2bc130e62f5",
      "name": "Loop Over Links"
    },
    {
      "parameters": {
        "operation": "select",
        "table": {
          "__rl": true,
          "value": "Ranking",
          "mode": "list",
          "cachedResultName": "Ranking"
        },
        "returnAll": true,
        "where": {
          "values": [
            {
              "column": "Link",
              "value": "={{ $json.url }}"
            },
            {
              "column": "topic_id",
              "value": "={{$('Aggregating Input Variables').first().json.idtopic[0]}}"
            }
          ]
        },
        "options": {
          "detailedOutput": true
        }
      },
      "type": "n8n-nodes-base.mySql",
      "typeVersion": 2.4,
      "position": [
        5344,
        -480
      ],
      "id": "3f15d29c-ab43-4dfd-a85c-7d770c2bea0d",
      "name": "Check Existence of the Link",
      "credentials": {
        "mySql": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "64d548f1-5961-464b-bd52-f0550e0b7858",
              "leftValue": "={{ $json.data }}",
              "rightValue": "",
              "operator": {
                "type": "array",
                "operation": "empty",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        5520,
        -480
      ],
      "id": "4c64f0c4-8d01-4beb-8079-6976a907b80a",
      "name": "If Link Does Not Exist, Continue"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "b2998da2-bff5-439e-b114-a655f8245886",
              "leftValue": "={{ $json.stdout }}",
              "rightValue": "{\"message\": \"Internal server error\"}",
              "operator": {
                "type": "string",
                "operation": "contains"
              }
            },
            {
              "id": "001d554a-4042-4864-a6b2-ddf49c7ab1a9",
              "leftValue": "={{ $json.stdout }}",
              "rightValue": "{\"error\": \"URL parameter is missing\"}",
              "operator": {
                "type": "string",
                "operation": "contains"
              }
            },
            {
              "id": "574b6888-1d07-40c9-985e-66bc63dc4273",
              "leftValue": "={{ $json.stdout }}",
              "rightValue": "\"error\": \"403 Client Error: Forbidden for url",
              "operator": {
                "type": "string",
                "operation": "contains"
              }
            }
          ],
          "combinator": "or"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        6128,
        -784
      ],
      "id": "c26d9ae0-a18a-45d8-bb7a-a37d7c6f2149",
      "name": "If Scraping is Done, Continue"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "0a11d788-a57b-41ce-a59f-78138f7ea090",
              "leftValue": "={{ $json.stdout }}",
              "rightValue": "\"error\": \"403 Client Error",
              "operator": {
                "type": "string",
                "operation": "contains"
              }
            },
            {
              "id": "9de670e0-2447-4317-aca6-661786d727f6",
              "leftValue": "={{ $json.stdout }}",
              "rightValue": "\"error\": \"404 Client Error",
              "operator": {
                "type": "string",
                "operation": "contains"
              }
            },
            {
              "id": "6ae46cca-97f7-4953-8f19-da4dad09b800",
              "leftValue": "={{ $json.stdout }}",
              "rightValue": "\"error\"",
              "operator": {
                "type": "string",
                "operation": "contains"
              }
            }
          ],
          "combinator": "or"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        5936,
        -336
      ],
      "id": "4a62afae-cc35-4998-bc0e-66e86206a556",
      "name": "If Scraping Does Not Contain Errors, Continue"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "223aa338-f7c9-49fc-9516-d947439c8720",
              "leftValue": "={{ $json.output.mentioned_suppliers }}",
              "rightValue": "=suppliers_mentioned",
              "operator": {
                "type": "string",
                "operation": "empty",
                "singleValue": true
              }
            },
            {
              "id": "870f2e3f-fb4c-4c1e-a0cb-cc35ce811ad3",
              "leftValue": "={{ $json.output.mentioned_suppliers }}",
              "rightValue": "No Mentioned Suppliers",
              "operator": {
                "type": "string",
                "operation": "contains"
              }
            }
          ],
          "combinator": "or"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        7536,
        -560
      ],
      "id": "03ebfed6-ba84-4c54-9e43-d1697e9abded",
      "name": "If Suppliers Exist, Continue"
    },
    {
      "parameters": {
        "jsCode": "const inputText = $('If Suppliers Exist, Continue').first().json.output;\nconsole.log(\"debug alaeddine : \", inputText);\n\n// No need to parse - inputText is already an object\nif (!inputText.mentioned_suppliers) {\n  return [];\n}\n\n// Split by comma, trim whitespace, and remove empty strings\nconst suppliersArray = inputText.mentioned_suppliers\n  .split(',')\n  .map(supplier => supplier.trim())\n  .filter(supplier => supplier !== \"\");\n\n// Format each supplier into an output item for n8n\nreturn suppliersArray.map(supplier => ({ json: { supplier } }));"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        7824,
        -720
      ],
      "id": "2ea85e81-c64a-4c43-b309-b60410838899",
      "name": "Preparing Suppliers as Items",
      "alwaysOutputData": true,
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "workflowId": {
          "__rl": true,
          "value": "wqLtZRrarAx4CRJo",
          "mode": "list",
          "cachedResultName": "Sourcing_Agent_LinkedIn_validator_production"
        },
        "workflowInputs": {
          "mappingMode": "defineBelow",
          "value": {
            "Company Name": "={{ $json.supplier }}",
            "Topic": "={{ $('Code').first().json.topic }}",
            "Topic Definition": "={{ $('Code').first().json.definition }}",
            "Topic ID": "={{ parseInt($('Aggregating Input Variables').first().json.idtopic) }}"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "Company Name",
              "displayName": "Company Name",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "canBeUsedToMatch": true
            },
            {
              "id": "Topic",
              "displayName": "Topic",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "canBeUsedToMatch": true
            },
            {
              "id": "Topic ID",
              "displayName": "Topic ID",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "canBeUsedToMatch": true,
              "type": "number"
            },
            {
              "id": "Topic Definition",
              "displayName": "Topic Definition",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "canBeUsedToMatch": true
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": true
        },
        "mode": "each",
        "options": {
          "waitForSubWorkflow": true
        }
      },
      "type": "n8n-nodes-base.executeWorkflow",
      "typeVersion": 1.2,
      "position": [
        7984,
        -608
      ],
      "id": "17d22499-949e-4edb-a387-fbafe9969724",
      "name": "Supplier Validator Sub Workflow",
      "retryOnFail": true,
      "maxTries": 2,
      "waitBetweenTries": 5000,
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "fieldsToAggregate": {
          "fieldToAggregate": [
            {
              "fieldToAggregate": "item",
              "renameField": true,
              "outputFieldName": "Identified Suppliers"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.aggregate",
      "typeVersion": 1,
      "position": [
        8176,
        -560
      ],
      "id": "d9679505-9417-48fe-a6f7-e9d25d37670e",
      "name": "Aggregating Validated Suppliers"
    },
    {
      "parameters": {
        "jsCode": "return items.map(item => {\n  // 1. R\u00e9cup\u00e9rer la liste des fournisseurs identifi\u00e9s et compter leur nombre\n  const identifiedSuppliers = item.json[\"Identified Suppliers\"] || [];\n  const numberOfSuppliers = identifiedSuppliers.length;\n\n  // 2. R\u00e9cup\u00e9rer le lien de l\u2019\u00e9l\u00e9ment courant\n  const link = $('Loop Over Links').first().json.url || \"\";\n\n  // Fonction de normalisation : on retire les espaces et tous les caract\u00e8res non alphanum\u00e9riques\n  const normalize = str =>\n    str.toLowerCase().replace(/\\s+/g, '').replace(/[^a-z0-9]/g, '');\n\n  // Normaliser le lien\n  const normalizedLink = normalize(link);\n\n  // 3. V\u00e9rifier si le lien contient une correspondance partielle du nom du fournisseur\n  const isInHouse = identifiedSuppliers.some(supplier => {\n    const normalizedSupplier = normalize(supplier);\n    return normalizedLink.includes(normalizedSupplier);\n  });\n\n  // 4. D\u00e9terminer si c'est un fournisseur tiers\n  const isThirdParty = !isInHouse;\n\n  // 5. Extraire le domaine original du lien avec une expression r\u00e9guli\u00e8re\n  const match = link.match(/^https?:\\/\\/([^/]+)/);\n  const source = match ? match[1] : \"Invalid URL\";\n\n  // 6. Retourner le r\u00e9sultat final\n  return {\n    json: {\n      numberOfSuppliers,\n      thirdParty: isThirdParty ? \"Yes\" : \"No\",\n      source\n    }\n  };\n});\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        8400,
        -560
      ],
      "id": "49fab42c-273b-4b7a-b3be-136308b1c7b5",
      "name": "Preparing Variables for Ranking"
    },
    {
      "parameters": {
        "jsCode": "// Retrieve the necessary variables from the first input item\nconst numberOfSuppliers = $input.first().json.numberOfSuppliers;\nconst thirdParty = $input.first().json.thirdParty; // Expected to be \"Yes\" or \"No\"\n\nlet rank;\n\n// Apply the ranking criteria:\nif (numberOfSuppliers < 1) {\n  rank = 3;\n} else if (numberOfSuppliers === 1) {\n  rank = 4;\n} else if (numberOfSuppliers > 1 && thirdParty === \"No\") {\n  rank = 6;\n} else if (numberOfSuppliers > 1 && thirdParty === \"Yes\") {\n  rank = 7;\n}\n\n// Return the result as an output item.\nreturn [\n  {\n    json: {\n      rank: rank,\n      // you can also forward other properties if needed\n    

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

LLMS_Query_Links_stabilisation. Uses mySql, lmChatOpenAi, executeCommand, chainLlm. Webhook trigger; 95 nodes.

Source: https://github.com/alaeddine-hash/docker-n8n-exports/blob/main/n8n-workflows/TmIV9kJgU673IsL3.json — 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

LLMS_Query_Links_stabilisation. Uses mySql, lmChatOpenAi, executeCommand, chainLlm. Webhook trigger; 95 nodes.

MySQL, OpenAI Chat, Execute Command +6
AI & RAG

sourcing_agent_production. Uses mySql, openAi, executeCommand, emailSend. Webhook trigger; 78 nodes.

MySQL, OpenAI, Execute Command +5
AI & RAG

sourcing_agent_production. Uses mySql, openAi, executeCommand, emailSend. Webhook trigger; 78 nodes.

MySQL, OpenAI, Execute Command +5
AI & RAG

Main_Sourcing_Agent. Uses mySql, openAi, executeCommand, chainLlm. Webhook trigger; 70 nodes.

MySQL, OpenAI, Execute Command +6
AI & RAG

Main_Sourcing_Agent. Uses mySql, openAi, executeCommand, chainLlm. Webhook trigger; 70 nodes.

MySQL, OpenAI, Execute Command +6