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
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
Automatically analyze your full sports performance evolution using your Strava activities, enriched with AI insights and delivered directly to your email — all powered by your own n8n instance.