BrightData Weekly Comparison
Shared 12/3/2025
1 views
Visual Workflow
JSON Code
{
"id": "pNaD8QIgVGDqbCoU",
"meta": {
"instanceId": "3af183a3db355380be4f6d2f3dfb18bdaa750e90f99a48f91bd71080ee6bcbe8",
"templateCredsSetupCompleted": true
},
"name": "BrightData Weekly Comparison",
"tags": [],
"nodes": [
{
"id": "a2966ed3-e998-47ca-83b3-b7dc4832bc8f",
"name": "Structured Output Parser",
"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": "Set workflow variables",
"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": "Merge workflow variables with Google Sheet data",
"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": "Read from comparison spreadsheets",
"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": "Loop over each comparison URL",
"type": "n8n-nodes-base.splitInBatches",
"position": [
400,
-115
],
"parameters": {
"options": {}
},
"typeVersion": 3
},
{
"id": "c50e3ec4-c3bf-4417-94ad-5b57ad6424da",
"name": "Web scraping and data extraction agent",
"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": "Upload current week JSON file",
"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": "Update comparison sheet with current week file data",
"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": "Check presence of previous week's file",
"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": "Download previous week's file",
"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": "Convert previous week's file to JSON",
"type": "n8n-nodes-base.extractFromFile",
"position": [
2160,
-365
],
"parameters": {
"options": {},
"operation": "fromJson"
},
"typeVersion": 1
},
{
"id": "67b8ee0a-2ab0-43cd-b24f-0c8ba6b04cd1",
"name": "Set previous week and current week",
"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": "Check if test mode",
"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": "Mock previous week changes",
"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": "Detect changes between weeks",
"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": "Generate Markdown from detected changes",
"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": "Convert Markdown to 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": "Create comparison document",
"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": "Update comparison document with results",
"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": "Update comparison spreadsheet with comparison file",
"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": "Send email of comparison results",
"type": "n8n-nodes-base.gmail",
"position": [
4340,
-260
],
"webhookId": "8d51cb0f-f585-4222-93dd-2fffcead588e",
"parameters": {
"sendTo": "example@gmail.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": "Sticky Note",
"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": "Schedule Trigger",
"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": "Auto-fixing Output Parser",
"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": "Sticky Note1",
"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": "Sticky Note2",
"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": "Sticky Note3",
"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": "Sticky Note4",
"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": "Sticky Note5",
"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": "Sticky Note6",
"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": "Sticky Note7",
"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": "Convert current week JSON response to file",
"type": "n8n-nodes-base.convertToFile",
"position": [
1060,
-365
],
"parameters": {
"options": {},
"operation": "toJson"
},
"typeVersion": 1.1
},
{
"id": "dc49334a-6d00-4476-b84c-7bddfa3c0911",
"name": "Sticky Note8",
"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": {
"GPT-4.1": {
"ai_languageModel": [
[
{
"node": "Web scraping and data extraction agent",
"type": "ai_languageModel",
"index": 0
},
{
"node": "Auto-fixing Output Parser",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Schedule Trigger": {
"main": [
[
{
"node": "Set workflow variables",
"type": "main",
"index": 0
}
]
]
},
"Check if test mode": {
"main": [
[
{
"node": "Mock previous week changes",
"type": "main",
"index": 0
}
],
[
{
"node": "Detect changes between weeks",
"type": "main",
"index": 0
}
]
]
},
"scrape_as_markdown": {
"ai_tool": [
[
{
"node": "Web scraping and data extraction agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Set workflow variables": {
"main": [
[
{
"node": "Read from comparison spreadsheets",
"type": "main",
"index": 0
}
]
]
},
"Convert Markdown to HTML": {
"main": [
[
{
"node": "Create comparison document",
"type": "main",
"index": 0
}
]
]
},
"Structured Output Parser": {
"ai_outputParser": [
[
{
"node": "Auto-fixing Output Parser",
"type": "ai_outputParser",
"index": 0
}
]
]
},
"Auto-fixing Output Parser": {
"ai_outputParser": [
[
{
"node": "Web scraping and data extraction agent",
"type": "ai_outputParser",
"index": 0
}
]
]
},
"Create comparison document": {
"main": [
[
{
"node": "Update comparison document with results",
"type": "main",
"index": 0
}
]
]
},
"Mock previous week changes": {
"main": [
[
{
"node": "Detect changes between weeks",
"type": "main",
"index": 0
}
]
]
},
"Detect changes between weeks": {
"main": [
[
{
"node": "Generate Markdown from detected changes",
"type": "main",
"index": 0
}
]
]
},
"Download previous week's file": {
"main": [
[
{
"node": "Convert previous week's file to JSON",
"type": "main",
"index": 0
}
]
]
},
"Loop over each comparison URL": {
"main": [
[],
[
{
"node": "Web scraping and data extraction agent",
"type": "main",
"index": 0
}
]
]
},
"Upload current week JSON file": {
"main": [
[
{
"node": "Update comparison sheet with current week file data",
"type": "main",
"index": 0
}
]
]
},
"Send email of comparison results": {
"main": [
[
{
"node": "Loop over each comparison URL",
"type": "main",
"index": 0
}
]
]
},
"Read from comparison spreadsheets": {
"main": [
[
{
"node": "Merge workflow variables with Google Sheet data",
"type": "main",
"index": 0
}
]
]
},
"Set previous week and current week": {
"main": [
[
{
"node": "Check if test mode",
"type": "main",
"index": 0
}
]
]
},
"Convert previous week's file to JSON": {
"main": [
[
{
"node": "Set previous week and current week",
"type": "main",
"index": 0
}
]
]
},
"Check presence of previous week's file": {
"main": [
[
{
"node": "Download previous week's file",
"type": "main",
"index": 0
}
],
[
{
"node": "Loop over each comparison URL",
"type": "main",
"index": 0
}
]
]
},
"Web scraping and data extraction agent": {
"main": [
[
{
"node": "Convert current week JSON response to file",
"type": "main",
"index": 0
}
]
]
},
"Generate Markdown from detected changes": {
"main": [
[
{
"node": "Convert Markdown to HTML",
"type": "main",
"index": 0
}
]
]
},
"Update comparison document with results": {
"main": [
[
{
"node": "Update comparison spreadsheet with comparison file",
"type": "main",
"index": 0
}
]
]
},
"Convert current week JSON response to file": {
"main": [
[
{
"node": "Upload current week JSON file",
"type": "main",
"index": 0
}
]
]
},
"Merge workflow variables with Google Sheet data": {
"main": [
[
{
"node": "Loop over each comparison URL",
"type": "main",
"index": 0
}
]
]
},
"Update comparison spreadsheet with comparison file": {
"main": [
[
{
"node": "Send email of comparison results",
"type": "main",
"index": 0
}
]
]
},
"Update comparison sheet with current week file data": {
"main": [
[
{
"node": "Check presence of previous week's file",
"type": "main",
"index": 0
}
]
]
}
}
}