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 →
{
"name": "Project 5",
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
0,
0
],
"id": "f56e8806-c6bf-4eca-9e4d-d90307308813",
"name": "Run Manually"
},
{
"parameters": {
"resource": "fileFolder",
"queryString": "Wayfair Reports",
"filter": {},
"options": {}
},
"type": "n8n-nodes-base.googleDrive",
"typeVersion": 3,
"position": [
208,
0
],
"id": "a654c3f5-4971-42c8-87eb-4d8e07553645",
"name": "Find Wayfair Reports Folder",
"credentials": {
"googleDriveOAuth2Api": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"resource": "fileFolder",
"filter": {
"folderId": {
"__rl": true,
"value": "={{ $json.id }}",
"mode": "id"
}
},
"options": {}
},
"type": "n8n-nodes-base.googleDrive",
"typeVersion": 3,
"position": [
416,
0
],
"id": "b29857e1-ce37-461a-b5ed-697eb8c17faf",
"name": "List Root Contents",
"credentials": {
"googleDriveOAuth2Api": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "// Identify categories (folders) and template file\n // Note: mimeType not available, so we identify by name patterns\n const items = $input.all();\n\n const categories = [];\n let templateFileId = null;\n\n for (const item of items) {\n const name = item.json.name || '';\n const id = item.json.id;\n\n // Template file ends with .html and contains 'template'\n if (name.toLowerCase().includes('template') && name.endsWith('.html')) {\n templateFileId = id;\n console.log('Found template:', name);\n }\n // Category folders don't have file extensions\n else if (!name.includes('.')) {\n categories.push({\n categoryName: name.toLowerCase().replace(/\\s+/g, '_'),\n categoryFolderId: id\n });\n console.log('Found category:', name);\n }\n }\n\n console.log('Total categories:', categories.length);\n console.log('Template ID:', templateFileId);\n\n // Return first category for simplified version\n if (categories.length > 0) {\n return [{\n json: {\n ...categories[0],\n templateFileId\n }\n }];\n }\n\n return [];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
624,
0
],
"id": "6cae58d8-9b85-438c-86db-396ef0484b7f",
"name": "Identify Category & Template"
},
{
"parameters": {
"operation": "download",
"fileId": {
"__rl": true,
"value": "={{ $json.templateFileId }}",
"mode": "id"
},
"options": {}
},
"type": "n8n-nodes-base.googleDrive",
"typeVersion": 3,
"position": [
832,
0
],
"id": "6a6e7ebc-40fa-43cc-9ac6-63eec7ad8c11",
"name": "Download Template",
"credentials": {
"googleDriveOAuth2Api": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "const item = $input.first();\nlet templateHtml = '';\n\nif (item.binary?.data) {\n const buffer = await this.helpers.getBinaryDataBuffer(0, 'data');\n templateHtml = buffer.toString('utf8'); // no 'const' - assigns to outer variable\n console.log('Template loaded, length:', templateHtml.length);\n}\n\nconst categoryInfo = $('Identify Category & Template').first().json;\n\nreturn [{\n json: {\n categoryName: categoryInfo.categoryName,\n categoryFolderId: categoryInfo.categoryFolderId,\n templateHtml\n }\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1040,
0
],
"id": "30d10755-3261-4b2d-8bc7-d00a4075b3ea",
"name": "Store Template"
},
{
"parameters": {
"resource": "fileFolder",
"filter": {
"folderId": {
"__rl": true,
"value": "={{ $('Identify Category & Template').item.json.categoryFolderId }}",
"mode": "id"
}
},
"options": {}
},
"type": "n8n-nodes-base.googleDrive",
"typeVersion": 3,
"position": [
1248,
0
],
"id": "f5c6977a-ca20-473c-8f9c-3e875283a47d",
"name": "List Date Folders",
"credentials": {
"googleDriveOAuth2Api": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "// Get only the LATEST date folder\nconst items = $input.all();\nconst categoryInfo = $('Store Template').first().json;\n\n// Filter to date folders (YYYY-MM-DD pattern) and sort descending\nconst dateFolders = items\n .filter(item => /^\\d{4}-\\d{2}-\\d{2}$/.test(item.json.name))\n .map(item => ({\n dateFolderId: item.json.id,\n dateFolderName: item.json.name\n }))\n .sort((a, b) => b.dateFolderName.localeCompare(a.dateFolderName));\n\nconsole.log('Date folders found:', dateFolders.length);\nconsole.log('Latest:', dateFolders[0]?.dateFolderName);\n\n// Return only the latest one\nif (dateFolders.length > 0) {\n return [{\n json: {\n ...categoryInfo,\n ...dateFolders[0]\n }\n }];\n}\n\nreturn [];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1456,
0
],
"id": "bc3fca46-9901-41d6-b3b0-277c0dafcf99",
"name": "Get Latest Date Only"
},
{
"parameters": {
"resource": "fileFolder",
"filter": {
"folderId": {
"__rl": true,
"value": "={{ $json.dateFolderId }}",
"mode": "id"
}
},
"options": {}
},
"type": "n8n-nodes-base.googleDrive",
"typeVersion": 3,
"position": [
1664,
0
],
"id": "70105beb-0bc6-41df-b84c-06f6dc787dc2",
"name": "List P2/P3 Files",
"credentials": {
"googleDriveOAuth2Api": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "// Identify P2 and P3 file IDs\nconst items = $input.all();\nconst context = $('Get Latest Date Only').first().json;\n\nlet p2FileId = null;\nlet p3FileId = null;\n\nfor (const item of items) {\n const name = (item.json.name || '').toLowerCase();\n const id = item.json.id;\n \n if (name.includes('p2') || name.includes('market')) {\n p2FileId = id;\n console.log('Found P2:', name);\n }\n if (name.includes('p3') || name.includes('competitor')) {\n p3FileId = id;\n console.log('Found P3:', name);\n }\n}\n\nconsole.log('P2 ID:', p2FileId);\nconsole.log('P3 ID:', p3FileId);\n\nreturn [{\n json: {\n ...context,\n p2FileId,\n p3FileId,\n hasP2: !!p2FileId,\n hasP3: !!p3FileId\n }\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1888,
0
],
"id": "391a1769-ef7f-4592-80a3-95bf0eac5a46",
"name": "Identify P2 & P3 Files"
},
{
"parameters": {
"operation": "download",
"fileId": {
"__rl": true,
"value": "={{ $json.p2FileId }}",
"mode": "id"
},
"options": {}
},
"type": "n8n-nodes-base.googleDrive",
"typeVersion": 3,
"position": [
2080,
-112
],
"id": "2136e517-0140-451c-9b70-2495f29f149f",
"name": "Download P2",
"credentials": {
"googleDriveOAuth2Api": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"operation": "download",
"fileId": {
"__rl": true,
"value": "={{ $json.p3FileId }}",
"mode": "id"
},
"options": {}
},
"type": "n8n-nodes-base.googleDrive",
"typeVersion": 3,
"position": [
2080,
80
],
"id": "56b65252-d13d-4f26-b56c-612731f5178f",
"name": "Download P3",
"credentials": {
"googleDriveOAuth2Api": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "// Rename binary key from 'data' to 'p2_binary' to avoid merge conflicts\nconst items = $input.all();\n\nreturn items.map(item => {\n const newItem = {\n json: { ...item.json },\n binary: {}\n };\n \n if (item.binary?.data) {\n newItem.binary.p2_binary = item.binary.data;\n console.log('Renamed P2 binary');\n }\n \n return newItem;\n});"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2288,
-112
],
"id": "c2c018d3-e397-4ef9-936c-9363833197e1",
"name": "Rename P2 Binary"
},
{
"parameters": {
"jsCode": "// Rename binary key from 'data' to 'p3_binary' to avoid merge conflicts\nconst items = $input.all();\n\nreturn items.map(item => {\n const newItem = {\n json: { ...item.json },\n binary: {}\n };\n \n if (item.binary?.data) {\n newItem.binary.p3_binary = item.binary.data;\n console.log('Renamed P3 binary');\n }\n \n return newItem;\n});"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2288,
80
],
"id": "0e099046-d65e-45be-b489-d41a230349ed",
"name": "Rename P3 Binary"
},
{
"parameters": {
"mode": "combine",
"combineBy": "combineByPosition",
"options": {}
},
"type": "n8n-nodes-base.merge",
"typeVersion": 3.2,
"position": [
2544,
0
],
"id": "bf842c89-8d2f-498f-abaa-a7cfef2c4d24",
"name": "Merge P2 & P3"
},
{
"parameters": {
"jsCode": "// ================================================================\n// PARSE P2 & P3 REPORTS - Complete Extraction\n// ================================================================\n\nconst cheerio = require('cheerio');\nconst item = $input.first();\n\n// Get context from earlier nodes\nconst context = $('Identify P2 & P3 Files').first().json;\n\n// Extract binary content\nlet p2Html = '';\nlet p3Html = '';\n\nconsole.log('Binary keys available:', Object.keys(item.binary || {}));\n\nif (item.binary?.p2_binary) {\n const p2Buffer = await this.helpers.getBinaryDataBuffer(0, 'p2_binary');\n p2Html = p2Buffer.toString('utf8');\n}\nif (item.binary?.p3_binary) {\n const p3Buffer = await this.helpers.getBinaryDataBuffer(0, 'p3_binary');\n p3Html = p3Buffer.toString('utf8');\n}\n\n\n// ================================================================\n// PARSE P2 (Market Trend Report)\n// ================================================================\nlet p2Data = {\n keyInsight: '',\n marketMetrics: { size2025: '', size2033: '', cagr: '' },\n microSegments: [],\n colorPalette: [],\n risks: [],\n recommendations: [],\n moodboardImages: [],\n attributeBars: { materials: [], sizes: [], colors: [], prices: [] }\n};\n\ntry {\n if (p2Html) {\n const $p2 = cheerio.load(p2Html);\n \n // Key Insight\n p2Data.keyInsight = $p2('.key-insight p').first().text().trim() ||\n $p2('.summary-hero .key-insight p').first().text().trim() ||\n $p2('.executive-summary p').first().text().trim();\n console.log('P2 Key Insight:', p2Data.keyInsight.substring(0, 100));\n \n // Market Metrics - look for profile-card elements\n $p2('.profile-card').each((i, el) => {\n const heading = $p2(el).find('h4').text().toLowerCase();\n const value = $p2(el).find('p').first().text().trim();\n \n if (heading.includes('market size') && heading.includes('2025')) {\n p2Data.marketMetrics.size2025 = value;\n } else if (heading.includes('projected') && heading.includes('2033')) {\n p2Data.marketMetrics.size2033 = value;\n } else if (heading.includes('cagr')) {\n p2Data.marketMetrics.cagr = value;\n }\n });\n console.log('P2 Market Metrics:', JSON.stringify(p2Data.marketMetrics));\n \n // Micro-Segments\n $p2('.segment-card').each((i, el) => {\n const name = $p2(el).find('h4').first().text().trim();\n const tier = $p2(el).find('.segment-badge').text().trim();\n const description = $p2(el).find('.segment-description, p').first().text().trim();\n const metaText = $p2(el).find('.segment-meta').text();\n \n // Extract colors and use cases from meta\n const colorsMatch = metaText.match(/\ud83c\udfa8\\s*([^\ud83c\udfe0]+)/);\n const useCasesMatch = metaText.match(/\ud83c\udfe0\\s*(.+)/);\n \n if (name) {\n p2Data.microSegments.push({\n name: name.replace(/^\\d+\\.\\s*/, ''),\n tier: tier || 'Emerging',\n description: description.substring(0, 300),\n colors: colorsMatch?.[1]?.trim() || '',\n useCases: useCasesMatch?.[1]?.trim() || ''\n });\n }\n });\n console.log('P2 Segments found:', p2Data.microSegments.length);\n \n // Color Palette\n $p2('.color-swatch, .color-card').each((i, el) => {\n const style = $p2(el).attr('style') || '';\n const innerStyle = $p2(el).find('[style*=\"background\"]').attr('style') || '';\n const allStyles = style + ' ' + innerStyle;\n const hexMatch = allStyles.match(/#[A-Fa-f0-9]{6}/);\n const name = $p2(el).find('.swatch-label, .color-name').text().trim() || \n $p2(el).text().trim().split('\\n')[0];\n \n if (hexMatch && p2Data.colorPalette.length < 6) {\n p2Data.colorPalette.push({\n hex: hexMatch[0],\n name: name.substring(0, 30) || `Color ${i + 1}`\n });\n }\n });\n console.log('P2 Colors found:', p2Data.colorPalette.length);\n \n // Risks\n $p2('.risk-card').each((i, el) => {\n const title = $p2(el).find('h4, .risk-title').first().text().trim();\n const className = $p2(el).attr('class') || '';\n let severity = 'medium';\n if (className.includes('high') || className.includes('risk-high')) severity = 'high';\n else if (className.includes('low') || className.includes('risk-low')) severity = 'low';\n \n const description = $p2(el).find('p, .risk-description').first().text().trim();\n const mitigation = $p2(el).find('.risk-mitigation').text().replace(/Mitigation:?/gi, '').trim();\n \n if (title) {\n p2Data.risks.push({ title, severity, description: description.substring(0, 200), mitigation });\n }\n });\n console.log('P2 Risks found:', p2Data.risks.length);\n \n // Recommendations\n $p2('.rec-item, .recommendation-card').each((i, el) => {\n const title = $p2(el).find('h4, strong').first().text().trim();\n const description = $p2(el).find('p').first().text().trim();\n const impact = $p2(el).find('.rec-impact').text().trim();\n \n if (title && p2Data.recommendations.length < 5) {\n p2Data.recommendations.push({ title, description: description.substring(0, 200), impact });\n }\n });\n console.log('P2 Recommendations found:', p2Data.recommendations.length);\n \n // Moodboard Images (base64)\n $p2('img').each((i, el) => {\n const src = $p2(el).attr('src');\n if (src?.startsWith('data:image') && p2Data.moodboardImages.length < 4) {\n p2Data.moodboardImages.push(src);\n }\n });\n console.log('P2 Moodboard images found:', p2Data.moodboardImages.length);\n \n // Attribute Distribution Bars - FIXED selectors\n $p2('.attribute-card').each((i, card) => {\n const cardTitle = $p2(card).find('h3').first().text().toLowerCase();\n const bars = [];\n\n $p2(card).find('.data-row').each((j, row) => {\n const label = $p2(row).find('.label').text().trim();\n const value = $p2(row).find('.value').text().trim();\n const barStyle = $p2(row).find('.bar').attr('style') || '';\n const widthMatch = barStyle.match(/([\\d.]+)%/);\n\n if (label) {\n bars.push({\n label,\n value: value || '',\n width: widthMatch ? parseFloat(widthMatch[1]) : 0\n });\n }\n });\n\n if (cardTitle.includes('material')) {\n p2Data.attributeBars.materials = bars;\n } else if (cardTitle.includes('size')) {\n p2Data.attributeBars.sizes = bars;\n } else if (cardTitle.includes('color') || cardTitle.includes('pattern')) {\n p2Data.attributeBars.colors = bars;\n } else if (cardTitle.includes('price')) {\n p2Data.attributeBars.prices = bars;\n }\n });\n console.log('P2 Attribute bars - materials:', p2Data.attributeBars.materials.length,\n ', sizes:', p2Data.attributeBars.sizes.length,\n ', colors:', p2Data.attributeBars.colors.length);\n }\n} catch (e) {\n console.error('P2 Parse Error:', e.message);\n}\n\n// ================================================================\n// PARSE P3 (Competitor Analysis Report)\n// ================================================================\nlet p3Data = {\n keyInsight: '',\n scope: { amazonProducts: 0, walmartProducts: 0, wayfairProducts: 0 },\n pricePositioning: {\n wayfair: { avg: '', min: '', max: '' },\n amazon: { avg: '', min: '', max: '' },\n walmart: { avg: '', min: '', max: '' }\n },\n comparisonTable: [],\n priceBands: { budget: {}, mid: {}, premium: {} },\n priceGaps: [],\n opportunities: [],\n quickWins: [],\n strategicInitiatives: [],\n suppliers: [],\n discountTiers: []\n};\n\ntry {\n if (p3Html) {\n const $p3 = cheerio.load(p3Html);\n \n // Key Insight\n p3Data.keyInsight = $p3('.key-insight p').first().text().trim();\n console.log('P3 Key Insight:', p3Data.keyInsight.substring(0, 100));\n \n // Scope - Product Counts\n const scopeText = $p3('.scope-card, .scope-section').text();\n const amazonMatch = scopeText.match(/Amazon[^\\d]*(\\d+)/i);\n const walmartMatch = scopeText.match(/Walmart[^\\d]*(\\d+)/i);\n const wayfairMatch = scopeText.match(/Wayfair[^\\d]*(\\d+)/i);\n if (amazonMatch) p3Data.scope.amazonProducts = parseInt(amazonMatch[1]);\n if (walmartMatch) p3Data.scope.walmartProducts = parseInt(walmartMatch[1]);\n if (wayfairMatch) p3Data.scope.wayfairProducts = parseInt(wayfairMatch[1]);\n console.log('P3 Scope:', JSON.stringify(p3Data.scope));\n \n // Price Positioning - from comparison table\n $p3('.comparison-table tbody tr').each((i, row) => {\n const cells = $p3(row).find('td');\n const attribute = $p3(cells[0]).text().toLowerCase();\n \n if (attribute.includes('price')) {\n // Parse Wayfair prices\n const wayfairText = $p3(cells[1]).text();\n const wayfairAvg = wayfairText.match(/Avg:\\s*\\$([\\d,.]+)/i);\n const wayfairMin = wayfairText.match(/Min:\\s*\\$([\\d,.]+)/i);\n const wayfairMax = wayfairText.match(/Max:\\s*\\$([\\d,.]+)/i);\n if (wayfairAvg) p3Data.pricePositioning.wayfair.avg = '$' + wayfairAvg[1];\n if (wayfairMin) p3Data.pricePositioning.wayfair.min = '$' + wayfairMin[1];\n if (wayfairMax) p3Data.pricePositioning.wayfair.max = '$' + wayfairMax[1];\n \n // Parse Amazon prices\n const amazonText = $p3(cells[2]).text();\n const amazonAvg = amazonText.match(/Avg:\\s*\\$([\\d,.]+)/i);\n const amazonMin = amazonText.match(/Min:\\s*\\$([\\d,.]+)/i);\n const amazonMax = amazonText.match(/Max:\\s*\\$([\\d,.]+)/i);\n if (amazonAvg) p3Data.pricePositioning.amazon.avg = '$' + amazonAvg[1];\n if (amazonMin) p3Data.pricePositioning.amazon.min = '$' + amazonMin[1];\n if (amazonMax) p3Data.pricePositioning.amazon.max = '$' + amazonMax[1];\n \n // Parse Walmart prices\n const walmartText = $p3(cells[3]).text();\n const walmartAvg = walmartText.match(/Avg:\\s*\\$([\\d,.]+)/i);\n const walmartMin = walmartText.match(/Min:\\s*\\$([\\d,.]+)/i);\n const walmartMax = walmartText.match(/Max:\\s*\\$([\\d,.]+)/i);\n if (walmartAvg) p3Data.pricePositioning.walmart.avg = '$' + walmartAvg[1];\n if (walmartMin) p3Data.pricePositioning.walmart.min = '$' + walmartMin[1];\n if (walmartMax) p3Data.pricePositioning.walmart.max = '$' + walmartMax[1];\n }\n });\n console.log('P3 Price Positioning:', JSON.stringify(p3Data.pricePositioning));\n \n // Comparison Table\n $p3('.comparison-table tbody tr').each((i, row) => {\n const cells = $p3(row).find('td');\n if (cells.length >= 5) {\n const attribute = $p3(cells[0]).text().trim();\n const wayfair = $p3(cells[1]).text().trim().substring(0, 200);\n const amazon = $p3(cells[2]).text().trim().substring(0, 200);\n const walmart = $p3(cells[3]).text().trim().substring(0, 200);\n const winner = $p3(cells[4]).text().trim();\n \n if (attribute) {\n p3Data.comparisonTable.push({ attribute, wayfair, amazon, walmart, winner });\n }\n }\n });\n console.log('P3 Comparison rows:', p3Data.comparisonTable.length);\n \n // Price Bands\n $p3('.band').each((i, el) => {\n const text = $p3(el).text();\n const values = text.match(/W:\\s*(\\d+)\\s*\\|\\s*A:\\s*(\\d+)\\s*\\|\\s*Wm:\\s*(\\d+)/i);\n \n if (text.toLowerCase().includes('budget') && values) {\n p3Data.priceBands.budget = { wayfair: values[1], amazon: values[2], walmart: values[3] };\n } else if (text.toLowerCase().includes('mid') && values) {\n p3Data.priceBands.mid = { wayfair: values[1], amazon: values[2], walmart: values[3] };\n } else if (text.toLowerCase().includes('premium') && values) {\n p3Data.priceBands.premium = { wayfair: values[1], amazon: values[2], walmart: values[3] };\n }\n });\n console.log('P3 Price Bands:', JSON.stringify(p3Data.priceBands));\n \n // Price Gaps Heatmap\n $p3('.gap').each((i, el) => {\n const range = $p3(el).find('span').first().text().trim();\n const description = $p3(el).find('span').last().text().trim();\n const className = $p3(el).attr('class') || '';\n let level = 'moderate';\n if (className.includes('hot')) level = 'hot';\n else if (className.includes('warm')) level = 'warm';\n else if (className.includes('cool') || className.includes('open')) level = 'open';\n \n if (range) {\n p3Data.priceGaps.push({ range, description, level });\n }\n });\n console.log('P3 Price Gaps:', p3Data.priceGaps.length);\n \n // Opportunities\n $p3('.whitespace-opportunities .opportunity-card, .opportunity-cards .opportunity-card').each((i, el) => {\n const title = $p3(el).find('h4').text().replace(/[\ud83c\udfaf\u26a1\ud83d\udcca\ud83d\udca1\ud83d\udd25]/g, '').trim();\n const description = $p3(el).find('p').first().text().trim();\n const className = $p3(el).attr('class') || '';\n const priority = className.includes('high') ? 'high' : (className.includes('low') ? 'low' : 'medium');\n \n if (title && p3Data.opportunities.length < 6) {\n p3Data.opportunities.push({ title, description: description.substring(0, 250), priority });\n }\n });\n console.log('P3 Opportunities:', p3Data.opportunities.length);\n \n // Quick Wins\n $p3('.quick-wins .rec-card').each((i, el) => {\n const title = $p3(el).find('h4').text().replace(/^\\d+\\.\\s*/, '').trim();\n const description = $p3(el).find('p').first().text().trim();\n const priority = $p3(el).attr('class')?.includes('high') ? 'high' : 'medium';\n \n if (title) {\n p3Data.quickWins.push({ title, description: description.substring(0, 200), priority });\n }\n });\n console.log('P3 Quick Wins:', p3Data.quickWins.length);\n \n // Strategic Initiatives\n $p3('.strategic-plays .rec-card, .strategic-initiatives .rec-card').each((i, el) => {\n const title = $p3(el).find('h4').text().replace(/^\\d+\\.\\s*/, '').trim();\n const description = $p3(el).find('p').first().text().trim();\n const priority = $p3(el).attr('class')?.includes('high') ? 'high' : 'medium';\n \n if (title) {\n p3Data.strategicInitiatives.push({ title, description: description.substring(0, 200), priority });\n }\n });\n console.log('P3 Strategic Initiatives:', p3Data.strategicInitiatives.length);\n \n // Suppliers\n $p3('.supplier-identification table tbody tr, .suppliers-observed table tbody tr').each((i, row) => {\n const cells = $p3(row).find('td');\n if (cells.length >= 4) {\n const name = $p3(cells[0]).text().trim();\n const platform = $p3(cells[1]).text().trim();\n \n // Skip if it looks like a product row\n if (name.length > 50 || platform.startsWith('$')) return;\n \n if (name && p3Data.suppliers.length < 10) {\n p3Data.suppliers.push({\n name,\n platform,\n knownFor: $p3(cells[2]).text().trim().substring(0, 100),\n priceRange: $p3(cells[3]).text().trim(),\n website: $p3(cells[4])?.find('a').attr('href') || ''\n });\n }\n }\n });\n console.log('P3 Suppliers:', p3Data.suppliers.length);\n \n // Discount Tiers\n $p3('.discount-tier').each((i, el) => {\n const label = $p3(el).find('span').first().text().trim();\n const values = $p3(el).find('span').last().text().trim();\n const match = values.match(/W:\\s*(\\d+)\\s*\\|\\s*Wm:\\s*(\\d+)/i);\n \n if (label && match) {\n p3Data.discountTiers.push({\n label,\n wayfair: parseInt(match[1]),\n walmart: parseInt(match[2])\n });\n }\n });\n console.log('P3 Discount Tiers:', p3Data.discountTiers.length);\n }\n} catch (e) {\n console.error('P3 Parse Error:', e.message);\n}\n\n// Return combined data\nreturn [{\n json: {\n categoryName: context.categoryName,\n dateFolderName: context.dateFolderName,\n templateHtml: context.templateHtml,\n p2Data,\n p3Data\n }\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2752,
0
],
"id": "1481c570-79c8-4b5e-95a2-de433ed10868",
"name": "Parse P2 & P3 Reports"
},
{
"parameters": {
"jsCode": "// ================================================================\n// BUILD COMPLETE DASHBOARD HTML\n// ================================================================\n\nconst data = $input.first().json;\nconst { categoryName, dateFolderName, templateHtml, p2Data, p3Data } = data;\n\nlet html = templateHtml;\n\n// Helper function for safe replacement\nconst replace = (placeholder, value) => {\n html = html.replace(new RegExp(placeholder.replace(/[{}]/g, '\\\\$&'), 'g'), value || '');\n};\n\n// ================================================================\n// BASIC INFO\n// ================================================================\nconst categoryTitle = categoryName.replace(/_/g, ' ').replace(/\\b\\w/g, l => l.toUpperCase());\nreplace('{{CATEGORY_TITLE}}', categoryTitle);\nreplace('{{CATEGORY_NAME}}', categoryTitle);\nreplace('{{REPORT_DATE}}', dateFolderName);\nreplace('{{GENERATED_AT}}', new Date().toISOString().split('T')[0]);\n\n// Product counts\nconst amazonProducts = p3Data.scope.amazonProducts || 0;\nconst walmartProducts = p3Data.scope.walmartProducts || 0;\nconst wayfairProducts = p3Data.scope.wayfairProducts || 0;\nconst totalProducts = amazonProducts + walmartProducts + wayfairProducts;\n\nreplace('{{AMAZON_PRODUCTS}}', amazonProducts.toString());\nreplace('{{WALMART_PRODUCTS}}', walmartProducts.toString());\nreplace('{{WAYFAIR_PRODUCTS}}', wayfairProducts.toString());\nreplace('{{TOTAL_PRODUCTS}}', totalProducts.toString());\n\n// ================================================================\n// KEY INSIGHTS\n// ================================================================\nreplace('{{TREND_KEY_INSIGHT}}', p2Data.keyInsight || 'Market trend data not available.');\nreplace('{{COMPETITIVE_KEY_INSIGHT}}', p3Data.keyInsight || 'Competitive analysis data not available.');\n\n// ================================================================\n// MARKET METRICS (P2)\n// ================================================================\nreplace('{{MARKET_SIZE_2025}}', p2Data.marketMetrics.size2025 || 'N/A');\nreplace('{{MARKET_SIZE_2033}}', p2Data.marketMetrics.size2033 || 'N/A');\nreplace('{{MARKET_CAGR}}', p2Data.marketMetrics.cagr || 'N/A');\n\n// ================================================================\n// TARGET SEGMENT (First segment from P2)\n// ================================================================\nconst targetSegment = p2Data.microSegments[0] || {};\nreplace('{{TARGET_SEGMENT_NAME}}', targetSegment.name || 'Emerging Segment');\nreplace('{{TARGET_SEGMENT_TIER}}', targetSegment.tier || 'Tier 1');\nreplace('{{TARGET_SEGMENT_USE_CASES}}', targetSegment.useCases || 'Various applications');\n\n// ================================================================\n// PRICE POSITIONING (P3)\n// ================================================================\nreplace('{{WAYFAIR_AVG_PRICE}}', p3Data.pricePositioning.wayfair.avg || 'N/A');\nreplace('{{WAYFAIR_MIN_PRICE}}', p3Data.pricePositioning.wayfair.min || 'N/A');\nreplace('{{WAYFAIR_MAX_PRICE}}', p3Data.pricePositioning.wayfair.max || 'N/A');\nreplace('{{AMAZON_AVG_PRICE}}', p3Data.pricePositioning.amazon.avg || 'N/A');\nreplace('{{AMAZON_MIN_PRICE}}', p3Data.pricePositioning.amazon.min || 'N/A');\nreplace('{{AMAZON_MAX_PRICE}}', p3Data.pricePositioning.amazon.max || 'N/A');\nreplace('{{WALMART_AVG_PRICE}}', p3Data.pricePositioning.walmart.avg || 'N/A');\nreplace('{{WALMART_MIN_PRICE}}', p3Data.pricePositioning.walmart.min || 'N/A');\nreplace('{{WALMART_MAX_PRICE}}', p3Data.pricePositioning.walmart.max || 'N/A');\n\n// ================================================================\n// PRICE BANDS (P3)\n// ================================================================\nreplace('{{BUDGET_WAYFAIR}}', p3Data.priceBands.budget?.wayfair || '0');\nreplace('{{BUDGET_AMAZON}}', p3Data.priceBands.budget?.amazon || '0');\nreplace('{{BUDGET_WALMART}}', p3Data.priceBands.budget?.walmart || '0');\nreplace('{{MID_WAYFAIR}}', p3Data.priceBands.mid?.wayfair || '0');\nreplace('{{MID_AMAZON}}', p3Data.priceBands.mid?.amazon || '0');\nreplace('{{MID_WALMART}}', p3Data.priceBands.mid?.walmart || '0');\nreplace('{{PREMIUM_WAYFAIR}}', p3Data.priceBands.premium?.wayfair || '0');\nreplace('{{PREMIUM_AMAZON}}', p3Data.priceBands.premium?.amazon || '0');\nreplace('{{PREMIUM_WALMART}}', p3Data.priceBands.premium?.walmart || '0');\n\n// ================================================================\n// RISK COUNTS (P2)\n// ================================================================\nconst highRisks = p2Data.risks.filter(r => r.severity === 'high').length;\nconst mediumRisks = p2Data.risks.filter(r => r.severity === 'medium').length;\nconst lowRisks = p2Data.risks.filter(r => r.severity === 'low').length;\nreplace('{{HIGH_RISKS}}', highRisks.toString());\nreplace('{{MEDIUM_RISKS}}', mediumRisks.toString());\nreplace('{{LOW_RISKS}}', lowRisks.toString());\n\n// ================================================================\n// SEGMENTS HTML (P2)\n// ================================================================\nlet segmentsHtml = '';\np2Data.microSegments.forEach((seg, i) => {\n segmentsHtml += `\n <div class=\"segment-card\">\n <div class=\"segment-header\">\n <span class=\"segment-number\">${i + 1}</span>\n <div class=\"segment-title\">${seg.name}</div>\n <span class=\"badge badge-${seg.tier === 'Tier 1' ? 'success' : 'info'}\">${seg.tier}</span>\n </div>\n <p class=\"segment-description\">${seg.description}</p>\n <div class=\"segment-meta\">\n ${seg.colors ? `<span>\ud83c\udfa8 ${seg.colors}</span>` : ''}\n ${seg.useCases ? `<span>\ud83c\udfe0 ${seg.useCases}</span>` : ''}\n </div>\n </div>`;\n});\nreplace('{{SEGMENTS_HTML}}', segmentsHtml || '<p class=\"empty-state\">No segment data available</p>');\n\n// ================================================================\n// COLOR PALETTE HTML (P2)\n// ================================================================\nlet colorPaletteHtml = '';\np2Data.colorPalette.forEach(color => {\n colorPaletteHtml += `\n <div class=\"color-swatch\">\n <div class=\"swatch-color\" style=\"background-color: ${color.hex};\"></div>\n <div class=\"swatch-info\">\n <span class=\"swatch-name\">${color.name}</span>\n <span class=\"swatch-hex\">${color.hex}</span>\n </div>\n </div>`;\n});\nreplace('{{COLOR_PALETTE_HTML}}', colorPaletteHtml || '<p class=\"empty-state\">No color data available</p>');\n\n// ================================================================\n// MOODBOARD HTML (P2)\n// ================================================================\nlet moodboardHtml = '';\np2Data.moodboardImages.forEach((src, i) => {\n moodboardHtml += `<div class=\"mood-image\"><img src=\"${src}\" alt=\"Mood ${i + 1}\" loading=\"lazy\"></div>`;\n});\nreplace('{{MOODBOARD_HTML}}', moodboardHtml || '<p class=\"empty-state\">No moodboard images available</p>');\n\n// ================================================================\n// RISKS HTML (P2)\n// ================================================================\nlet risksHtml = '';\np2Data.risks.forEach(risk => {\n risksHtml += `\n <div class=\"risk-card risk-${risk.severity}\">\n <div class=\"risk-header\">\n <h4>${risk.title}</h4>\n <span class=\"badge badge-${risk.severity === 'high' ? 'danger' : (risk.severity === 'low' ? 'success' : 'warning')}\">${risk.severity}</span>\n </div>\n <p>${risk.description}</p>\n ${risk.mitigation ? `<div class=\"risk-mitigation\"><strong>Mitigation:</strong> ${risk.mitigation}</div>` : ''}\n </div>`;\n});\nreplace('{{RISKS_HTML}}', risksHtml || '<p class=\"empty-state\">No risk data available</p>');\n\n// ================================================================\n// COMPARISON TABLE HTML (P3)\n// ================================================================\nlet comparisonTableHtml = '';\np3Data.comparisonTable.forEach(row => {\n const winnerClass = row.winner.toLowerCase().includes('wayfair') ? 'winner-wayfair' : \n (row.winner.toLowerCase().includes('amazon') ? 'winner-amazon' : 'winner-walmart');\n comparisonTableHtml += `\n <tr>\n <td><strong>${row.attribute}</strong></td>\n <td>${row.wayfair}</td>\n <td>${row.amazon}</td>\n <td>${row.walmart}</td>\n <td class=\"${winnerClass}\">${row.winner}</td>\n </tr>`;\n});\nreplace('{{COMPARISON_TABLE_HTML}}', comparisonTableHtml || '<tr><td colspan=\"5\">No comparison data available</td></tr>');\n\n// ================================================================\n// PRICE GAPS HTML (P3)\n// ================================================================\nlet priceGapsHtml = '';\np3Data.priceGaps.forEach(gap => {\n priceGapsHtml += `\n <div class=\"price-gap gap-${gap.level}\">\n <span class=\"gap-range\">${gap.range}</span>\n <span class=\"gap-description\">${gap.description}</span>\n </div>`;\n});\nreplace('{{PRICE_GAPS_HTML}}', priceGapsHtml || '<p class=\"empty-state\">No price gap data available</p>');\n\n// ================================================================\n// OPPORTUNITIES HTML (P3)\n// ================================================================\nlet opportunitiesHtml = '';\np3Data.opportunities.forEach(opp => {\n opportunitiesHtml += `\n <div class=\"opportunity-card priority-${opp.priority}\">\n <h4>${opp.title}</h4>\n <p>${opp.description}</p>\n <span class=\"badge badge-${opp.priority === 'high' ? 'danger' : 'warning'}\">${opp.priority} priority</span>\n </div>`;\n});\nreplace('{{OPPORTUNITIES_HTML}}', opportunitiesHtml || '<p class=\"empty-state\">No opportunity data available</p>');\n\n// ================================================================\n// QUICK WINS HTML (P3)\n// ================================================================\nlet quickWinsHtml = '';\np3Data.quickWins.forEach((win, i) => {\n quickWinsHtml += `\n <div class=\"action-card priority-${win.priority}\">\n <div class=\"action-number\">${i + 1}</div>\n <div class=\"action-content\">\n <h4>${win.title}</h4>\n <p>${win.description}</p>\n </div>\n </div>`;\n});\nreplace('{{QUICK_WINS_HTML}}', quickWinsHtml || '<p class=\"empty-state\">No quick wins available</p>');\n\n// ================================================================\n// STRATEGIC INITIATIVES HTML (P3)\n// ================================================================\nlet strategicHtml = '';\np3Data.strategicInitiatives.forEach((init, i) => {\n strategicHtml += `\n <div class=\"initiative-card priority-${init.priority}\">\n <div class=\"initiative-number\">${i + 1}</div>\n <div class=\"initiative-content\">\n <h4>${init.title}</h4>\n <p>${init.description}</p>\n </div>\n </div>`;\n});\nreplace('{{STRATEGIC_INITIATIVES_HTML}}', strategicHtml || '<p class=\"empty-state\">No strategic initiatives available</p>');\n\n// ================================================================\n// SUPPLIERS HTML (P3)\n// ================================================================\nlet suppliersHtml = '';\np3Data.suppliers.forEach(supplier => {\n suppliersHtml += `\n <tr>\n <td><strong>${supplier.name}</strong></td>\n <td><span class=\"badge badge-${supplier.platform.toLowerCase() === 'amazon' ? 'warning' : 'info'}\">${supplier.platform}</span></td>\n <td>${supplier.knownFor}</td>\n <td>${supplier.priceRange}</td>\n <td>${supplier.website ? `<a href=\"${supplier.website}\" target=\"_blank\">Visit</a>` : '-'}</td>\n </tr>`;\n});\nreplace('{{SUPPLIERS_HTML}}', suppliersHtml || '<tr><td colspan=\"5\">No supplier data available</td></tr>');\n\n// ================================================================\n// SUPPLIER CARDS HTML (for Key Suppliers section)\n// ================================================================\nlet supplierChipsHtml = '';\np3Data.suppliers.slice(0, 5).forEach(supplier => {\n const platformClass = supplier.platform.toLowerCase();\n const searchUrl = platformClass === 'amazon' \n ? `https://www.amazon.com/s?k=${encodeURIComponent(supplier.name)}` \n : platformClass === 'walmart' \n ? `https://www.walmart.com/search?q=${encodeURIComponent(supplier.name)}`\n : `https://www.wayfair.com/keyword.html?keyword=${encodeURIComponent(supplier.name)}`;\n supplierChipsHtml += `<a href=\"${searchUrl}\" target=\"_blank\" class=\"supplier-card\"><span class=\"supplier-name\">${supplier.name}</span><span class=\"platform ${platformClass}\">${supplier.platform}</span></a>`;\n});\nreplace('{{SUPPLIER_CHIPS_HTML}}', supplierChipsHtml || '<div class=\"empty-state\">No suppliers found</div>');\n\n// ================================================================\n// ATTRIBUTE BARS HTML (P2)\n// ================================================================\nconst buildBarsHtml = (bars) => {\n if (!bars || bars.length === 0) return '<p class=\"empty-state\">No data available</p>';\n return bars.map(bar => `\n <div class=\"bar-item\">\n <div class=\"bar-label\">${bar.label}</div>\n <div class=\"bar-track\">\n <div class=\"bar-fill\" style=\"width: ${bar.width}%;\"></div>\n </div>\n <div class=\"bar-value\">${bar.value}</div>\n </div>`).join('');\n};\n\nreplace('{{MATERIALS_BARS_HTML}}', buildBarsHtml(p2Data.attributeBars.materials));\nreplace('{{SIZES_BARS_HTML}}', buildBarsHtml(p2Data.attributeBars.sizes));\nreplace('{{COLORS_BARS_HTML}}', buildBarsHtml(p2Data.attributeBars.colors));\nreplace('{{PRICES_BARS_HTML}}', buildBarsHtml(p2Data.attributeBars.prices));\n\n// ================================================================\n// DISCOUNT TIERS HTML (P3)\n// ================================================================\nlet discountTiersHtml = '';\np3Data.discountTiers.forEach(tier => {\n discountTiersHtml += `\n <div class=\"discount-tier\">\n <span class=\"tier-label\">${tier.label}</span>\n <span class=\"tier-values\">W: ${tier.wayfair} | Wm: ${tier.walmart}</span>\n </div>`;\n});\nreplace('{{DISCOUNT_TIERS_HTML}}', discountTiersHtml || '<p class=\"empty-state\">No discount data available</p>');\n\n// ================================================================\n// ALERTS HTML (from risks and opportunities)\n// ================================================================\nlet alertsHtml = '';\nconst alertRisks = p2Data.risks.filter(r => r.severity === 'high').slice(0, 2);\nconst alertOpps = p3Data.opportunities.slice(0, 1);\nalertRisks.forEach(risk => {\n alertsHtml += `<div class=\"alert-card high\"><div class=\"alert-type\">Risk Alert</div><h4>${risk.title}</h4><p>${risk.description.substring(0, 100)}...</p></div>`;\n});\nalertOpps.forEach(opp => {\n alertsHtml += `<div class=\"alert-card opportunity\"><div class=\"alert-type\">Opportunity</div><h4>${opp.title}</h4><p>${opp.description.substring(0, 100)}...</p></div>`;\n});\nreplace('{{ALERTS_HTML}}', alertsHtml || '<div class=\"alert-card medium\"><div class=\"alert-type\">Info</div><h4>No alerts</h4><p>All metrics within normal range</p></div>');\n\n// ================================================================\n// TOP ACTIONS HTML\n// ================================================================\nlet topActionsHtml = '';\np3Data.quickWins.slice(0, 3).forEach((win, i) => {\n topActionsHtml += `<div class=\"quick-action\"><span class=\"action-num\">${i + 1}</span><span class=\"action-text\">${win.title}</span></div>`;\n});\nreplace('{{TOP_ACTIONS_HTML}}', topActionsHtml || '<div class=\"quick-action\"><span class=\"action-num\">!</span><span class=\"action-text\">Review full analysis for recommendations</span></div>');\n\n// ================================================================\n// TREND RECOMMENDATIONS HTML (P2)\n// ================================================================\nlet trendRecsHtml = '';\np2Data.recommendations.forEach((rec, i) => {\n trendRecsHtml += `\n <div class=\"rec-item\">\n <div class=\"rec-number\">${i + 1}</div>\n <div class=\"rec-content\">\n <h4>${rec.title}</h4>\n <p>${rec.description}</p>\n ${rec.impact ? `<span class=\"rec-impact\">${rec.impact}</span>` : ''}\n </div>\n </div>`;\n});\nreplace('{{TREND_RECOMMENDATIONS_HTML}}', trendRecsHtml || '<p class=\"empty-state\">No recommendations available</p>');\n\n// Generate filename\nconst fileName = `${categoryTitle.replace(/\\s+/g, '_')}_Dashboard_${dateFolderName}.html`;\n\nconsole.log('Dashboard built successfully!');\nconsole.log('File name:', fileName);\n\nreturn [{\n json: {\n html,\n fileName,\n categoryName,\n dateFolderName\n }\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2960,
0
],
"id": "95f59d8e-56a4-4836-86b7-e9a2a86b3496",
"name": "Build Dashboard HTML"
},
{
"parameters": {
"jsCode": "// Prepare HTML for download as binary file\nconst { html, fileName } = $input.first().json;\n\nconst buffer = Buffer.from(html, 'utf8');\nconst binaryData = {\n data: buffer.toString('base64'),\n mimeType: 'text/html',\n fileName: fileName\n};\n\nreturn [{\n json: { fileName },\n binary: { data: binaryData }\n}];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
3168,
0
],
"id": "37610383-bf81-4f8c-9592-c5a3f5d2f986",
"name": "Prepare Download"
}
],
"connections": {
"Run Manually": {
"main": [
[
{
"node": "Find Wayfair Reports Folder",
"type": "main",
"index": 0
}
]
]
},
"Find Wayfair Reports Folder": {
"main": [
[
{
"node": "List Root Contents",
"type": "main",
"index": 0
}
]
]
},
"List Root Contents": {
"main": [
[
{
"node": "Identify Category & Template",
"type": "main",
"index": 0
}
]
]
},
"Identify Category & Template": {
"main": [
[
{
"node": "Download Template",
"type": "main",
"index": 0
}
]
]
},
"Download Template": {
"main": [
[
{
"node": "Store Template",
"type": "main",
"index": 0
}
]
]
},
"Store Template": {
"main": [
[
{
"node": "List Date Folders",
"type": "main",
"index": 0
}
]
]
},
"List Date Folders": {
"main": [
[
{
"node": "Get Latest Date Only",
"type": "main",
"index": 0
}
]
]
},
"Get Latest Date Only": {
"main": [
[
{
"node": "List P2/P3 Files",
"type": "main",
"index": 0
}
]
]
},
"List P2/P3 Files": {
"main": [
[
{
"node": "Identify P2 & P3 Files",
"type": "main",
"index": 0
}
]
]
},
"Identify P2 & P3 Files": {
"main": [
[
{
"node": "Download P2",
"type": "main",
"index": 0
},
{
"node": "Download P3",
"type": "main",
"index": 0
}
]
]
},
"Download P2": {
"main": [
[
{
"node": "Rename P2 Binary",
"type": "main",
"index": 0
}
]
]
},
"Download P3": {
"main": [
[
{
"node": "Rename P3 Binary",
"type": "main",
"index": 0
}
]
]
},
"Rename P2 Binary": {
"main": [
[
{
"node": "Merge P2 & P3",
"type": "main",
"index": 0
}
]
]
},
"Rename P3 Binary": {
"main": [
[
{
"node": "Merge P2 & P3",
"type": "main",
"index": 1
}
]
]
},
"Merge P2 & P3": {
"main": [
[
{
"node": "Parse P2 & P3 Reports",
"type": "main",
"index": 0
}
]
]
},
"Parse P2 & P3 Reports": {
"main": [
[
{
"node": "Build Dashboard HTML",
"type": "main",
"index": 0
}
]
]
},
"Build Dashboard HTML": {
"main": [
[
{
"node": "Prepare Download",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1",
"binaryMode": "separate"
},
"versionId": "5cce026d-2c40-4abc-9af4-eb4122aebd5f",
"meta": {
"templateCredsSetupCompleted": true
},
"id": "PlD984qyDOtIYHhZ",
"tags": []
}
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.
googleDriveOAuth2Api
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Project 5. Uses googleDrive. Event-driven trigger; 18 nodes.
Source: https://github.com/lungania/wayfair-market-intelligence-AI-agents/blob/main/workflows/Dashboard.json — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
CLEAN Agent - Manual Trigger. Uses googleDrive, googleSheets, httpRequest. Event-driven trigger; 49 nodes.
🤖🧑💻 AI Agent for Top n8n Creators Leaderboard Reporting. Uses httpRequest, executeWorkflowTrigger, readWriteFile, googleDrive. Event-driven trigger; 49 nodes.
💥 AI Image to Professional Video Workflow using NanoBanana Ultra & Kling AI. Uses googleSheets, googleDrive, httpRequest, editImage. Event-driven trigger; 45 nodes.
🤹🤖 This workflow (AI Document Generator with Anthropic Agent Skills and Uploading to Google Drive) automates the process of generating, downloading, and storing professionally formatted files (PDF, DO
Voice RAG Chatbot with ElevenLabs and OpenAI. Uses httpRequest, googleDrive. Event-driven trigger; 23 nodes.