This workflow follows the Execute Workflow Trigger → 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 →
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.executeWorkflowTrigger",
"typeVersion": 1.1,
"position": [
-600,
300
],
"id": "pc100001-0001-0001-0001-000000000001",
"name": "Execute Workflow Trigger"
},
{
"parameters": {
"jsCode": "const input = $input.first().json || {};\nreturn [{ json: {\n sheetId: input.sheetId || '1cD3xVEhfP5Iy-PwumXymLctXgI1X_mbEUlhjHDZPhto', // YOUR_GOOGLE_SHEET_ID \u2014 Steward_Deals\n trackingSheetName: input.trackingSheetName || 'Tracked Prices',\n historySheetName: input.historySheetName || 'Price History',\n currency: input.currency || 'CHF'\n} }];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-400,
300
],
"id": "pc100001-0001-0001-0001-000000000002",
"name": "Config"
},
{
"parameters": {
"documentId": {
"__rl": true,
"value": "={{ $('Config').first().json.sheetId }}",
"mode": "id"
},
"sheetName": {
"__rl": true,
"value": "={{ $('Config').first().json.trackingSheetName }}",
"mode": "name"
},
"options": {}
},
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
-180,
300
],
"id": "pc100001-0001-0001-0001-000000000003",
"name": "Load Tracked",
"alwaysOutputData": true,
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "const items = $input.all();\nconst active = items.filter(i => i.json.url && (i.json.status || '').toLowerCase() === 'active');\n\nif (active.length === 0) {\n return [{ json: { empty: true } }];\n}\n\nreturn active;"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
40,
300
],
"id": "pc100001-0001-0001-0001-000000000004",
"name": "Filter Active"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "check-empty",
"leftValue": "={{ $json.empty }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "equals"
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
260,
300
],
"id": "pc100001-0001-0001-0001-000000000005",
"name": "Empty?"
},
{
"parameters": {
"jsCode": "return [{ json: { priceReport: [], priceSection: '' } }];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
500,
220
],
"id": "pc100001-0001-0001-0001-000000000006",
"name": "Return Empty"
},
{
"parameters": {
"batchSize": 1,
"options": {}
},
"type": "n8n-nodes-base.splitInBatches",
"typeVersion": 3,
"position": [
500,
380
],
"id": "pc100001-0001-0001-0001-000000000007",
"name": "Loop Items"
},
{
"parameters": {
"url": "={{ $json.url }}",
"options": {
"response": {
"response": {
"fullResponse": true,
"responseFormat": "text"
}
},
"timeout": 15000
}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
720,
460
],
"id": "pc100001-0001-0001-0001-000000000008",
"name": "Fetch Page",
"onError": "continueRegularOutput"
},
{
"parameters": {
"jsCode": "const html = $input.first().json.body || $input.first().json.data || '';\nconst tracked = $('Loop Items').first().json;\nconst currency = $('Config').first().json.currency;\nconst url = tracked.url;\n\nlet newPrice = null;\n\n// Strategy 1: JSON-LD structured data\nconst jsonLdBlocks = html.match(/<script[^>]*type=[\"']application\\/ld\\+json[\"'][^>]*>[\\s\\S]*?<\\/script>/gi) || [];\nfor (const block of jsonLdBlocks) {\n try {\n const jsonStr = block.replace(/<script[^>]*>/, '').replace(/<\\/script>/, '');\n const ld = JSON.parse(jsonStr);\n const items = Array.isArray(ld) ? ld : [ld];\n for (const item of items) {\n if (item['@type'] === 'Product' || (item['@type']?.includes?.('Product'))) {\n const offers = item.offers;\n if (offers) {\n const offer = Array.isArray(offers) ? offers[0] : offers;\n if (offer.price) newPrice = parseFloat(offer.price);\n else if (offer.lowPrice) newPrice = parseFloat(offer.lowPrice);\n }\n }\n }\n } catch (e) { /* skip */ }\n}\n\n// Strategy 2: Open Graph meta\nif (newPrice === null) {\n const ogPrice = html.match(/<meta[^>]*property=[\"']product:price:amount[\"'][^>]*content=[\"']([^\"']+)[\"']/i);\n if (ogPrice) newPrice = parseFloat(ogPrice[1]);\n}\n\n// Strategy 3: Regex for Swiss price patterns\nif (newPrice === null) {\n const priceMatch = html.match(/(\\d+[.,]\\d{2})\\s*(?:CHF|\\.\\u2013)|CHF\\s*(\\d+[.,]\\d{2})|(?:CHF|Fr\\.)\\s*(\\d+)\\.\\u2013/);\n if (priceMatch) {\n const priceStr = priceMatch[1] || priceMatch[2] || priceMatch[3];\n newPrice = parseFloat(priceStr.replace(',', '.'));\n }\n}\n\nconst oldPrice = tracked.current_price != null && tracked.current_price !== '' ? Number(tracked.current_price) : null;\nconst today = new Date().toISOString().split('T')[0];\n\n// Determine change\nlet priceChanged = false;\nlet direction = 'unchanged';\nif (newPrice !== null && oldPrice !== null && newPrice !== oldPrice) {\n priceChanged = true;\n direction = newPrice < oldPrice ? 'down' : 'up';\n}\n\n// Update extremes\nlet lowestPrice = tracked.lowest_price != null && tracked.lowest_price !== '' ? Number(tracked.lowest_price) : null;\nlet highestPrice = tracked.highest_price != null && tracked.highest_price !== '' ? Number(tracked.highest_price) : null;\nif (newPrice !== null) {\n if (lowestPrice === null || newPrice < lowestPrice) lowestPrice = newPrice;\n if (highestPrice === null || newPrice > highestPrice) highestPrice = newPrice;\n}\n\n// Determine notification\nconst notifyMode = tracked.notify_mode || 'always';\nlet shouldNotify = false;\nif (newPrice !== null) {\n if (notifyMode === 'always') shouldNotify = true;\n else if (notifyMode === 'on_change') shouldNotify = priceChanged;\n else if (notifyMode === 'threshold') {\n const threshold = tracked.price_threshold != null && tracked.price_threshold !== '' ? Number(tracked.price_threshold) : null;\n shouldNotify = threshold !== null && newPrice <= threshold;\n }\n}\n\nreturn [{\n json: {\n url,\n product_name: tracked.product_name,\n domain: tracked.domain,\n old_price: oldPrice,\n new_price: newPrice,\n currency,\n previous_price: oldPrice,\n current_price: newPrice !== null ? newPrice : oldPrice,\n lowest_price: lowestPrice,\n highest_price: highestPrice,\n last_checked: today,\n priceChanged,\n direction,\n shouldNotify,\n notify_mode: notifyMode\n }\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
940,
460
],
"id": "pc100001-0001-0001-0001-000000000009",
"name": "Extract & Compare"
},
{
"parameters": {
"operation": "append",
"documentId": {
"__rl": true,
"value": "={{ $('Config').first().json.sheetId }}",
"mode": "id"
},
"sheetName": {
"__rl": true,
"value": "={{ $('Config').first().json.historySheetName }}",
"mode": "name"
},
"columns": {
"mappingMode": "defineBelow",
"value": {
"date": "={{ $json.last_checked }}",
"url": "={{ $json.url }}",
"product_name": "={{ $json.product_name }}",
"price": "={{ $json.new_price }}",
"currency": "={{ $json.currency }}"
}
},
"options": {}
},
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
1080,
460
],
"id": "pc100001-0001-0001-0001-000000000014",
"name": "Append History",
"onError": "continueRegularOutput",
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"content": "Appends to Price History tab:\ndate, url, product_name, price, currency\nonError: continueRegularOutput",
"height": 80,
"width": 240,
"color": 5
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
1040,
410
],
"id": "pc100001-0001-0001-0001-000000000015",
"name": "Sticky Note - Append History"
},
{
"parameters": {
"operation": "update",
"documentId": {
"__rl": true,
"value": "={{ $('Config').first().json.sheetId }}",
"mode": "id"
},
"sheetName": {
"__rl": true,
"value": "={{ $('Config').first().json.trackingSheetName }}",
"mode": "name"
},
"columns": {
"mappingMode": "defineBelow",
"value": {
"current_price": "={{ $json.current_price }}",
"previous_price": "={{ $json.previous_price }}",
"lowest_price": "={{ $json.lowest_price }}",
"highest_price": "={{ $json.highest_price }}",
"last_checked": "={{ $json.last_checked }}"
}
},
"options": {
"cellFormat": "USER_ENTERED",
"handlingExtraData": "ignoreIt"
},
"filtersUI": {
"values": [
{
"lookupColumn": "url",
"lookupValue": "={{ $json.url }}"
}
]
}
},
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
1300,
460
],
"id": "pc100001-0001-0001-0001-000000000010",
"name": "Update Row",
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"aggregate": "aggregateAllItemData",
"options": {}
},
"type": "n8n-nodes-base.aggregate",
"typeVersion": 1,
"position": [
1600,
300
],
"id": "pc100001-0001-0001-0001-000000000011",
"name": "Collect Results"
},
{
"parameters": {
"jsCode": "const results = $input.first().json.data || [];\nconst toNotify = results.filter(r => r.shouldNotify);\n\nif (toNotify.length === 0) {\n return [{ json: { priceReport: [], priceSection: '' } }];\n}\n\nconst lines = toNotify.map(r => {\n if (!r.priceChanged) {\n return ` ${r.product_name}: ${r.new_price} ${r.currency} \\u27a1\\ufe0f`;\n }\n const pct = (((r.new_price - r.old_price) / r.old_price) * 100).toFixed(1);\n const arrow = r.direction === 'down' ? '\\ud83d\\udcc9' : '\\ud83d\\udcc8';\n return ` ${r.product_name}: ${r.old_price} \\u2192 ${r.new_price} ${r.currency} ${arrow} (${pct > 0 ? '+' : ''}${pct}%)`;\n});\n\nconst priceSection = `\\n\\n\\ud83d\\udccd *Price Tracker* (${toNotify.length} item${toNotify.length === 1 ? '' : 's'})\\n${lines.join('\\n')}`;\n\nreturn [{ json: { priceReport: toNotify, priceSection } }];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1820,
300
],
"id": "pc100001-0001-0001-0001-000000000012",
"name": "Build Report"
},
{
"parameters": {
"content": "## Price Checker\n\nBatch price-checking engine. Called by daily-briefing (schedule) or deal-finder (on-demand).\n\n**Flow:**\n1. Load all tracked items from Google Sheet\n2. Filter to active items only\n3. For each: HTTP GET product page, extract price (JSON-LD / OG meta / regex)\n4. Compare with stored price, update sheet (current, previous, low, high)\n5. Build report with items that should be notified\n6. Return `{ priceReport, priceSection }` to caller\n\n**Notify modes:**\n- `always` - include in every report\n- `on_change` - only when price changed\n- `threshold` - only when price drops below threshold (Phase 3)\n\n**Price extraction strategies:**\n1. JSON-LD `@type: Product` (most reliable)\n2. Open Graph `product:price:amount`\n3. Regex for Swiss price patterns (CHF)\n4. Graceful degradation if all fail",
"height": 460,
"width": 340
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-600,
-160
],
"id": "pc100001-0001-0001-0001-000000000013",
"name": "Sticky Note - Overview"
},
{
"parameters": {
"content": "**Setup required**\nThe sheetId default points to the developer's Google Sheet.\nReplace it with your own sheet ID in the Config node.\nCallers can also pass sheetId as input to override.\nSee setup-guide.md for full post-import checklist.",
"height": 140,
"width": 340,
"color": 3
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-420,
200
],
"id": "pc100001-0001-0001-0001-000000000016",
"name": "Sticky Note - Config Setup"
}
],
"connections": {
"Execute Workflow Trigger": {
"main": [
[
{
"node": "Config",
"type": "main",
"index": 0
}
]
]
},
"Config": {
"main": [
[
{
"node": "Load Tracked",
"type": "main",
"index": 0
}
]
]
},
"Load Tracked": {
"main": [
[
{
"node": "Filter Active",
"type": "main",
"index": 0
}
]
]
},
"Filter Active": {
"main": [
[
{
"node": "Empty?",
"type": "main",
"index": 0
}
]
]
},
"Empty?": {
"main": [
[
{
"node": "Return Empty",
"type": "main",
"index": 0
}
],
[
{
"node": "Loop Items",
"type": "main",
"index": 0
}
]
]
},
"Loop Items": {
"main": [
[
{
"node": "Collect Results",
"type": "main",
"index": 0
}
],
[
{
"node": "Fetch Page",
"type": "main",
"index": 0
}
]
]
},
"Fetch Page": {
"main": [
[
{
"node": "Extract & Compare",
"type": "main",
"index": 0
}
]
]
},
"Extract & Compare": {
"main": [
[
{
"node": "Append History",
"type": "main",
"index": 0
}
]
]
},
"Append History": {
"main": [
[
{
"node": "Update Row",
"type": "main",
"index": 0
}
]
]
},
"Update Row": {
"main": [
[
{
"node": "Loop Items",
"type": "main",
"index": 0
}
]
]
},
"Collect Results": {
"main": [
[
{
"node": "Build Report",
"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.
googleSheetsOAuth2Api
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Price-Checker. Uses executeWorkflowTrigger, googleSheets, httpRequest. Event-driven trigger; 16 nodes.
Source: https://github.com/runfish5/micro-services/blob/main/projects/n8n/10_steward/workflows/subworkflows/price-checker.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.
Google Maps Email Scraper Template. Uses removeDuplicates, splitInBatches, httpRequest, splitOut. Event-driven trigger; 26 nodes.
Email Scapper. Uses httpRequest, googleSheets, executeWorkflowTrigger. Event-driven trigger; 26 nodes.
ITHome比賽進程. Uses httpRequest, googleSheets, executeWorkflowTrigger, n8n. Event-driven trigger; 25 nodes.
25-scrape-business-emails-from-google-maps-without-the-use-of-any-third-party-apis. Uses httpRequest, googleSheets, executeWorkflowTrigger. Event-driven trigger; 23 nodes.
Intelligent URL Validation - Validates PDF URLs before attempting download, extracting filenames from URLs and generating fallback names when needed, preventing wasted processing time Binary File Hand