This workflow corresponds to n8n.io template #10154 — we link there as the canonical source.
This workflow follows the Gmail → Google Sheets recipe pattern — see all workflows that pair these two integrations.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"id": "wesSFaik8lD7g9lq",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "Monitor Competitor Prices with Firecrawl, GPT-4.1 & Send Alerts to Gmail",
"tags": [
{
"id": "7zsdOA50QGm7RNqx",
"name": "Monitoring",
"createdAt": "2025-10-23T16:41:17.031Z",
"updatedAt": "2025-10-23T16:41:17.031Z"
},
{
"id": "BL8TsHYj5FkNYzfi",
"name": "E-commerce",
"createdAt": "2025-10-23T16:41:16.985Z",
"updatedAt": "2025-10-23T16:41:16.985Z"
},
{
"id": "dlf9zFSN3j6s2jgO",
"name": "Business Intelligence",
"createdAt": "2025-10-23T16:41:17.008Z",
"updatedAt": "2025-10-23T16:41:17.008Z"
},
{
"id": "lpozR2Ct8reF9bCk",
"name": "AI",
"createdAt": "2025-10-23T16:41:17.062Z",
"updatedAt": "2025-10-23T16:41:17.062Z"
}
],
"nodes": [
{
"id": "1a7684b3-eb26-414f-ad30-e613306b50b1",
"name": "\ud83d\udcca Read Historical Data",
"type": "n8n-nodes-base.googleSheets",
"notes": "Loads previous scan data for comparison",
"position": [
-1472,
176
],
"parameters": {
"options": {},
"sheetName": {
"mode": "name",
"value": "Historical Data"
},
"documentId": {
"__rl": true,
"mode": "id",
"value": "={{ $env.GOOGLE_SHEET_ID }}"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.7
},
{
"id": "9239f63d-2717-4248-918a-b886016c9a98",
"name": "\ud83d\udd00 Merge Current with Historical",
"type": "n8n-nodes-base.merge",
"notes": "Combines current scrape with historical data for comparison",
"position": [
-1296,
0
],
"parameters": {
"mode": "combine",
"options": {},
"fieldsToMatchString": "rawResponse.message.content"
},
"typeVersion": 3
},
{
"id": "3b453791-3aca-4563-97d7-34d1bc751824",
"name": "\ud83d\udd0d Detect Price & Stock Changes",
"type": "n8n-nodes-base.code",
"notes": "Intelligent change detection with alert level classification",
"position": [
-1120,
0
],
"parameters": {
"jsCode": "// Compare current prices with historical and detect changes\nconst results = [];\n\nfor (const item of $input.all()) {\n const current = item.json;\n \n // Skip error items\n if (current.error) {\n results.push({ json: current });\n continue;\n }\n \n // Find historical data for this competitor\n const historical = $('\ud83d\udcca Read Historical Data').all()\n .find(h => h.json.competitorName === current.competitorName);\n \n let alertLevel = 'none';\n let changes = [];\n \n if (historical && historical.json.currentPrice) {\n const oldPrice = parseFloat(historical.json.currentPrice);\n const newPrice = parseFloat(current.currentPrice);\n const priceChange = newPrice - oldPrice;\n const priceChangePercent = ((priceChange / oldPrice) * 100).toFixed(2);\n \n current.priceChange = priceChange;\n current.priceChangePercent = parseFloat(priceChangePercent);\n current.previousPrice = oldPrice;\n \n // Determine alert level based on price changes\n if (Math.abs(priceChangePercent) >= 20) {\n alertLevel = 'critical';\n changes.push(`Price ${priceChange > 0 ? 'increased' : 'decreased'} by ${Math.abs(priceChangePercent)}%`);\n } else if (Math.abs(priceChangePercent) >= 10) {\n alertLevel = 'warning';\n changes.push(`Price ${priceChange > 0 ? 'increased' : 'decreased'} by ${Math.abs(priceChangePercent)}%`);\n } else if (Math.abs(priceChangePercent) >= 5) {\n alertLevel = 'info';\n changes.push(`Minor price change: ${priceChangePercent}%`);\n }\n \n // Check if it's a new low price\n const historicalLow = parseFloat(historical.json.lowestPrice || oldPrice);\n if (newPrice < historicalLow) {\n current.isNewLow = true;\n alertLevel = alertLevel === 'none' ? 'info' : alertLevel;\n changes.push('\ud83c\udfaf NEW LOWEST PRICE!');\n }\n current.lowestPrice = Math.min(newPrice, historicalLow);\n \n // Stock level changes\n if (historical.json.stockLevel !== current.stockLevel) {\n if (current.stockLevel === 'Out of Stock') {\n alertLevel = 'critical';\n changes.push('\ud83d\udce6 Product went OUT OF STOCK');\n } else if (current.stockLevel === 'Low Stock') {\n alertLevel = alertLevel === 'none' ? 'warning' : alertLevel;\n changes.push('\u26a0\ufe0f Stock level is LOW');\n } else if (historical.json.stockLevel === 'Out of Stock' && current.inStock) {\n alertLevel = alertLevel === 'none' ? 'info' : alertLevel;\n changes.push('\u2705 Back in stock!');\n }\n }\n \n // Rating changes\n const oldRating = parseFloat(historical.json.rating || 0);\n const newRating = parseFloat(current.rating || 0);\n const ratingChange = newRating - oldRating;\n \n if (Math.abs(ratingChange) >= 0.5) {\n alertLevel = alertLevel === 'none' ? 'info' : alertLevel;\n changes.push(`\u2b50 Rating ${ratingChange > 0 ? 'improved' : 'dropped'} by ${Math.abs(ratingChange).toFixed(1)} stars`);\n }\n \n // Review count changes\n const oldReviews = parseInt(historical.json.reviewCount || 0);\n const newReviews = parseInt(current.reviewCount || 0);\n const reviewDiff = newReviews - oldReviews;\n \n if (reviewDiff > 0) {\n changes.push(`\ud83d\udcac ${reviewDiff} new review${reviewDiff > 1 ? 's' : ''}`);\n }\n } else {\n // First time seeing this competitor\n alertLevel = 'info';\n changes.push('\ud83c\udd95 First time tracking this competitor');\n current.lowestPrice = current.currentPrice;\n }\n \n current.alertLevel = alertLevel;\n current.changesSummary = changes.join(' | ');\n current.hasChanges = alertLevel !== 'none';\n \n results.push({ json: current });\n}\n\nreturn results;"
},
"typeVersion": 2
},
{
"id": "c83f80f9-8954-406e-a43a-20299f94d4ef",
"name": "\ud83d\udcbe Update Historical Data",
"type": "n8n-nodes-base.googleSheets",
"notes": "Saves current data to historical tracking sheet",
"position": [
-944,
80
],
"parameters": {
"columns": {
"value": {
"rating": "={{ $json.rating }}",
"inStock": "={{ $json.inStock }}",
"currency": "={{ $json.currency }}",
"scrapedAt": "={{ $json.scrapedAt }}",
"productUrl": "={{ $json.productUrl }}",
"stockLevel": "={{ $json.stockLevel }}",
"lowestPrice": "={{ $json.lowestPrice }}",
"productName": "={{ $json.productName }}",
"reviewCount": "={{ $json.reviewCount }}",
"currentPrice": "={{ $json.currentPrice }}",
"originalPrice": "={{ $json.originalPrice }}",
"competitorName": "={{ $json.competitorName }}"
},
"mappingMode": "defineBelow"
},
"options": {},
"operation": "append",
"sheetName": {
"mode": "name",
"value": "Historical Data"
},
"documentId": {
"__rl": true,
"mode": "id",
"value": "= {{ $env.GOOGLE_SHEET_ID }}"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.7
},
{
"id": "c1ba6419-781d-4c2d-bfa6-dfe202751e19",
"name": "\ud83d\udcdd Log Alert Details",
"type": "n8n-nodes-base.googleSheets",
"notes": "Logs all alerts to separate tracking sheet",
"position": [
-944,
-80
],
"parameters": {
"columns": {
"value": {
"rating": "={{ $json.rating }}",
"timestamp": "={{ $json.scrapedAt }}",
"alertLevel": "={{ $json.alertLevel }}",
"productUrl": "={{ $json.productUrl }}",
"stockLevel": "={{ $json.stockLevel }}",
"priceChange": "={{ $json.priceChange || 0 }}",
"productName": "={{ $json.productName }}",
"currentPrice": "={{ $json.currentPrice }}",
"previousPrice": "={{ $json.previousPrice || 'N/A' }}",
"changesSummary": "={{ $json.changesSummary }}",
"competitorName": "={{ $json.competitorName }}",
"priceChangePercent": "={{ $json.priceChangePercent || 0 }}"
},
"mappingMode": "defineBelow"
},
"options": {},
"operation": "append",
"sheetName": {
"mode": "name",
"value": "Alert Log"
},
"documentId": {
"__rl": true,
"mode": "id",
"value": "={{ $env.GOOGLE_SHEET_ID }}"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.7
},
{
"id": "233f07c7-c121-4e14-abf1-0a917b023a11",
"name": "\ud83d\udcca Aggregate Daily Digest",
"type": "n8n-nodes-base.code",
"notes": "Combines all alerts into a comprehensive summary",
"position": [
-944,
-240
],
"parameters": {
"jsCode": "// Aggregate all items for daily digest email\nconst allItems = $input.all();\n\nconst criticalAlerts = allItems.filter(item => item.json.alertLevel === 'critical');\nconst warningAlerts = allItems.filter(item => item.json.alertLevel === 'warning');\nconst infoAlerts = allItems.filter(item => item.json.alertLevel === 'info');\nconst noChanges = allItems.filter(item => item.json.alertLevel === 'none');\n\nconst summary = {\n totalCompetitors: allItems.length,\n criticalCount: criticalAlerts.length,\n warningCount: warningAlerts.length,\n infoCount: infoAlerts.length,\n noChangeCount: noChanges.length,\n timestamp: new Date().toISOString(),\n criticalAlerts: criticalAlerts.map(i => ({\n competitor: i.json.competitorName,\n product: i.json.productName,\n changes: i.json.changesSummary,\n price: `${i.json.currency} ${i.json.currentPrice}`,\n priceChange: i.json.priceChangePercent ? `${i.json.priceChangePercent}%` : 'N/A',\n stock: i.json.stockLevel,\n url: i.json.productUrl\n })),\n warningAlerts: warningAlerts.map(i => ({\n competitor: i.json.competitorName,\n product: i.json.productName,\n changes: i.json.changesSummary,\n price: `${i.json.currency} ${i.json.currentPrice}`,\n priceChange: i.json.priceChangePercent ? `${i.json.priceChangePercent}%` : 'N/A',\n stock: i.json.stockLevel\n })),\n infoAlerts: infoAlerts.map(i => ({\n competitor: i.json.competitorName,\n product: i.json.productName,\n changes: i.json.changesSummary\n }))\n};\n\nreturn [{ json: summary }];"
},
"typeVersion": 2
},
{
"id": "bb79bf76-29bd-44d9-8064-48c14622e1f5",
"name": "Scrape URL: nike.com",
"type": "@mendable/n8n-nodes-firecrawl.firecrawl",
"position": [
-1904,
-176
],
"parameters": {
"url": "https://www.nike.com/sg/w/mens-shoes-nik1zy7ok",
"operation": "scrape",
"scrapeOptions": {
"options": {
"formats": {
"format": [
{
"type": "json",
"prompt": "price of the shoe"
}
]
},
"headers": {}
}
},
"requestOptions": {}
},
"credentials": {
"firecrawlApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "905a1a25-92aa-4459-8d4e-8392d6ca4e61",
"name": "When clicking \u2018Execute workflow\u2019",
"type": "n8n-nodes-base.manualTrigger",
"position": [
-2096,
32
],
"parameters": {},
"typeVersion": 1
},
{
"id": "fb7b71da-f70d-48d8-afef-ee42bdb48567",
"name": "Send a message",
"type": "n8n-nodes-base.gmail",
"position": [
-800,
-240
],
"parameters": {
"sendTo": " info@example.com",
"message": "The pricing of the competitors is attached",
"options": {},
"subject": "Shoes pricing"
},
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
},
"typeVersion": 2.1
},
{
"id": "5c08b0c2-5bc1-4594-be36-938e67308a1f",
"name": "Scrape URL: adidas.com",
"type": "@mendable/n8n-nodes-firecrawl.firecrawl",
"position": [
-1904,
-16
],
"parameters": {
"url": "=https://www.adidas.com/us/men-shoes",
"operation": "scrape",
"scrapeOptions": {
"options": {
"formats": {
"format": [
{
"type": "json",
"prompt": "price of the shoe"
}
]
},
"headers": {}
}
},
"requestOptions": {}
},
"credentials": {
"firecrawlApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "57ac5f72-cc18-4919-bdea-4e16939b8080",
"name": "Scrape URL: sneakerpricer.com",
"type": "@mendable/n8n-nodes-firecrawl.firecrawl",
"position": [
-1904,
144
],
"parameters": {
"url": "=https://www.sneakerpricer.com/us-EN",
"operation": "scrape",
"scrapeOptions": {
"options": {
"formats": {
"format": [
{
"type": "json",
"prompt": "price of the shoe"
}
]
},
"headers": {}
}
},
"requestOptions": {}
},
"credentials": {
"firecrawlApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "5eb1ffd8-d98d-4682-b200-92ab2432fd81",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
-2656,
-400
],
"parameters": {
"width": 2032,
"height": 880,
"content": "## Introduction\nAutomate price monitoring for e-commerce competitors\u2014ideal for retailers, analysts, and pricing teams.\n\n**\u26a0\ufe0f Self-Hosted Only:** Requires self-hosted n8n instance.\n## How It Works\nScrapes competitor URLs, extracts data via AI, detects price/stock changes, logs to Google Sheets with email alerts.\n## Workflow Template\nTrigger \u2192 Scrape \u2192 AI Extract \u2192 Parse \u2192 Compare \u2192 Detect Changes \u2192 Update Sheets + Alert\n## Workflow Steps\n1. **Scraping:** Firecrawl fetches Nike, Adidas, Sneaker data\n2. **AI Extraction:** Processes product details\n3. **Parsing:** Structures response\n4. **Historical Check:** Reads Sheets data\n5. **Change Detection:** Identifies price/stock updates\n6. **Dual Output:** Updates Sheets + sends alerts\n## Setup Instructions\n1. **Firecrawl API**\nGet key from dashboard \u2192 Add to n8n\n2. **OpenAI API**\nGet key from platform \u2192 Add to n8n\n3. **Google Sheets OAuth2**\nCreate OAuth2 in Google Cloud Console \u2192 Authorize in n8n \u2192 Enable API\n4. **Gmail OAuth2**\nUse same project \u2192 Authorize in n8n \u2192 Enable API\n5. **Spreadsheet Setup**\nCreate Sheet with required columns \u2192 Copy ID from URL \u2192 Paste in workflow\n## Prerequisites\nSelf-hosted n8n, Firecrawl account, OpenAI key, Google account (Sheets + Gmail OAuth2)\n## Customization\nAdd URLs, adjust thresholds, integrate Slack\n## Benefits\nSaves 2+ hours daily, real-time tracking, automated alerts-time competitor tracking, automated alerts, historical data analysis."
},
"typeVersion": 1
},
{
"id": "738fd9c2-91ff-4f15-b7a3-1aa0e4c1f8a1",
"name": "Converts unstructured AI text into organized, usable data fields",
"type": "n8n-nodes-base.code",
"notes": "Parses and validates the AI extracted data",
"position": [
-1504,
-16
],
"parameters": {
"jsCode": "// Parse AI response and clean data\nconst items = [];\n\nfor (const item of $input.all()) {\n try {\n // Parse the AI response\n let parsed;\n const response = item.json.choices?.[0]?.message?.content || item.json.message || '';\n \n // Remove markdown code blocks if present\n const cleaned = response.replace(/```json\\n?|```\\n?/g, '').trim();\n \n try {\n parsed = JSON.parse(cleaned);\n } catch (e) {\n // Try to extract JSON from the response\n const jsonMatch = cleaned.match(/\\{[\\s\\S]*\\}/);\n if (jsonMatch) {\n parsed = JSON.parse(jsonMatch[0]);\n } else {\n throw new Error('Could not parse JSON from AI response');\n }\n }\n \n // Enrich with metadata\n items.push({\n json: {\n ...parsed,\n scrapedAt: new Date().toISOString(),\n priceChange: 0, // Will be calculated in comparison\n priceChangePercent: 0,\n isNewLow: false,\n alertLevel: 'none'\n }\n });\n } catch (error) {\n console.error('Failed to parse item:', error.message);\n // Add error item for debugging\n items.push({\n json: {\n error: error.message,\n rawResponse: item.json,\n competitorName: 'Parse Error',\n scrapedAt: new Date().toISOString()\n }\n });\n }\n}\n\nreturn items;"
},
"typeVersion": 2
},
{
"id": "2a1d727b-8147-42e7-bfcd-c3ed37af7bc7",
"name": "\ud83e\udd16 AI Extract Product Data using GPT-4.1-mini",
"type": "n8n-nodes-base.openAi",
"notes": "Uses OpenAI to intelligently extract structured data from HTML",
"position": [
-1696,
-16
],
"parameters": {
"prompt": {
"messages": [
{
"role": "system",
"content": "You are a precise e-commerce data extraction expert. Extract shoes information from HTML and return ONLY valid JSON with no markdown formatting.\n\nExtract these fields:\n- productName: string\n- currentPrice: number (numeric value only, no currency symbols)\n- originalPrice: number (if discounted, otherwise same as currentPrice)\n- currency: string (USD, EUR, etc.)\n- inStock: boolean\n- stockLevel: string (\"In Stock\", \"Low Stock\", \"Out of Stock\", \"Limited\", etc.)\n- rating: number (0-5 scale)\n- reviewCount: number\n- lastUpdated: string (current ISO timestamp)\n- productUrl: string (from context)\n- competitorName: string (from context)\n\nReturn ONLY the JSON object, no explanations."
},
{
"content": "HTML Content:\n{{ $json.body }}\n\nProduct URL: {{ $json.url || 'unknown' }}\nCompetitor: {{ $json.competitor || 'unknown' }}\n\nExtract the product data as JSON:"
}
]
},
"options": {
"temperature": 0.1
},
"resource": "chat",
"chatModel": "gpt-4.1-mini",
"requestOptions": {}
},
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.1
},
{
"id": "652a2128-6e01-4d6e-8bcb-b3f844cec2a8",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1584,
-352
],
"parameters": {
"color": 6,
"width": 352,
"height": 240,
"content": "## Google Sheets Structure\n**Required Columns:**\n- **Product Name** (Column A)\n- **Current Price** (Column B)\n- **Previous Price** (Column C)\n- **Stock Status** (Column D)\n- **Last Updated** (Column E)\n- **URL** (Column F)\n- **Change Detected** (Column G)"
},
"typeVersion": 1
}
],
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "703c4545-13fd-46b1-9a69-7e8c6ec4c656",
"connections": {
"Send a message": {
"main": [
[]
]
},
"Scrape URL: nike.com": {
"main": [
[
{
"node": "\ud83e\udd16 AI Extract Product Data using GPT-4.1-mini",
"type": "main",
"index": 0
}
]
]
},
"Scrape URL: adidas.com": {
"main": [
[
{
"node": "\ud83e\udd16 AI Extract Product Data using GPT-4.1-mini",
"type": "main",
"index": 0
}
]
]
},
"\ud83d\udcca Read Historical Data": {
"main": [
[
{
"node": "\ud83d\udd00 Merge Current with Historical",
"type": "main",
"index": 1
}
]
]
},
"\ud83d\udcca Aggregate Daily Digest": {
"main": [
[
{
"node": "Send a message",
"type": "main",
"index": 0
}
]
]
},
"Scrape URL: sneakerpricer.com": {
"main": [
[
{
"node": "\ud83e\udd16 AI Extract Product Data using GPT-4.1-mini",
"type": "main",
"index": 0
}
]
]
},
"\ud83d\udd0d Detect Price & Stock Changes": {
"main": [
[
{
"node": "\ud83d\udcca Aggregate Daily Digest",
"type": "main",
"index": 0
},
{
"node": "\ud83d\udcbe Update Historical Data",
"type": "main",
"index": 0
},
{
"node": "\ud83d\udcdd Log Alert Details",
"type": "main",
"index": 0
}
]
]
},
"\ud83d\udd00 Merge Current with Historical": {
"main": [
[
{
"node": "\ud83d\udd0d Detect Price & Stock Changes",
"type": "main",
"index": 0
}
]
]
},
"When clicking \u2018Execute workflow\u2019": {
"main": [
[
{
"node": "Scrape URL: nike.com",
"type": "main",
"index": 0
},
{
"node": "Scrape URL: adidas.com",
"type": "main",
"index": 0
},
{
"node": "Scrape URL: sneakerpricer.com",
"type": "main",
"index": 0
}
]
]
},
"\ud83e\udd16 AI Extract Product Data using GPT-4.1-mini": {
"main": [
[
{
"node": "Converts unstructured AI text into organized, usable data fields",
"type": "main",
"index": 0
}
]
]
},
"Converts unstructured AI text into organized, usable data fields": {
"main": [
[
{
"node": "\ud83d\udd00 Merge Current with Historical",
"type": "main",
"index": 0
}
]
]
}
}
}
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.
firecrawlApigmailOAuth2googleSheetsOAuth2ApiopenAiApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Automate price monitoring for e-commerce competitors—ideal for retailers, analysts, and pricing teams. ⚠️ Self-Hosted Only: Requires self-hosted n8n instance.
Source: https://n8n.io/workflows/10154/ — 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.
This intelligent email automation workflow helps you maximize engagement through domain-based outreach. It utilizes AI-powered personalization and strategic follow-ups to increase response rates. The
Complete AI-powered sales system Automates lead capture, qualification, and follow-up from multiple channels. AI INTELLIGENCE:
Send a target niche and location via Telegram message Workflow discovers businesses via Google Maps API AI enriches contacts with email and LinkedIn data via Serper GPT-4o scores and qualifies each le
LeadInboxTriageBot_GT. Uses gmailTrigger, openAi, googleSheets, gmail. Event-driven trigger; 36 nodes.
This workflow is designed for SEO professionals, digital agencies, content creators, and WordPress site owners who want to improve their search engine rankings automatically. It’s also perfect for cur