Comparaison hebdomadaire BrightData
Ceci est unAI, Marketingworkflow d'automatisation du domainecontenant 35 nœuds.Utilise principalement des nœuds comme If, Set, Code, Gmail, Markdown, combinant la technologie d'intelligence artificielle pour une automatisation intelligente. Surveillance automatisée des modifications de sites web avec Bright Data, GPT-4.1 et Google Workspace
- •Compte Google et informations d'identification Gmail API
- •Informations d'identification Google Drive API
- •Informations d'identification Google Sheets API
- •Clé API OpenAI
Nœuds utilisés (35)
Catégorie
{
"id": "pNaD8QIgVGDqbCoU",
"meta": {
"instanceId": "3af183a3db355380be4f6d2f3dfb18bdaa750e90f99a48f91bd71080ee6bcbe8",
"templateCredsSetupCompleted": true
},
"name": "BrightData Weekly Comparison",
"tags": [],
"nodes": [
{
"id": "a2966ed3-e998-47ca-83b3-b7dc4832bc8f",
"name": "Analyseur de Sortie Structurée",
"type": "@n8n/n8n-nodes-langchain.outputParserStructured",
"position": [
700,
140
],
"parameters": {
"jsonSchemaExample": "{\n \"filename\": \"domain-path-DD-MM-YYYY.md\",\n \"metadata\": {\n \"pageTitle\": \"The primary title of the webpage\",\n \"metaDescription\": \"The meta description content\"\n },\n \"headings\": {\n \"h1\": [\"List of all H1 headings\"],\n \"h2\": [\"List of all H2 headings\"],\n \"h3\": [\"List of all H3 headings\"]\n },\n \"pricing\": [\n {\n \"planName\": \"Name of the pricing plan/tier\",\n \"currency\": \"USD/EUR/GBP/etc.\",\n \"interval\": \"monthly/yearly/one-time\",\n \"price\": \"Numerical price value\",\n \"features\": [\"List of features or benefits included in this plan\"]\n }\n ],\n \"navigation\": {\n \"mainMenu\": [\"Primary navigation items\"],\n \"subMenu\": [\"Secondary navigation items if present\"]\n },\n \"callToAction\": [\"All CTA elements and their text\"],\n \"contactInfo\": {\n \"phone\": [\"Phone numbers found\"],\n \"email\": [\"Email addresses found\"],\n \"address\": [\"Physical addresses found\"],\n \"formPresent\": true\n },\n \"banners\": [\"Content from promotional banners\"],\n \"faq\": [\n {\n \"question\": \"FAQ question\",\n \"answer\": \"FAQ answer\"\n }\n ]\n}"
},
"typeVersion": 1.2
},
{
"id": "6d4b38bd-fb58-4675-9b83-ade043914d65",
"name": "Définir les variables du workflow",
"type": "n8n-nodes-base.set",
"position": [
-260,
-120
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "96347cce-576a-413c-9c9b-b2257eced54d",
"name": "DriveFolderID",
"type": "string",
"value": ""
},
{
"id": "b2f5e75f-575d-40ed-89a8-1c2e537d3220",
"name": "ComparisonSpreadsheetID",
"type": "string",
"value": ""
},
{
"id": "f64718d6-a5f2-491e-abec-b26a6e6125e8",
"name": "ComparisonSpreadsheetSheetName",
"type": "string",
"value": "Sheet1"
},
{
"id": "1a71595c-58c3-4eda-9b7b-5c306123db86",
"name": "Email",
"type": "string",
"value": ""
},
{
"id": "ed5fe6bd-b43b-45b1-b1e8-b20beb329e4d",
"name": "IsTest",
"type": "boolean",
"value": false
}
]
},
"includeOtherFields": true
},
"typeVersion": 3.4
},
{
"id": "6ddda34f-556e-49a5-a103-7641a3f0598d",
"name": "Fusionner les variables avec les données de la feuille Google",
"type": "n8n-nodes-base.set",
"position": [
180,
-120
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "e129ca16-8675-485e-9f21-e5d22b76c6e6",
"name": "ComparisonSpreadsheetFileName",
"type": "string",
"value": "={{ $('Set workflow variables').item.json.ComparisonSpreadsheetFileName }}"
},
{
"id": "5fdee148-82eb-4542-9fe7-1ba37ec09571",
"name": "ComparisonSpreadsheetSheetName",
"type": "string",
"value": "={{ $('Set workflow variables').item.json.ComparisonSpreadsheetSheetName }}"
},
{
"id": "7cce5924-a67a-45a2-8792-cd9ad28834b8",
"name": "Email",
"type": "string",
"value": "={{ $('Set workflow variables').item.json.Email }}"
},
{
"id": "e445b570-384f-4558-99d3-12802b0fa900",
"name": "DriveFolderID",
"type": "string",
"value": "={{ $('Set workflow variables').item.json.DriveFolderID }}"
},
{
"id": "d487666e-4dd3-4d20-8769-d0daf3bab268",
"name": "IsTest",
"type": "boolean",
"value": "={{ $('Set workflow variables').item.json.IsTest }}"
}
]
},
"includeOtherFields": true
},
"typeVersion": 3.4
},
{
"id": "45eefb28-dce0-4fc3-81a1-865762c73226",
"name": "Lire les feuilles de calcul de comparaison",
"type": "n8n-nodes-base.googleSheets",
"position": [
-40,
-120
],
"parameters": {
"options": {},
"sheetName": {
"__rl": true,
"mode": "name",
"value": "={{ $json.ComparisonSpreadsheetSheetName }}"
},
"documentId": {
"__rl": true,
"mode": "id",
"value": "={{ $json.ComparisonSpreadsheetID }}"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"id": "vnXMSwscCP06bp9u",
"name": "Google Sheets account"
}
},
"typeVersion": 4.5
},
{
"id": "c04cec36-1664-48d7-a9ae-a5665bc2c188",
"name": "Boucler sur chaque URL de comparaison",
"type": "n8n-nodes-base.splitInBatches",
"position": [
400,
-115
],
"parameters": {
"options": {}
},
"typeVersion": 3
},
{
"id": "c50e3ec4-c3bf-4417-94ad-5b57ad6424da",
"name": "Agent de scraping web et extraction de données",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
652,
-365
],
"parameters": {
"text": "=Please scrape the following url {{ $json.URL }}\n",
"options": {
"systemMessage": "=## Role\nYou are an expert Web Data Extraction Specialist with a specialization in content analysis, information architecture, and structured data organization.\n\n## Task\nYour primary task is to extract, organize, and present key website components from any URL provided by the user.\nTo achieve this, you will need to perform the following:\n- Scrape the target webpage using the scrape_as_markdown tool to obtain its content\n- Generate a clean filename based on the URL and current date for storing the content as JSON\n- Systematically extract and categorize the following elements from the markdown content\n- Format the extracted data into a structured JSON object containing the processed elements\n\n## Tools\nYou can use the following tools:\n1. **scrape_as_markdown**: Use this tool to extract the full content of a webpage in Markdown format. This tool can bypass bot detection and CAPTCHA systems to ensure reliable extraction.\n\n## Input\nYou will receive input as a URL to the webpage that requires data extraction.\n\n## Output\nYour output must be a single, valid JSON object containing all extracted elements and a clean filename. Do not include any explanatory text before or after the JSON.\n\n{\n \"filename\": \"domain-path-DD-MM-YYYY.json\",\n \"metadata\": {\n \"pageTitle\": \"The primary title of the webpage\",\n \"metaDescription\": \"The meta description content\"\n },\n \"headings\": {\n \"h1\": [\"List of all H1 headings\"],\n \"h2\": [\"List of all H2 headings\"],\n \"h3\": [\"List of all H3 headings\"]\n },\n \"pricing\": [\n {\n \"planName\": \"Name of the pricing plan/tier\",\n \"currency\": \"USD/EUR/GBP/etc.\",\n \"interval\": \"monthly/yearly/one-time\",\n \"price\": \"Numerical price value\",\n \"features\": [\"List of features or benefits included in this plan\"]\n }\n ],\n \"navigation\": {\n \"mainMenu\": [\"Primary navigation items\"],\n \"subMenu\": [\"Secondary navigation items if present\"]\n },\n \"callToAction\": [\"All CTA elements and their text\"],\n \"contactInfo\": {\n \"phone\": [\"Phone numbers found\"],\n \"email\": [\"Email addresses found\"],\n \"address\": [\"Physical addresses found\"],\n \"formPresent\": true/false\n },\n \"banners\": [\"Content from promotional banners\"],\n \"faq\": [\n {\n \"question\": \"FAQ question\",\n \"answer\": \"FAQ answer\"\n }\n ]\n}\n\nFor any elements not found on the webpage, include an empty array [] or appropriate null value. Ensure the JSON is properly formatted and valid. If the extraction process encounters any errors, include an additional \"errors\" key with relevant details.\n\nWhen extracting pricing information, analyze the webpage carefully to identify all pricing plans. For each plan, determine:\n- The name of the plan (e.g., \"Basic\", \"Pro\", \"Enterprise\")\n- The currency symbol or code used\n- Whether the pricing is monthly, yearly, or one-time\n- The numerical price value\n- All features or details associated with that pricing tier\n\nThe filename should be generated using the following format:\n- Extract the domain name from the URL (without www.)\n- Extract the path (without any query parameters or fragments)\n- Replace slashes with hyphens\n- Add the current date in DD-MM-YYYY format\n- Use .json as the file extension\n- For example, if the URL is https://asana.com/pricing and today is May 20, 2025, the filename would be \"asana-pricing-20-05-2025.json\"\n- Today's date is {{ $now }}\n\nEven if some pricing information is ambiguous or incomplete, make reasonable inferences and include all relevant details in the structured format."
},
"promptType": "define",
"hasOutputParser": true
},
"typeVersion": 1.9
},
{
"id": "c1f69dc5-6612-4575-aaf4-1d6a7a70c423",
"name": "Téléverser le fichier JSON de la semaine actuelle",
"type": "n8n-nodes-base.googleDrive",
"position": [
1280,
-365
],
"parameters": {
"name": "={{ $('Web scraping and data extraction agent').item.json.output.filename }}",
"driveId": {
"__rl": true,
"mode": "list",
"value": "My Drive",
"cachedResultUrl": "https://drive.google.com/drive/my-drive",
"cachedResultName": "My Drive"
},
"options": {},
"folderId": {
"__rl": true,
"mode": "id",
"value": "={{ $('Loop over each comparison URL').item.json.DriveFolderID }}"
}
},
"credentials": {
"googleDriveOAuth2Api": {
"id": "ENsCK6J7JBSny3Pv",
"name": "Google Drive account"
}
},
"typeVersion": 3
},
{
"id": "e4a1fd2c-e1a5-4065-a9c8-76d1c854af95",
"name": "Mettre à jour la feuille de comparaison avec les données de la semaine actuelle",
"type": "n8n-nodes-base.googleSheets",
"position": [
1500,
-365
],
"parameters": {
"columns": {
"value": {
"URL": "={{ $('Loop over each comparison URL').item.json.URL }}",
"Previous Week ID": "={{ $('Loop over each comparison URL').item.json[\"Current Week File ID\"] }}",
"Current Week File ID": "={{ $json.id }}",
"Current Week File Link": "={{ $json.webViewLink }}",
"Previous Week File Link": "={{ $('Loop over each comparison URL').item.json[\"Current Week File Link\"] }}"
},
"schema": [
{
"id": "URL",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "URL",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Previous Week ID",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Previous Week ID",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Previous Week File Link",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Previous Week File Link",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Current Week File ID",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Current Week File ID",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Current Week File Link",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Current Week File Link",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Comparison File",
"type": "string",
"display": true,
"removed": true,
"required": false,
"displayName": "Comparison File",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "row_number",
"type": "string",
"display": true,
"removed": true,
"readOnly": true,
"required": false,
"displayName": "row_number",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [
"URL"
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "update",
"sheetName": {
"__rl": true,
"mode": "list",
"value": "gid=0",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1e4oheZjmxb3P7OXGY0uLWZKz2ENcp7XlEpIloOtRHEk/edit#gid=0",
"cachedResultName": "Sheet1"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "1e4oheZjmxb3P7OXGY0uLWZKz2ENcp7XlEpIloOtRHEk",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1e4oheZjmxb3P7OXGY0uLWZKz2ENcp7XlEpIloOtRHEk/edit?usp=drivesdk",
"cachedResultName": "BrightData Scraping Comparison"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"id": "vnXMSwscCP06bp9u",
"name": "Google Sheets account"
}
},
"typeVersion": 4.5
},
{
"id": "32934604-9e72-4f6a-aa55-fc0abff1d85c",
"name": "Vérifier la présence du fichier de la semaine précédente",
"type": "n8n-nodes-base.if",
"position": [
1720,
-365
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "d46019c6-ec3b-4e8b-80b2-44ffb2a82e51",
"operator": {
"type": "string",
"operation": "notEmpty",
"singleValue": true
},
"leftValue": "={{ $json[\"Previous Week ID\"] }}",
"rightValue": ""
}
]
}
},
"typeVersion": 2.2
},
{
"id": "301dfc46-f1d4-4b1a-892b-25ecb4495b77",
"name": "Télécharger le fichier de la semaine précédente",
"type": "n8n-nodes-base.googleDrive",
"position": [
1940,
-365
],
"parameters": {
"fileId": {
"__rl": true,
"mode": "id",
"value": "={{ $json[\"Previous Week ID\"] }}"
},
"options": {},
"operation": "download"
},
"credentials": {
"googleDriveOAuth2Api": {
"id": "ENsCK6J7JBSny3Pv",
"name": "Google Drive account"
}
},
"typeVersion": 3
},
{
"id": "036d00c9-7414-4883-ac56-ef9368195b85",
"name": "Convertir le fichier de la semaine précédente en JSON",
"type": "n8n-nodes-base.extractFromFile",
"position": [
2160,
-365
],
"parameters": {
"options": {},
"operation": "fromJson"
},
"typeVersion": 1
},
{
"id": "67b8ee0a-2ab0-43cd-b24f-0c8ba6b04cd1",
"name": "Définir la semaine précédente et la semaine actuelle",
"type": "n8n-nodes-base.set",
"position": [
2380,
-365
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "af75985e-4f86-4493-9fbd-0d0c7b383001",
"name": "previous",
"type": "object",
"value": "={{ $json.data[0].output }}"
},
{
"id": "718aeb81-59e9-49f9-aa5b-dc3c194d2efa",
"name": "current",
"type": "object",
"value": "={{ $('Web scraping and data extraction agent').item.json.output }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "40b828f7-08f2-41ea-a6f7-595c5e4ae3e0",
"name": "Vérifier le mode test",
"type": "n8n-nodes-base.if",
"position": [
2600,
-365
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "650a5f5e-3041-4d2f-a891-1d369b09c17a",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"leftValue": "={{ $('Loop over each comparison URL').item.json.IsTest }}",
"rightValue": ""
}
]
}
},
"typeVersion": 2.2
},
{
"id": "af562b9f-3cfc-4715-ae47-779a0554e7d2",
"name": "Simuler les modifications de la semaine précédente",
"type": "n8n-nodes-base.code",
"position": [
2820,
-440
],
"parameters": {
"jsCode": "// Code node in N8n\nconst inputData = $input.first().json;\n\n// Clone the previous data before modifying it\nconst previous = {...inputData.previous};\n\n// Change 1: Increase all numeric prices by $5 in the pricing array\nif (previous?.pricing && Array.isArray(previous.pricing)) {\n previous.pricing.forEach(plan => {\n // Check if price is a numeric string\n if (plan.price && !isNaN(parseFloat(plan.price))) {\n // Add $5 to the price\n plan.price = (parseFloat(plan.price) + 5).toString();\n }\n });\n}\n\n// Change 2: Modify a feature in the first paid plan (if exists)\nif (previous?.pricing && previous.pricing.length > 1) {\n const firstPaidPlan = previous.pricing.find(plan => \n plan.price && plan.price !== \"0\" && !isNaN(parseFloat(plan.price)));\n \n if (firstPaidPlan && Array.isArray(firstPaidPlan.features) && firstPaidPlan.features.length > 1) {\n // Add \"Premium\" prefix to the second feature (if it exists)\n if (firstPaidPlan.features[1]) {\n firstPaidPlan.features[1] = \"Premium \" + firstPaidPlan.features[1];\n }\n }\n}\n\n// Change 3: Update the first FAQ answer (if exists)\nif (previous?.faq && Array.isArray(previous.faq) && previous.faq.length > 0) {\n if (previous.faq[0] && previous.faq[0].answer) {\n // Add a sentence to the end of the answer\n previous.faq[0].answer += \" For more information, contact our sales team.\";\n }\n}\n\n// Return the data with current and modified previous\nreturn [{\n json: {\n current: inputData.current,\n previous: previous\n }\n}];"
},
"typeVersion": 2
},
{
"id": "dd764b6d-d412-44e6-8816-e7b66bbb44c6",
"name": "Détecter les changements entre les semaines",
"type": "n8n-nodes-base.code",
"position": [
3040,
-365
],
"parameters": {
"jsCode": "// N8n Code Node to compare previous and current data\n// Input: $input.first().json contains previous and current data objects\n\nfunction detectChanges() {\n const previous = $input.first().json.previous;\n const current = $input.first().json.current;\n \n // Initialize changes object with better structure for Markdown generation\n const changes = {\n detected: false,\n sections: {}, // Organized by section\n summary: {\n totalChanges: 0,\n timestamp: new Date().toISOString()\n }\n };\n \n // Helper function to add a change to the changes object\n function addChange(section, path, changeType, changeData) {\n // Initialize section if it doesn't exist\n if (!changes.sections[section]) {\n changes.sections[section] = {\n name: formatSectionName(section),\n changes: []\n };\n }\n \n // Format display path\n const displayPath = formatPath(path);\n \n // Add change to the section\n changes.sections[section].changes.push({\n displayPath,\n type: changeType,\n ...changeData\n });\n \n // Update global detected flag and counter\n changes.detected = true;\n changes.summary.totalChanges++;\n }\n \n // Helper function to format section name for display\n function formatSectionName(name) {\n // Convert camelCase to Title Case with spaces\n return name.replace(/([A-Z])/g, ' $1')\n .replace(/^./, str => str.toUpperCase())\n .trim();\n }\n \n // Helper function to format path for display\n function formatPath(path) {\n // Format the path elements to be more readable\n return path.replace(/\\[([^\\]]+)\\]/g, ' ($1)');\n }\n \n // Helper function to compare primitive values\n function compareValues(prevVal, currVal, path) {\n if (prevVal !== currVal) {\n const section = path.split('.')[0];\n addChange(section, path, 'changed', {\n old: prevVal,\n new: currVal\n });\n return true;\n }\n return false;\n }\n \n // Helper function to compare arrays\n function compareArrays(prevArr, currArr, path, matchKey = null) {\n let changed = false;\n const section = path.split('.')[0];\n const additions = [];\n const deletions = [];\n \n // Check for deletions\n for (let i = 0; i < prevArr.length; i++) {\n const prevItem = prevArr[i];\n \n if (matchKey && typeof prevItem === 'object' && prevItem !== null) {\n // For arrays of objects with a specified match key (like planName or question)\n const matchFound = currArr.some(currItem => currItem[matchKey] === prevItem[matchKey]);\n if (!matchFound) {\n deletions.push(prevItem);\n changed = true;\n }\n } else {\n // For arrays of primitives\n if (!currArr.includes(prevItem)) {\n deletions.push(prevItem);\n changed = true;\n }\n }\n }\n \n // Check for additions\n for (let i = 0; i < currArr.length; i++) {\n const currItem = currArr[i];\n \n if (matchKey && typeof currItem === 'object' && currItem !== null) {\n // For arrays of objects with a specified match key\n const matchFound = prevArr.some(prevItem => prevItem[matchKey] === currItem[matchKey]);\n if (!matchFound) {\n additions.push(currItem);\n changed = true;\n }\n } else {\n // For arrays of primitives\n if (!prevArr.includes(currItem)) {\n additions.push(currItem);\n changed = true;\n }\n }\n }\n \n // Check for changes in matching objects\n if (matchKey) {\n for (let i = 0; i < currArr.length; i++) {\n const currItem = currArr[i];\n if (typeof currItem === 'object' && currItem !== null) {\n const matchingPrevItem = prevArr.find(prevItem => prevItem[matchKey] === currItem[matchKey]);\n if (matchingPrevItem) {\n // Compare the matching objects recursively\n const itemPath = `${path}[${matchKey}=${currItem[matchKey]}]`;\n compareObjects(matchingPrevItem, currItem, itemPath);\n }\n }\n }\n }\n \n if (changed) {\n addChange(section, path, 'array_changed', {\n additions: additions.length > 0 ? additions : null,\n deletions: deletions.length > 0 ? deletions : null\n });\n }\n \n return changed;\n }\n \n // Helper function to compare objects\n function compareObjects(prevObj, currObj, path = '') {\n if (!prevObj || !currObj) return false;\n \n const allKeys = new Set([...Object.keys(prevObj), ...Object.keys(currObj)]);\n let changed = false;\n \n for (const key of allKeys) {\n // Skip rawMarkdown as requested\n if (key === 'rawMarkdown') continue;\n \n const keyPath = path ? `${path}.${key}` : key;\n const section = keyPath.split('.')[0];\n const prevVal = prevObj[key];\n const currVal = currObj[key];\n \n // Handle missing keys\n if (!(key in prevObj)) {\n addChange(section, keyPath, 'added', {\n value: currVal\n });\n changed = true;\n continue;\n }\n \n if (!(key in currObj)) {\n addChange(section, keyPath, 'removed', {\n value: prevVal\n });\n changed = true;\n continue;\n }\n \n // Compare based on type\n if (Array.isArray(prevVal) && Array.isArray(currVal)) {\n // Special handling for specific array types\n if (key === 'pricing') {\n compareArrays(prevVal, currVal, keyPath, 'planName');\n } else if (key === 'features') {\n compareArrays(prevVal, currVal, keyPath);\n } else if (key === 'faq') {\n compareArrays(prevVal, currVal, keyPath, 'question');\n } else {\n compareArrays(prevVal, currVal, keyPath);\n }\n } else if (\n typeof prevVal === 'object' && prevVal !== null &&\n typeof currVal === 'object' && currVal !== null\n ) {\n compareObjects(prevVal, currVal, keyPath);\n } else {\n compareValues(prevVal, currVal, keyPath);\n }\n }\n \n return changed;\n }\n \n // Start comparison - removed the .output reference\n compareObjects(previous, current);\n \n return [{ json: {previous, current, changes} }];\n}\n\n// Execute the function and return the results\nreturn detectChanges();"
},
"typeVersion": 2
},
{
"id": "e5a8b7c1-53ec-4a26-8791-30db494cde92",
"name": "Générer du Markdown à partir des changements détectés",
"type": "n8n-nodes-base.code",
"position": [
3260,
-365
],
"parameters": {
"jsCode": "// N8n Code Node to generate Markdown from structured changes\n// Input: $input.changes contains the comparison results from the improved comparison node\nfunction generateChangelogMarkdown() {\n // Changed to directly access the changes property from the input\n const changes = $input.first().json.changes;\n \n if (!changes || !changes.detected) {\n return [{ json: { markdown: \"# Changelog\\n\\nNo changes detected.\" }}];\n }\n \n // Start building the markdown\n // Getting the filename from the input for the title\n let fileNameWithoutExt = '';\n try {\n // Try to get the filename from the Loop Over Items node if available\n fileNameWithoutExt = $('Loop over each comparison URL').first().json[\"\"][0];\n } catch (e) {\n // Fallback to a generic title if the loop node isn't available\n fileNameWithoutExt = \"Website\";\n }\n \n let markdown = `# ${fileNameWithoutExt} Changes\\n\\n`;\n \n // Process each section in the changes object\n Object.values(changes.sections).forEach(section => {\n markdown += `## ${section.name}\\n\\n`;\n \n // Process each change in this section\n section.changes.forEach(change => {\n switch (change.type) {\n case 'changed':\n markdown += `- **${change.displayPath}** changed from \\`${formatValue(change.old)}\\` to \\`${formatValue(change.new)}\\`\\n`;\n break;\n \n case 'added':\n markdown += `- **${change.displayPath}** was added with value \\`${formatValue(change.value)}\\`\\n`;\n break;\n \n case 'removed':\n markdown += `- **${change.displayPath}** was removed (previously \\`${formatValue(change.value)}\\`)\\n`;\n break;\n \n case 'array_changed':\n markdown += `- **${change.displayPath}** has changes:\\n`;\n \n // Handle additions\n if (change.additions && change.additions.length > 0) {\n markdown += ` - **Added**:\\n`;\n change.additions.forEach(item => {\n markdown += ` - \\`${formatValue(item)}\\`\\n`;\n });\n }\n \n // Handle deletions\n if (change.deletions && change.deletions.length > 0) {\n markdown += ` - **Removed**:\\n`;\n change.deletions.forEach(item => {\n markdown += ` - \\`${formatValue(item)}\\`\\n`;\n });\n }\n break;\n }\n });\n \n markdown += '\\n';\n });\n \n // Add summary section\n markdown += `## Summary\\n\\n`;\n markdown += `Total changes detected: **${changes.summary.totalChanges}**\\n\\n`;\n markdown += `Generated on: **${new Date(changes.summary.timestamp).toLocaleDateString('en-US', {\n year: 'numeric',\n month: 'long',\n day: 'numeric'\n })}**\\n`;\n \n // Return in the format expected by n8n\n return [{ json: { markdown } }];\n}\n\n// Helper function to format values for markdown display\nfunction formatValue(value) {\n if (value === null || value === undefined) {\n return 'null';\n } else if (typeof value === 'object') {\n if (Array.isArray(value)) {\n // For arrays, simplify to show length\n if (value.length === 0) return '[]';\n if (value.length > 3) {\n return `[Array with ${value.length} items]`;\n }\n // For small arrays, show the items\n return JSON.stringify(value).substring(0, 60) + (JSON.stringify(value).length > 60 ? '...' : '');\n }\n \n // For objects with a name or key identifier, try to use that\n if (value.name) return value.name;\n if (value.title) return value.title;\n if (value.id) return value.id;\n if (value.planName) return value.planName;\n if (value.question) return value.question;\n \n // For other objects, shorten to reasonable length\n const objStr = JSON.stringify(value);\n return objStr.substring(0, 60) + (objStr.length > 60 ? '...' : '');\n } else if (typeof value === 'string') {\n // For strings, add quoting\n return value;\n } else {\n // For other primitives, convert to string\n return String(value);\n }\n}\n\n// Execute the function and return the markdown result\nreturn generateChangelogMarkdown();"
},
"typeVersion": 2
},
{
"id": "16c9f196-6acf-4d3a-946b-727c121b503a",
"name": "Convertir le Markdown en HTML",
"type": "n8n-nodes-base.markdown",
"position": [
3480,
-365
],
"parameters": {
"mode": "markdownToHtml",
"options": {},
"markdown": "={{ $json.markdown }}"
},
"typeVersion": 1
},
{
"id": "3df4c31f-db47-40a5-9140-d2de7b1fbf2e",
"name": "Créer un document de comparaison",
"type": "n8n-nodes-base.googleDocs",
"position": [
3700,
-365
],
"parameters": {
"title": "={{ $('Web scraping and data extraction agent').first().json.output.filename.replace(/\\.[^/.]+$/, '') + '-comparison.md' }}",
"folderId": "default"
},
"credentials": {
"googleDocsOAuth2Api": {
"id": "T3HEUOkeb37yLim7",
"name": "Google Docs account"
}
},
"typeVersion": 2
},
{
"id": "da24c1a3-fa3b-4b06-b5b9-78dd6d872756",
"name": "Mettre à jour le document de comparaison avec les résultats",
"type": "n8n-nodes-base.googleDocs",
"position": [
3920,
-365
],
"parameters": {
"actionsUi": {
"actionFields": [
{
"text": "={{ $('Convert Markdown to HTML').item.json.data }}",
"action": "insert"
}
]
},
"operation": "update",
"documentURL": "={{ $json.id }}"
},
"credentials": {
"googleDocsOAuth2Api": {
"id": "T3HEUOkeb37yLim7",
"name": "Google Docs account"
}
},
"typeVersion": 2
},
{
"id": "b532d34b-0ac0-4a00-9949-a9a959e27b31",
"name": "Mettre à jour la feuille de calcul avec le fichier de comparaison",
"type": "n8n-nodes-base.googleSheets",
"position": [
4140,
-365
],
"parameters": {
"columns": {
"value": {
"URL": "={{ $('Loop over each comparison URL').item.json.URL }}",
"Comparison File": "=https://docs.google.com/document/d/{{ $json.documentId }}"
},
"schema": [
{
"id": "URL",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "URL",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Previous Week ID",
"type": "string",
"display": true,
"removed": true,
"required": false,
"displayName": "Previous Week ID",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Previous Week File Link",
"type": "string",
"display": true,
"removed": true,
"required": false,
"displayName": "Previous Week File Link",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Current Week File ID",
"type": "string",
"display": true,
"removed": true,
"required": false,
"displayName": "Current Week File ID",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Current Week File Link",
"type": "string",
"display": true,
"removed": true,
"required": false,
"displayName": "Current Week File Link",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Comparison File",
"type": "string",
"display": true,
"required": false,
"displayName": "Comparison File",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "row_number",
"type": "string",
"display": true,
"removed": true,
"readOnly": true,
"required": false,
"displayName": "row_number",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [
"URL"
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "update",
"sheetName": {
"__rl": true,
"mode": "list",
"value": "gid=0",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1e4oheZjmxb3P7OXGY0uLWZKz2ENcp7XlEpIloOtRHEk/edit#gid=0",
"cachedResultName": "Sheet1"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "1e4oheZjmxb3P7OXGY0uLWZKz2ENcp7XlEpIloOtRHEk",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1e4oheZjmxb3P7OXGY0uLWZKz2ENcp7XlEpIloOtRHEk/edit?usp=drivesdk",
"cachedResultName": "BrightData Scraping Comparison"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"id": "vnXMSwscCP06bp9u",
"name": "Google Sheets account"
}
},
"typeVersion": 4.5
},
{
"id": "7ef3ba20-c79e-4d49-8a68-c11d20c77def",
"name": "Envoyer un email des résultats de comparaison",
"type": "n8n-nodes-base.gmail",
"position": [
4340,
-260
],
"webhookId": "8d51cb0f-f585-4222-93dd-2fffcead588e",
"parameters": {
"sendTo": "dave@emberautomation.com",
"message": "={{ $('Convert Markdown to HTML').item.json.data }}",
"options": {},
"subject": "={{ $now.format('yyyy-MM-dd') }}: {{ $('Loop over each comparison URL').item.json.URL }} weekly comparison"
},
"credentials": {
"gmailOAuth2": {
"id": "nkZOm8cNEGHWhTE4",
"name": "Gmail account"
}
},
"typeVersion": 2.1
},
{
"id": "a0926fe8-80c2-456b-b9c2-4dc1675c408c",
"name": "Note adhésive",
"type": "n8n-nodes-base.stickyNote",
"position": [
-540,
-1340
],
"parameters": {
"width": 760,
"height": 1060,
"content": "# BrightData Weekly Comparison\n## Overview\nThis workflow tracks changes on web pages, compares data, generates change reports, saves to Drive, and sends email notifications. **The workflow runs automatically on a weekly basis.**\n\n## Prerequisites\n- **Bright Data MCP Server**: Set up a Bright Data MCP server for web scraping\n- **n8n MCP Client Node**: Install the community node from `n8n-nodes-mcp` (https://www.npmjs.com/package/n8n-nodes-mcp)\n\n## Setup Steps\n1. **Duplicate Spreadsheet**\n - Make a copy of the comparison spreadsheet: [Sheet to Copy](https://docs.google.com/spreadsheets/d/1oPyAaTS8GMqlaBcyCO7G7MRtzMUUaOnA45JfWCzcCa8/edit?usp=sharing)\n - Update the URLs of web pages to track\n\n2. **Configure Variables**\n - Open the \"Set workflow variables\" node\n - Update these values:\n - `DriveFolderID`: Your Google Drive folder ID\n - `ComparisonSpreadsheetFileID`: `1e4oheZjmxb3P7OXGY0uLWZKz2ENcp7XlEpIloOtRHEk` (or your copied spreadsheet ID)\n - `ComparisonSpreadsheetSheetName`: Sheet name (usually \"Sheet1\")\n - `Email`: Where to send reports\n - `IsTest`: Set to `true` only for testing (set to `false` for regular operation)\n\n3. **Configure Bright Data MCP**\n - Set up your Bright Data MCP credentials in the MCP client node\n - Configure the MCP server using the documentation: https://github.com/luminati-io/brightdata-mcp\n\n4. **Set Up Credentials**\n - Configure credentials for:\n - Google Sheets\n - Google Drive\n - Google Docs\n - Gmail\n - OpenAI (or alternative AI provider)\n - Bright Data MCP\n\n5. **Run Workflow**\n - Execute the workflow manually to test\n - Check your Drive folder and email for results\n - **Once setup is complete, the workflow will run automatically every week**"
},
"typeVersion": 1
},
{
"id": "33c8b369-b6f7-4bb9-87e2-d202d998255d",
"name": "GPT-4.1",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
"position": [
620,
-145
],
"parameters": {
"model": {
"__rl": true,
"mode": "list",
"value": "gpt-4.1",
"cachedResultName": "gpt-4.1"
},
"options": {}
},
"credentials": {
"openAiApi": {
"id": "NuECwZyKpEfWhSN1",
"name": "OpenAi account"
}
},
"typeVersion": 1.2
},
{
"id": "700e8bb2-607d-4889-b248-88bf6464c587",
"name": "Déclencheur Planifié",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
-480,
-120
],
"parameters": {
"rule": {
"interval": [
{
"field": "weeks",
"triggerAtDay": [
3
],
"triggerAtHour": 12
}
]
}
},
"typeVersion": 1.2
},
{
"id": "c2919b53-c13f-4033-86f2-3b4e2e28dfa8",
"name": "scrape_as_markdown",
"type": "n8n-nodes-mcp.mcpClientTool",
"position": [
740,
-145
],
"parameters": {
"toolName": "scrape_as_markdown",
"operation": "executeTool",
"toolParameters": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('Tool_Parameters', ``, 'json') }}"
},
"credentials": {
"mcpClientApi": {
"id": "mO1My0js13704jqM",
"name": "MCP Client (STDIO) account"
}
},
"typeVersion": 1
},
{
"id": "cbbfbf09-9d2b-4c3b-bcbc-fc43f198f0c0",
"name": "Analyseur de Sortie Auto-corrigeant",
"type": "@n8n/n8n-nodes-langchain.outputParserAutofixing",
"position": [
640,
20
],
"parameters": {
"options": {
"prompt": "Instructions:\n--------------\n{instructions}\n--------------\nCompletion:\n--------------\n{completion}\n--------------\n\nAbove, the Completion did not satisfy the constraints given in the Instructions.\nError:\n--------------\n{error}\n--------------\n\nPlease try again. Please only respond with an answer that satisfies the constraints laid out in the Instructions:"
}
},
"typeVersion": 1
},
{
"id": "f3766efe-53a7-4311-9b53-af9f8d11b292",
"name": "Note adhésive1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-540,
-260
],
"parameters": {
"color": 2,
"width": 220,
"height": 340,
"content": "## Scheduled Weekly"
},
"typeVersion": 1
},
{
"id": "93f1107c-8e81-439b-a912-5900e36afd8b",
"name": "Note adhésive2",
"type": "n8n-nodes-base.stickyNote",
"position": [
-280,
-260
],
"parameters": {
"color": 3,
"width": 620,
"height": 340,
"content": "## Initialization\nInitializes the workflow variables, reads the main spreadsheet, then merges the spreadsheet results with the main workflow variables. "
},
"typeVersion": 1
},
{
"id": "f4882880-6ee4-4a44-9967-1be01de7151c",
"name": "Note adhésive3",
"type": "n8n-nodes-base.stickyNote",
"position": [
580,
-520
],
"parameters": {
"color": 4,
"width": 400,
"height": 780,
"content": "## AI Scraping\nFor the given URL, the agent will use the Scrape as Markdown tool to scrape the page getting the content as Markdown. It will then generate a JSON structure, extracting out relevant information from the page. "
},
"typeVersion": 1
},
{
"id": "8f6da1fd-c91a-4c99-94f8-b2464a0786be",
"name": "Note adhésive4",
"type": "n8n-nodes-base.stickyNote",
"position": [
1020,
-520
],
"parameters": {
"color": 5,
"width": 860,
"height": 400,
"content": "## Process current week results\nThe agent's response is turned into a binary file and then saved in Google Drive for the next week. Then the main spreadsheet is updated with that file. Finally checking if we have a previous week to compare this week to. If not, we finish and move on to the next item. If we do, then we move on and process the previous week's results vs the current week. "
},
"typeVersion": 1
},
{
"id": "25f758b4-2e11-4cde-bcc3-1a8e53106d29",
"name": "Note adhésive5",
"type": "n8n-nodes-base.stickyNote",
"position": [
1900,
-520
],
"parameters": {
"color": 6,
"width": 640,
"height": 400,
"content": "## Process previous week results\nRead the previous week's ID and download the file from Google Drive. Then, set variables ready for processing. "
},
"typeVersion": 1
},
{
"id": "f8d7c276-9540-431d-860d-10e15b769fe4",
"name": "Note adhésive6",
"type": "n8n-nodes-base.stickyNote",
"position": [
2560,
-520
],
"parameters": {
"color": 7,
"width": 440,
"height": 400,
"content": "## Mocking\nIf we are in test mode, then this will mock example changes. Otherwise, it will carry on as normal. "
},
"typeVersion": 1
},
{
"id": "c2dfbe5e-e79e-4cc8-91f2-a61bfef2721e",
"name": "Note adhésive7",
"type": "n8n-nodes-base.stickyNote",
"position": [
3020,
-520
],
"parameters": {
"color": 2,
"width": 1260,
"height": 400,
"content": "## Current week vs previous week comparison\nUsing code notes, we detect the changes from the previous week vs the current week and then convert that to a Markdown document and then use the Markdown to HTML node. Finally, creating a document in Google Docs, and then updating the main spreadsheet with that comparison document. "
},
"typeVersion": 1
},
{
"id": "a261bcd8-91a1-40f4-928f-19013b68b998",
"name": "Convertir la réponse JSON de la semaine actuelle en fichier",
"type": "n8n-nodes-base.convertToFile",
"position": [
1060,
-365
],
"parameters": {
"options": {},
"operation": "toJson"
},
"typeVersion": 1.1
},
{
"id": "dc49334a-6d00-4476-b84c-7bddfa3c0911",
"name": "Note adhésive8",
"type": "n8n-nodes-base.stickyNote",
"position": [
4300,
-520
],
"parameters": {
"width": 380,
"height": 400,
"content": "## Send the comparison email"
},
"typeVersion": 1
}
],
"active": false,
"pinData": {},
"settings": {
"executionOrder": "v1"
},
"versionId": "3556e422-eddc-40df-8f4f-860d621258d4",
"connections": {
"33c8b369-b6f7-4bb9-87e2-d202d998255d": {
"ai_languageModel": [
[
{
"node": "c50e3ec4-c3bf-4417-94ad-5b57ad6424da",
"type": "ai_languageModel",
"index": 0
},
{
"node": "cbbfbf09-9d2b-4c3b-bcbc-fc43f198f0c0",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"700e8bb2-607d-4889-b248-88bf6464c587": {
"main": [
[
{
"node": "6d4b38bd-fb58-4675-9b83-ade043914d65",
"type": "main",
"index": 0
}
]
]
},
"40b828f7-08f2-41ea-a6f7-595c5e4ae3e0": {
"main": [
[
{
"node": "af562b9f-3cfc-4715-ae47-779a0554e7d2",
"type": "main",
"index": 0
}
],
[
{
"node": "dd764b6d-d412-44e6-8816-e7b66bbb44c6",
"type": "main",
"index": 0
}
]
]
},
"c2919b53-c13f-4033-86f2-3b4e2e28dfa8": {
"ai_tool": [
[
{
"node": "c50e3ec4-c3bf-4417-94ad-5b57ad6424da",
"type": "ai_tool",
"index": 0
}
]
]
},
"6d4b38bd-fb58-4675-9b83-ade043914d65": {
"main": [
[
{
"node": "45eefb28-dce0-4fc3-81a1-865762c73226",
"type": "main",
"index": 0
}
]
]
},
"16c9f196-6acf-4d3a-946b-727c121b503a": {
"main": [
[
{
"node": "3df4c31f-db47-40a5-9140-d2de7b1fbf2e",
"type": "main",
"index": 0
}
]
]
},
"a2966ed3-e998-47ca-83b3-b7dc4832bc8f": {
"ai_outputParser": [
[
{
"node": "cbbfbf09-9d2b-4c3b-bcbc-fc43f198f0c0",
"type": "ai_outputParser",
"index": 0
}
]
]
},
"cbbfbf09-9d2b-4c3b-bcbc-fc43f198f0c0": {
"ai_outputParser": [
[
{
"node": "c50e3ec4-c3bf-4417-94ad-5b57ad6424da",
"type": "ai_outputParser",
"index": 0
}
]
]
},
"3df4c31f-db47-40a5-9140-d2de7b1fbf2e": {
"main": [
[
{
"node": "da24c1a3-fa3b-4b06-b5b9-78dd6d872756",
"type": "main",
"index": 0
}
]
]
},
"af562b9f-3cfc-4715-ae47-779a0554e7d2": {
"main": [
[
{
"node": "dd764b6d-d412-44e6-8816-e7b66bbb44c6",
"type": "main",
"index": 0
}
]
]
},
"dd764b6d-d412-44e6-8816-e7b66bbb44c6": {
"main": [
[
{
"node": "e5a8b7c1-53ec-4a26-8791-30db494cde92",
"type": "main",
"index": 0
}
]
]
},
"301dfc46-f1d4-4b1a-892b-25ecb4495b77": {
"main": [
[
{
"node": "036d00c9-7414-4883-ac56-ef9368195b85",
"type": "main",
"index": 0
}
]
]
},
"c04cec36-1664-48d7-a9ae-a5665bc2c188": {
"main": [
[],
[
{
"node": "c50e3ec4-c3bf-4417-94ad-5b57ad6424da",
"type": "main",
"index": 0
}
]
]
},
"c1f69dc5-6612-4575-aaf4-1d6a7a70c423": {
"main": [
[
{
"node": "e4a1fd2c-e1a5-4065-a9c8-76d1c854af95",
"type": "main",
"index": 0
}
]
]
},
"7ef3ba20-c79e-4d49-8a68-c11d20c77def": {
"main": [
[
{
"node": "c04cec36-1664-48d7-a9ae-a5665bc2c188",
"type": "main",
"index": 0
}
]
]
},
"45eefb28-dce0-4fc3-81a1-865762c73226": {
"main": [
[
{
"node": "6ddda34f-556e-49a5-a103-7641a3f0598d",
"type": "main",
"index": 0
}
]
]
},
"67b8ee0a-2ab0-43cd-b24f-0c8ba6b04cd1": {
"main": [
[
{
"node": "40b828f7-08f2-41ea-a6f7-595c5e4ae3e0",
"type": "main",
"index": 0
}
]
]
},
"036d00c9-7414-4883-ac56-ef9368195b85": {
"main": [
[
{
"node": "67b8ee0a-2ab0-43cd-b24f-0c8ba6b04cd1",
"type": "main",
"index": 0
}
]
]
},
"32934604-9e72-4f6a-aa55-fc0abff1d85c": {
"main": [
[
{
"node": "301dfc46-f1d4-4b1a-892b-25ecb4495b77",
"type": "main",
"index": 0
}
],
[
{
"node": "c04cec36-1664-48d7-a9ae-a5665bc2c188",
"type": "main",
"index": 0
}
]
]
},
"c50e3ec4-c3bf-4417-94ad-5b57ad6424da": {
"main": [
[
{
"node": "a261bcd8-91a1-40f4-928f-19013b68b998",
"type": "main",
"index": 0
}
]
]
},
"e5a8b7c1-53ec-4a26-8791-30db494cde92": {
"main": [
[
{
"node": "16c9f196-6acf-4d3a-946b-727c121b503a",
"type": "main",
"index": 0
}
]
]
},
"da24c1a3-fa3b-4b06-b5b9-78dd6d872756": {
"main": [
[
{
"node": "b532d34b-0ac0-4a00-9949-a9a959e27b31",
"type": "main",
"index": 0
}
]
]
},
"a261bcd8-91a1-40f4-928f-19013b68b998": {
"main": [
[
{
"node": "c1f69dc5-6612-4575-aaf4-1d6a7a70c423",
"type": "main",
"index": 0
}
]
]
},
"6ddda34f-556e-49a5-a103-7641a3f0598d": {
"main": [
[
{
"node": "c04cec36-1664-48d7-a9ae-a5665bc2c188",
"type": "main",
"index": 0
}
]
]
},
"b532d34b-0ac0-4a00-9949-a9a959e27b31": {
"main": [
[
{
"node": "7ef3ba20-c79e-4d49-8a68-c11d20c77def",
"type": "main",
"index": 0
}
]
]
},
"e4a1fd2c-e1a5-4065-a9c8-76d1c854af95": {
"main": [
[
{
"node": "32934604-9e72-4f6a-aa55-fc0abff1d85c",
"type": "main",
"index": 0
}
]
]
}
}
}Comment utiliser ce workflow ?
Copiez le code de configuration JSON ci-dessus, créez un nouveau workflow dans votre instance n8n et sélectionnez "Importer depuis le JSON", collez la configuration et modifiez les paramètres d'authentification selon vos besoins.
Dans quelles scénarios ce workflow est-il adapté ?
Avancé - Intelligence Artificielle, Marketing
Est-ce payant ?
Ce workflow est entièrement gratuit et peut être utilisé directement. Veuillez noter que les services tiers utilisés dans le workflow (comme l'API OpenAI) peuvent nécessiter un paiement de votre part.
Workflows recommandés
Daniel Shashko
@tomaxAI automation specialist and a marketing enthusiast. More than 6 years of experience in SEO/GEO. Senior SEO at Bright Data.
Partager ce workflow