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": {
"inputSource": "passthrough"
},
"type": "n8n-nodes-base.executeWorkflowTrigger",
"typeVersion": 1.1,
"position": [
5792,
5872
],
"id": "df100001-0001-0001-0001-000000000001",
"name": "Execute Workflow Trigger"
},
{
"parameters": {
"jsCode": "// Merge defaults with any values passed from calling workflow\nconst input = $input.first().json || {};\nconst defaults = {\n sheetId: '1cD3xVEhfP5Iy-PwumXymLctXgI1X_mbEUlhjHDZPhto', // YOUR_GOOGLE_SHEET_ID \u2014 Steward_Deals gdrive Sheet\n sheetName: 'Requirements',\n trackingSheetName: 'Tracked Prices',\n historySheetName: 'Price History',\n region: 'Switzerland',\n retailers: 'Digitec, Galaxus, Toppreise, IKEA, Interdiscount, MediaMarkt, Brack, Microspot',\n currency: 'CHF'\n};\n\n// Input values override defaults\nreturn [{ json: { ...defaults, ...input } }];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
5992,
5872
],
"id": "df100001-0001-0001-0001-000000000002",
"name": "Config"
},
{
"parameters": {
"jsCode": "// Parse incoming command from menu-handler\nconst input = $input.first().json;\nconst text = (input.text || '').trim();\nconst chatId = input.chatId;\n\n// Internal operations (called by other workflows)\nif (text.toLowerCase() === 'check_prices') {\n return [{ json: { operation: 'check_prices', chatId } }];\n}\n\n// Explicit track command\nif (text.toLowerCase().startsWith('track ')) {\n const remainder = text.substring(6).trim();\n const urlMatch = remainder.match(/https?:\\/\\/\\S+/);\n const url = urlMatch ? urlMatch[0] : '';\n const notifyMode = remainder.toLowerCase().includes('on_change') ? 'on_change' : 'always';\n return [{ json: { operation: 'track', url, notifyMode, chatId } }];\n}\n\n// Untrack / stop tracking\nif (text.toLowerCase().match(/^(untrack|stop tracking|stop)\\s/)) {\n const identifier = text.replace(/^(untrack|stop tracking|stop)\\s+/i, '').trim();\n return [{ json: { operation: 'untrack', identifier, chatId } }];\n}\n\n// List tracked items\nif (text.toLowerCase().match(/^(tracked|list|tracking|what am i tracking)/)) {\n return [{ json: { operation: 'tracked', chatId } }];\n}\n\n// History command \u2014 single product price chart\nif (text.toLowerCase().startsWith('history ')) {\n const identifier = text.substring(8).trim();\n return [{ json: { operation: 'history', identifier, chatId } }];\n}\n\n// Plot command \u2014 all products overlay chart\nif (text.toLowerCase().match(/^(plot|chart|graph|trends?)$/)) {\n return [{ json: { operation: 'plot', chatId } }];\n}\n\n// URL detection \u2014 message contains a URL, treat as track\nconst urlInText = text.match(/https?:\\/\\/\\S+/);\nif (urlInText && !['add','remove','pause','resume','digest'].some(op => text.toLowerCase().startsWith(op))) {\n const url = urlInText[0];\n const notifyMode = text.toLowerCase().includes('on_change') ? 'on_change' : 'always';\n return [{ json: { operation: 'track', url, notifyMode, chatId } }];\n}\n\n// Original operations (unchanged)\nlet operation = 'digest';\nlet category = '';\nlet constraints = '';\nlet maxPrice = '';\n\nif (text.toLowerCase().startsWith('add ')) {\n operation = 'add';\n const parts = text.substring(4).trim().split(/\\s+/);\n category = parts[0] || '';\n const priceIdx = parts.findIndex((p, i) => i > 0 && /^\\d+/.test(p));\n if (priceIdx > 0) {\n maxPrice = parts[priceIdx];\n constraints = parts.slice(priceIdx + 1).join(' ');\n } else {\n constraints = parts.slice(1).join(' ');\n }\n} else if (text.toLowerCase().startsWith('remove ')) {\n operation = 'remove';\n category = text.substring(7).trim();\n} else if (text.toLowerCase().startsWith('pause ')) {\n operation = 'pause';\n category = text.substring(6).trim();\n} else if (text.toLowerCase().startsWith('resume ')) {\n operation = 'resume';\n category = text.substring(7).trim();\n} else if (text) {\n operation = 'digest';\n category = text.toLowerCase();\n}\n\nreturn [{\n json: {\n operation,\n category: category.toLowerCase(),\n constraints,\n maxPrice,\n chatId\n }\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
6212,
5872
],
"id": "df100001-0001-0001-0001-000000000003",
"name": "Parse Command"
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "route-digest",
"leftValue": "={{ $json.operation }}",
"rightValue": "digest",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "digest"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "route-check-prices",
"leftValue": "={{ $json.operation }}",
"rightValue": "check_prices",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "check_prices"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "route-add",
"leftValue": "={{ $json.operation }}",
"rightValue": "add",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "add"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "route-remove",
"leftValue": "={{ $json.operation }}",
"rightValue": "remove",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "remove"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "route-pause",
"leftValue": "={{ $json.operation }}",
"rightValue": "pause",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "pause"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "route-resume",
"leftValue": "={{ $json.operation }}",
"rightValue": "resume",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "resume"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "route-track",
"leftValue": "={{ $json.operation }}",
"rightValue": "track",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "track"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "route-untrack",
"leftValue": "={{ $json.operation }}",
"rightValue": "untrack",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "untrack"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "route-tracked",
"leftValue": "={{ $json.operation }}",
"rightValue": "tracked",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "tracked"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "route-history",
"leftValue": "={{ $json.operation }}",
"rightValue": "history",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "history"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "route-plot",
"leftValue": "={{ $json.operation }}",
"rightValue": "plot",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "plot"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.switch",
"typeVersion": 3.2,
"position": [
6432,
5872
],
"id": "df100001-0001-0001-0001-000000000004",
"name": "Route Operation"
},
{
"parameters": {
"documentId": {
"__rl": true,
"value": "={{ $('Config').first().json.sheetId }}",
"mode": "id"
},
"sheetName": {
"__rl": true,
"value": "={{ $('Config').first().json.sheetName }}",
"mode": "name"
},
"options": {}
},
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
6672,
5632
],
"id": "df100001-0001-0001-0001-000000000005",
"name": "Load Requirements",
"alwaysOutputData": true,
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "// Filter requirements: active only, optional category filter\nconst command = $('Parse Command').first().json;\nconst categoryFilter = command.category;\nconst chatId = command.chatId;\nconst items = $input.all();\n\nconst filtered = items.filter(item => {\n const row = item.json;\n if ((row.status || '').toLowerCase() !== 'active') return false;\n if (categoryFilter && (row.category || '').toLowerCase() !== categoryFilter) return false;\n return true;\n});\n\nif (filtered.length === 0) {\n // If user asked for a specific category, still show \"not found\"\n if (categoryFilter) {\n return [{\n json: {\n empty: true,\n chatId,\n response: `No active requirements found for \\\"${categoryFilter}\\\".\\n\\nTry: /deals add ${categoryFilter} 500CHF your constraints here`\n }\n }];\n }\n\n // No requirements at all \u2014 use a demo product so it works out of the box\n return [{\n json: {\n seed: true,\n category: 'Batteries',\n constraints: 'rechargeable, AA, 4-pack, NiMH, long-lasting',\n max_price: '30 CHF',\n priority: 'low',\n status: 'active'\n }\n }];\n}\n\nreturn filtered;"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
6880,
5632
],
"id": "df100001-0001-0001-0001-000000000006",
"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": [
7088,
5584
],
"id": "df100001-0001-0001-0001-000000000007",
"name": "Check Empty"
},
{
"parameters": {
"jsCode": "const items = $input.all();\nconst chatId = $('Parse Command').first().json.chatId;\nconst categories = items.map(i => i.json.category).filter(Boolean);\nconst count = categories.length;\nconst list = categories.join(', ');\nreturn [{ json: { chatId, response: `\ud83d\udd0d Researching ${count} categor${count === 1 ? 'y' : 'ies'}: ${list}...` } }];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
7088,
5440
],
"id": "df100001-0001-0001-0001-000000000083",
"name": "Build Indicator"
},
{
"parameters": {
"options": {}
},
"type": "n8n-nodes-base.splitInBatches",
"typeVersion": 3,
"position": [
7584,
5808
],
"id": "df100001-0001-0001-0001-000000000009",
"name": "Loop Requirements"
},
{
"parameters": {
"messages": {
"message": [
{
"content": "=You are a shopping advisor for {{ $('Config').first().json.region }}. Find the current best value option for:\n\nCategory: {{ $json.category }}\nRequirements: {{ $json.constraints }}\nBudget: {{ $json.max_price }}\nPriority: {{ $json.priority }}\n\nSearch retailers in {{ $('Config').first().json.region }} ({{ $('Config').first().json.retailers }}, etc.) and recommend 2-3 options with:\n- Product name and model\n- Current price in {{ $('Config').first().json.currency }}\n- Where to buy (retailer name)\n- Why it's a good value\n\nFocus on products actually available in {{ $('Config').first().json.region }}. Be concise but include specific prices and retailers."
}
]
},
"options": {
"maxTokens": 1024
},
"requestOptions": {}
},
"type": "n8n-nodes-base.perplexity",
"typeVersion": 1,
"position": [
7904,
5776
],
"id": "df100001-0001-0001-0001-000000000010",
"name": "Perplexity Research",
"credentials": {
"perplexityApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "// Format Perplexity response with category header\nconst requirement = $('Loop Requirements').first().json;\nconst perplexityResponse = $input.first().json;\n\nconst content = perplexityResponse.choices?.[0]?.message?.content || 'No recommendations found.';\n\nconst emoji = {\n 'phone': '\ud83d\udcf1',\n 'desk': '\ud83e\ude91',\n 'laptop': '\ud83d\udcbb',\n 'kitchen': '\ud83c\udf73',\n 'audio': '\ud83c\udfa7',\n 'camera': '\ud83d\udcf7',\n 'tv': '\ud83d\udcfa',\n 'appliance': '\ud83c\udfe0'\n};\n\nconst icon = emoji[requirement.category.toLowerCase()] || '\ud83d\uded2';\n\nreturn [{\n json: {\n category: requirement.category,\n formatted: `${icon} *${requirement.category.toUpperCase()}* (Budget: ${requirement.max_price})\\n\\n${content}`,\n recommendations: content,\n last_researched: new Date().toISOString().split('T')[0]\n }\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
8064,
5792
],
"id": "df100001-0001-0001-0001-000000000011",
"name": "Format Result"
},
{
"parameters": {
"aggregate": "aggregateAllItemData",
"options": {}
},
"type": "n8n-nodes-base.aggregate",
"typeVersion": 1,
"position": [
7984,
5632
],
"id": "df100001-0001-0001-0001-000000000012",
"name": "Collect Results"
},
{
"parameters": {
"jsCode": "// Combine all formatted results into final digest\nconst results = $input.first().json.data || [];\nconst chatId = $('Parse Command').first().json.chatId;\n\nif (results.length === 0) {\n return [{\n json: {\n chatId,\n response: '\ud83d\uded2 No recommendations generated.'\n }\n }];\n}\n\nconst digest = results.map(r => r.formatted).join('\\n\\n---\\n\\n');\nconst header = `\ud83d\uded2 *Deal Finder Digest*\\n_${results.length} categor${results.length === 1 ? 'y' : 'ies'} researched_\\n\\n`;\n\nreturn [{\n json: {\n chatId,\n response: header + digest\n }\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
8208,
5632
],
"id": "df100001-0001-0001-0001-000000000013",
"name": "Build Digest"
},
{
"parameters": {
"operation": "append",
"documentId": {
"__rl": true,
"value": "={{ $('Config').first().json.sheetId }}",
"mode": "id"
},
"sheetName": {
"__rl": true,
"value": "={{ $('Config').first().json.sheetName }}",
"mode": "name"
},
"columns": {
"mappingMode": "defineBelow",
"value": {
"category": "={{ $('Parse Command').first().json.category }}",
"constraints": "={{ $('Parse Command').first().json.constraints }}",
"max_price": "={{ $('Parse Command').first().json.maxPrice }}",
"priority": "medium",
"status": "active"
}
},
"options": {}
},
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
6672,
5872
],
"id": "df100001-0001-0001-0001-000000000015",
"name": "Append Requirement",
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"url": "={{ $('Parse Command').first().json.url }}",
"options": {
"response": {
"response": {
"fullResponse": true,
"responseFormat": "text"
}
}
}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
6672,
6412
],
"id": "df100001-0001-0001-0001-000000000040",
"name": "Fetch Product Page"
},
{
"parameters": {
"jsCode": "const html = $input.first().json.body || $input.first().json.data || '';\nconst url = $('Parse Command').first().json.url;\nconst currency = $('Config').first().json.currency;\n\nlet productName = 'Unknown Product';\nlet currentPrice = 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 productName = item.name || productName;\n const offers = item.offers;\n if (offers) {\n const offer = Array.isArray(offers) ? offers[0] : offers;\n if (offer.price) currentPrice = parseFloat(offer.price);\n else if (offer.lowPrice) currentPrice = parseFloat(offer.lowPrice);\n }\n }\n }\n } catch (e) { /* skip malformed JSON-LD */ }\n}\n\n// Strategy 2: Open Graph meta tags\nif (productName === 'Unknown Product') {\n const ogTitle = html.match(/<meta[^>]*property=[\"']og:title[\"'][^>]*content=[\"']([^\"']+)[\"']/i);\n if (ogTitle) productName = ogTitle[1].replace(/\\s*[|\\-\\u2013].*$/, '').trim();\n}\nif (currentPrice === null) {\n const ogPrice = html.match(/<meta[^>]*property=[\"']product:price:amount[\"'][^>]*content=[\"']([^\"']+)[\"']/i);\n if (ogPrice) currentPrice = parseFloat(ogPrice[1]);\n}\n\n// Strategy 3: HTML title fallback\nif (productName === 'Unknown Product') {\n const titleTag = html.match(/<title[^>]*>([^<]+)<\\/title>/i);\n if (titleTag) productName = titleTag[1].replace(/\\s*[|\\-\\u2013].*$/, '').trim();\n}\n\n// Strategy 4: Regex fallback for Swiss price patterns\nif (currentPrice === 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 currentPrice = parseFloat(priceStr.replace(',', '.'));\n }\n}\n\n// Extract domain from URL\nconst domain = url.match(/https?:\\/\\/(?:www\\.)?([^/]+)/)?.[1] || '';\nconst today = new Date().toISOString().split('T')[0];\n\nreturn [{\n json: {\n url,\n product_name: productName,\n domain,\n current_price: currentPrice,\n currency,\n previous_price: null,\n lowest_price: currentPrice,\n highest_price: currentPrice,\n first_tracked: today,\n last_checked: today,\n status: 'active',\n notify_mode: $('Parse Command').first().json.notifyMode || 'always',\n price_threshold: null,\n chatId: $('Parse Command').first().json.chatId\n }\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
6892,
6412
],
"id": "df100001-0001-0001-0001-000000000041",
"name": "Parse Product Page"
},
{
"parameters": {
"operation": "append",
"documentId": {
"__rl": true,
"value": "={{ $('Config').first().json.sheetId }}",
"mode": "id"
},
"sheetName": {
"__rl": true,
"value": "={{ $('Config').first().json.trackingSheetName }}",
"mode": "name"
},
"columns": {
"mappingMode": "defineBelow",
"value": {
"url": "={{ $json.url }}",
"product_name": "={{ $json.product_name }}",
"domain": "={{ $json.domain }}",
"current_price": "={{ $json.current_price }}",
"currency": "={{ $json.currency }}",
"previous_price": "",
"lowest_price": "={{ $json.lowest_price }}",
"highest_price": "={{ $json.highest_price }}",
"first_tracked": "={{ $json.first_tracked }}",
"last_checked": "={{ $json.last_checked }}",
"status": "={{ $json.status }}",
"notify_mode": "={{ $json.notify_mode }}",
"price_threshold": ""
}
},
"options": {}
},
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
7112,
6412
],
"id": "df100001-0001-0001-0001-000000000042",
"name": "Append Tracked Item",
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
}
},
{
"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": [
6672,
6632
],
"id": "df100001-0001-0001-0001-000000000044",
"name": "Load Tracked for Untrack",
"alwaysOutputData": true,
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "// Find tracked item by name or URL substring\nconst identifier = ($('Parse Command').first().json.identifier || '').toLowerCase();\nconst items = $input.all();\n\nlet rowIndex = -1;\nlet matchedName = '';\nfor (let i = 0; i < items.length; i++) {\n const row = items[i].json;\n const name = (row.product_name || '').toLowerCase();\n const url = (row.url || '').toLowerCase();\n if (name.includes(identifier) || url.includes(identifier)) {\n rowIndex = i + 2; // +2 for header row and 1-based index\n matchedName = row.product_name || row.url;\n break;\n }\n}\n\nreturn [{\n json: {\n identifier,\n rowIndex,\n found: rowIndex > 0,\n matchedName\n }\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
6892,
6632
],
"id": "df100001-0001-0001-0001-000000000045",
"name": "Find Tracked Row"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "check-tracked-found",
"leftValue": "={{ $json.found }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "equals"
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
7112,
6632
],
"id": "df100001-0001-0001-0001-000000000046",
"name": "Check Tracked Found"
},
{
"parameters": {
"operation": "delete",
"documentId": {
"__rl": true,
"value": "={{ $('Config').first().json.sheetId }}",
"mode": "id"
},
"sheetName": {
"__rl": true,
"value": "={{ $('Config').first().json.trackingSheetName }}",
"mode": "name"
},
"deleteType": "specificRows",
"rowsToDelete": "={{ $json.rowIndex }}"
},
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
7332,
6572
],
"id": "df100001-0001-0001-0001-000000000047",
"name": "Delete Tracked Row",
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
}
},
{
"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": [
6672,
6872
],
"id": "df100001-0001-0001-0001-000000000050",
"name": "Load All Tracked",
"alwaysOutputData": true,
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "const items = $input.all();\nconst chatId = $('Parse Command').first().json.chatId;\nconst activeItems = items.filter(i => i.json.url);\n\nif (activeItems.length === 0) {\n return [{ json: { chatId, response: '\ud83d\udccb No tracked items yet.\\n\\nTrack a product:\\n/deals track https://digitec.ch/...\\n\\nOr add a requirement:\\n/deals add phone 800CHF flagship camera' } }];\n}\n\nconst lines = activeItems.map((item, idx) => {\n const r = item.json;\n const statusIcon = r.status === 'paused' ? '\u23f8\ufe0f ' : '';\n const priceInfo = r.current_price != null && r.current_price !== '' ? `${r.current_price} ${r.currency}` : 'checking...';\n\n let changeInfo = '';\n if (r.previous_price != null && r.previous_price !== '' && r.current_price != null && r.current_price !== '' && Number(r.previous_price) !== Number(r.current_price)) {\n const prev = Number(r.previous_price);\n const curr = Number(r.current_price);\n const pct = (((curr - prev) / prev) * 100).toFixed(1);\n const arrow = curr < prev ? '\ud83d\udcc9' : '\ud83d\udcc8';\n changeInfo = ` (was ${prev}, ${pct > 0 ? '+' : ''}${pct}%) ${arrow}`;\n }\n\n const notifyLabel = r.notify_mode === 'on_change' ? '\ud83d\udd15 On change' : r.notify_mode === 'threshold' ? `\ud83d\udcb0 < ${r.price_threshold}` : '\ud83d\udd14 Daily';\n\n return `${idx + 1}. ${statusIcon}*${r.product_name}*\\n \ud83d\udcb0 ${priceInfo}${changeInfo}\\n \ud83d\udd17 ${r.domain} | ${notifyLabel}`;\n});\n\nconst response = `\ud83d\udccb *Tracked Items* (${activeItems.length})\\n\\n${lines.join('\\n\\n')}`;\nreturn [{ json: { chatId, response } }];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
6892,
6872
],
"id": "df100001-0001-0001-0001-000000000051",
"name": "Format Tracked List"
},
{
"parameters": {
"workflowId": {
"__rl": true,
"value": "YOUR_PRICE_CHECKER_WORKFLOW_ID",
"mode": "list",
"cachedResultUrl": "/workflow/YOUR_PRICE_CHECKER_WORKFLOW_ID",
"cachedResultName": "price-checker"
},
"options": {}
},
"type": "n8n-nodes-base.executeWorkflow",
"typeVersion": 1.2,
"position": [
6672,
5408
],
"id": "df100001-0001-0001-0001-000000000053",
"name": "Execute Price Checker"
},
{
"parameters": {
"jsCode": "// Format price-checker output for Telegram reply\nconst chatId = $('Parse Command').first().json.chatId;\nconst data = $input.first().json || {};\n\nconst priceSection = data.priceSection || '';\nconst priceReport = data.priceReport || [];\n\nif (!priceSection && priceReport.length === 0) {\n return [{ json: { chatId, response: '\ud83d\udccd No tracked items to check.\\n\\nUse /deals track <url> to start tracking a product.' } }];\n}\n\nreturn [{ json: { chatId, response: priceSection || '\ud83d\udccd Price check complete \u2014 no updates.' } }];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
6880,
5408
],
"id": "df100001-0001-0001-0001-000000000054",
"name": "Build Price Response"
},
{
"parameters": {
"content": "## Deal Finder\n\nPersonal shopping advisor with price tracking and Perplexity research.\nRegion, retailers, and currency are configurable in the Config node.\n\n**Requirement Commands:**\n- `/deals` - Get recommendations for all active categories\n- `/deals phone` - Get recommendations for specific category\n- `/deals add phone 800CHF flagship camera` - Add requirement\n- `/deals remove phone` - Remove requirement\n- `/deals pause phone` - Pause requirement\n- `/deals resume phone` - Resume requirement\n\n**Tracking Commands:**\n- `/deals track <url>` - Track a product's price\n- `/deals tracked` - List all tracked items\n- `/deals untrack <name>` - Stop tracking an item\n- `check_prices` - Internal: run price checker\n\n**Config (with defaults):**\n- `region`: Switzerland\n- `retailers`: Digitec, Galaxus, Toppreise, etc.\n- `currency`: CHF\n\nCallers can override via Execute Workflow input.",
"height": 480,
"width": 340
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
5792,
5392
],
"id": "df100001-0001-0001-0001-000000000035",
"name": "Sticky Note - Overview"
},
{
"parameters": {
"content": "### Google Sheet Schemas\n\n**Requirements tab:**\n| Column | Example |\n|--------|--------|\n| category | Phone |\n| constraints | flagship, good camera |\n| max_price | 800 CHF |\n| priority | high / medium / low |\n| status | active / paused |\n| recommendations | (Perplexity output) |\n| last_researched | 2026-02-17 |\n\n**Tracked Prices tab:**\n| Column | Example |\n|--------|--------|\n| url | https://digitec.ch/... |\n| product_name | Raspberry Pi 5 8GB |\n| domain | digitec.ch |\n| current_price | 89.90 |\n| currency | CHF |\n| previous_price | 95.00 |\n| lowest_price | 85.00 |\n| highest_price | 99.00 |\n| first_tracked | 2026-02-08 |\n| last_checked | 2026-02-10 |\n| status | active / paused |\n| notify_mode | always / on_change / threshold |\n| price_threshold | 80.00 |",
"height": 520,
"width": 340
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
6152,
5392
],
"id": "df100001-0001-0001-0001-000000000036",
"name": "Sticky Note - Schema"
},
{
"parameters": {
"content": "workflowId: YOUR_PRICE_CHECKER_WORKFLOW_ID\n(no required inputs \u2014 price-checker Config has defaults)",
"height": 80,
"color": 5
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
6640,
5360
],
"id": "df100001-0001-0001-0001-000000000060",
"name": "Sticky Note - Price Checker"
},
{
"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.\nchatId is passed by the caller (menu-handler).\nSee setup-guide.md for full post-import checklist.",
"height": 140,
"width": 340,
"color": 3
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
5972,
5832
],
"id": "df100001-0001-0001-0001-000000000061",
"name": "Sticky Note - Config Setup"
},
{
"parameters": {
"documentId": {
"__rl": true,
"value": "={{ $('Config').first().json.sheetId }}",
"mode": "id"
},
"sheetName": {
"__rl": true,
"value": "={{ $('Config').first().json.sheetName }}",
"mode": "name"
},
"options": {}
},
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
6672,
6132
],
"id": "df100001-0001-0001-0001-000000000056",
"name": "Load for Modify",
"alwaysOutputData": true,
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "// Find row by category and pass through operation\nconst command = $('Parse Command').first().json;\nconst category = command.category;\nconst operation = command.operation;\nconst chatId = command.chatId;\nconst items = $input.all();\n\nlet rowIndex = -1;\nlet currentStatus = '';\nfor (let i = 0; i < items.length; i++) {\n if ((items[i].json.category || '').toLowerCase() === category) {\n rowIndex = i + 2; // +2 for header row and 1-based index\n currentStatus = items[i].json.status || '';\n break;\n }\n}\n\nreturn [{\n json: { category, operation, chatId, rowIndex, found: rowIndex > 0, currentStatus }\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
6892,
6132
],
"id": "df100001-0001-0001-0001-000000000057",
"name": "Find & Route"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "check-found-modify",
"leftValue": "={{ $json.found }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "equals"
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
7112,
6132
],
"id": "df100001-0001-0001-0001-000000000062",
"name": "Check Found"
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "route-remove",
"leftValue": "={{ $json.operation }}",
"rightValue": "remove",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "remove"
}
]
},
"options": {
"fallbackOutput": "extra"
}
},
"type": "n8n-nodes-base.switch",
"typeVersion": 3.2,
"position": [
7332,
6072
],
"id": "df100001-0001-0001-0001-000000000063",
"name": "Action Switch"
},
{
"parameters": {
"operation": "delete",
"documentId": {
"__rl": true,
"value": "={{ $('Config').first().json.sheetId }}",
"mode": "id"
},
"sheetName": {
"__rl": true,
"value": "={{ $('Config').first().json.sheetName }}",
"mode": "name"
},
"deleteType": "specificRows",
"rowsToDelete": "={{ $json.rowIndex }}"
},
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
7552,
6012
],
"id": "df100001-0001-0001-0001-000000000064",
"name": "Delete Requirement",
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"operation": "update",
"documentId": {
"__rl": true,
"value": "={{ $('Config').first().json.sheetId }}",
"mode": "id"
},
"sheetName": {
"__rl": true,
"value": "={{ $('Config').first().json.sheetName }}",
"mode": "name"
},
"columns": {
"mappingMode": "defineBelow",
"value": {
"status": "={{ $json.operation === 'pause' ? 'paused' : 'active' }}"
}
},
"options": {
"cellFormat": "USER_ENTERED",
"handlingExtraData": "ignoreIt"
},
"filtersUI": {
"values": [
{
"lookupColumn": "category",
"lookupValue": "={{ $json.category }}"
}
]
}
},
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
7552,
6132
],
"id": "df100001-0001-0001-0001-000000000065",
"name": "Update Status",
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "const { operation, category, chatId } = $('Find & Route').first().json;\nconst emoji = { remove: '\u2705', pause: '\u23f8\ufe0f', resume: '\u25b6\ufe0f' };\nconst verb = { remove: 'Removed', pause: 'Paused', resume: 'Resumed' };\nconst suffix = {\n remove: 'from watchlist',\n pause: \"\u2014 won't appear in digests\",\n resume: '\u2014 will appear in digests'\n};\n\nreturn [{\n json: {\n chatId,\n response: `${emoji[operation]} ${verb[operation]} \"${category}\" ${suffix[operation]}`\n }\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
7772,
6072
],
"id": "df100001-0001-0001-0001-000000000066",
"name": "Build Modify Response"
},
{
"parameters": {
"jsCode": "const command = $('Parse Command').first().json;\nconst { operation, chatId } = command;\nlet response;\nif (operation === 'untrack') {\n response = `\u274c Item \"${command.identifier}\" not found in tracked items`;\n} else {\n response = `\u274c Category \"${command.category}\" not found in watchlist`;\n}\nreturn [{ json: { chatId, response } }];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
7332,
6252
],
"id": "df100001-0001-0001-0001-000000000067",
"name": "Build Not Found"
},
{
"parameters": {
"jsCode": "const command = $('Parse Command').first().json;\nreturn [{\n json: {\n chatId: command.chatId,\n response: `\u2705 Added \"${command.category}\" to watchlist\\n\\nBudget: ${command.maxPrice || 'not specified'}\\nConstraints: ${command.constraints || 'none'}\\n\\nUse /deals to get recommendations.`\n }\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
6880,
5872
],
"id": "df100001-0001-0001-0001-000000000068",
"name": "Build Add Response"
},
{
"parameters": {
"jsCode": "const p = $('Parse Product Page').first().json;\nconst priceText = p.current_price !== null ? `${p.current_price} ${p.currency}` : 'checking...';\nconst notifyText = p.notify_mode === 'on_change' ? 'only when price changes' : 'every day in briefing';\n\nreturn [{\n json: {\n chatId: p.chatId,\n response: `\ud83d\udccd Now tracking: *${p.product_name}*\\n\ud83d\udcb0 Current price: ${priceText} (${p.domain})\\n\ud83d\udcca Updates: ${notifyText}\\n\\nUse /deals tracked to see all items.`\n }\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
7332,
6412
],
"id": "df100001-0001-0001-0001-000000000069",
"name": "Build Track Response"
},
{
"parameters": {
"jsCode": "const tracked = $('Find Tracked Row').first().json;\nconst chatId = $('Parse Command').first().json.chatId;\nreturn [{\n json: {\n chatId,\n response: `\u2705 Stopped tracking \"${tracked.matchedName}\"`\n }\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
7552,
6572
],
"id": "df100001-0001-0001-0001-000000000070",
"name": "Build Untrack Response"
},
{
"parameters": {
"chatId": "={{ $json.chatId }}",
"text": "={{ $json.response }}",
"additionalFields": {
"appendAttribution": false,
"parse_mode": "Markdown"
}
},
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
8672,
6096
],
"id": "df100001-0001-0001-0001-000000000072",
"name": "Send Reply",
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "check-seed",
"leftValue": "={{ $json.seed }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "equals"
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
7232,
5712
],
"id": "df100001-0001-0001-0001-000000000080",
"name": "Is Seed?"
},
{
"parameters": {
"operation": "append",
"documentId": {
"__rl": true,
"value": "={{ $('Config').first().json.sheetId }}",
"mode": "id"
},
"sheetName": {
"__rl": true,
"value": "={{ $('Config').first().json.sheetName }}",
"mode": "name"
},
"columns": {
"mappingMode": "defineBelow",
"value": {
"category": "={{ $json.category }}",
"constraints": "={{ $json.constraints }}",
"max_price": "={{ $json.max_price }}",
"priority": "={{ $json.priority }}",
"status": "={{ $json.status }}",
"recommendations": "",
"last_researched": ""
},
"matchingColumns": [],
"schema": [
{
"id": "category",
"displayName": "category",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "constraints",
"displayName": "constraints",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "max_price",
"displayName": "max_price",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "priority",
"displayName": "priority",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "status",
"displayName": "status",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "recommendations",
"displayName": "recommendations",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "last_researched",
"displayName": "last_researched",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
}
]
},
"options": {}
},
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
7392,
5696
],
"id": "df100001-0001-0001-0001-000000000081",
"name": "Seed Demo",
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"operation": "update",
"documentId": {
"__rl": true,
"value": "={{ $('Config').first().json.sheetId }}",
"mode": "id"
},
"sheetName": {
"__rl": true,
"value": "={{ $('Config').first().json.sheetName }}",
"mode": "name"
},
"columns": {
"mappingMode": "autoMapInputData",
"matchingColumns": [
"category"
],
"schema": [
{
"id": "category",
"displayName": "category",
"required": false,
"defaultMatch": true,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "recommendations",
"displayName": "recommendations",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "last_researched",
"displayName": "last_researched",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
}
]
},
"options": {
"cellFormat": "USER_ENTERED",
"handlingExtraData": "ignoreIt"
}
},
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
8256,
5792
],
"id": "df100001-0001-0001-0001-000000000082",
"name": "Save Results",
"alwaysOutputData": true,
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"documentId": {
"__rl": true,
"value": "={{ $('Config').first().json.sheetId }}",
"mode": "id"
},
"sheetName": {
"__rl": true,
"value": "={{ $('Config').first().json.historySheetName }}",
"mode": "name"
},
"options": {}
},
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
6672,
7100
],
"id": "df100001-0001-0001-0001-000000000090",
"name": "Load History",
"alwaysOutputData": true,
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "const items = $input.all().map(i => i.json);\nconst operation = $('Parse Command').first().json.operation;\nconst chatId = $('Parse Command').first().json.chatId;\nconst currency = $('Config').first().json.currency;\n\nif (operation === 'history') {\n const identifier = ($('Parse Command').first().json.identifier || '').toLowerCase();\n const matched = items.filter(i => {\n const name = (i.product_name || '').toLowerCase();\n const url = (i.url || '').toLowerCase();\n return name.includes(identifier) || url.includes(identifier);\n });\n\n if (matched.length === 0) {\n return [{ json: { hasData: false, chatId, response: `No price history found for \"${identifier}\".\\n\\nUse /deals tracked to see your tracked items.` } }];\n }\n\n const dataPoints = matched\n .filter(i => i.price != null && i.price !== '')\n .map(i => ({ date: i.date, price: Number(i.price) }))\n .sort((a, b) => a.date.localeCompare(b.date));\n\n const productName = matched[0].product_name || identifier;\n\n if (dataPoints.length < 2) {\n return [{ json: { hasData: false, chatId, response: `Only ${dataPoints.length} data point${dataPoints.length === 1 ? '' : 's'} for \"${productName}\".\\n\\nWait for more price checks to build a chart.` } }];\n }\n\n const prices = dataPoints.map(d => d.price);\n const current = prices[prices.length - 1];\n const first = prices[0];\n const low = Math.min(...prices);\n const high = Math.max(...prices);\n const changePct = (((current - first) / first) * 100).toFixed(1);\n const arrow = current < first ? '\\ud83d\\udcc9' : current > first ? '\\ud83d\\udcc8' : '\\u27a1\\ufe0f';\n\n const caption = `\\ud83d\\udcca *${productName}*\\n\\n\\ud83d\\udcb0 Current: ${current} ${currency}\\n${arrow} Change: ${changePct > 0 ? '+' : ''}${changePct}% since ${dataPoints[0].date}\\n\\ud83d\\udcc9 Low: ${low} ${currency} | \\ud83d\\udcc8 High: ${high} ${currency}\\n\\ud83d\\udcca ${dataPoints.length} data points (${dataPoints[0].date} \\u2014 ${dataPoints[dataPoints.length - 1].date})`;\n\n const chartConfig = {\n type: 'line',\n data: {\n labels: dataPoints.map(d => d.date),\n datasets: [{\n label: productName,\n data: dataPoints.map(d => d.price),\n fill: true,\n borderColor: 'rgb(54, 162, 235)',\n backgroundColor: 'rgba(54, 162, 235, 0.1)',\n tension: 0.3,\n pointRadius: 3\n }]\n },\n options: {\n plugins: {\n title: { display: true, text: `${productName} \\u2014 Price History`, font: { size: 16 } },\n legend: { display: false }\n },\n scales: {\n y: { title: { display: true, text: `Price (${currency})` }, beginAtZero: false },\n x: { title: { display: true, text: 'Date' } }\n }\n }\n };\n\n return [{ json: { hasData: true, chartConfig, chatId, caption } }];\n\n} else {\n // plot operation\n const validItems = items.filter(i => i.price != null && i.price !== '' && i.url);\n\n if (validItems.length === 0) {\n return [{ json: { hasData: false, chatId, response: 'No price history data yet.\\n\\nTrack products with /deals track <url> and wait for price checks to build up data.' } }];\n }\n\n const byProduct = {};\n for (const item of validItems) {\n const url = item.url;\n if (!byProduct[url]) {\n byProduct[url] = { name: item.product_name || url, points: [] };\n }\n byProduct[url].points.push({ date: item.date, price: Number(item.price) });\n }\n\n for (const key of Object.keys(byProduct)) {\n byProduct[key].points.sort((a, b) => a.date.localeCompare(b.date));\n }\n\n const allDates = [...new Set(validItems.map(i => i.date))].sort();\n\n const colors = [\n 'rgb(54, 162, 235)', 'rgb(255, 99, 132)', 'rgb(75, 192, 192)',\n 'rgb(255, 159, 64)', 'rgb(153, 102, 255)', 'rgb(255, 205, 86)',\n 'rgb(201, 203, 207)', 'rgb(0, 128, 0)'\n ];\n\n const products = Object.values(byProduct);\n const datasets = products.map((prod, idx) => {\n const dateMap = {};\n for (const p of prod.points) { dateMap[p.date] = p.price; }\n const data = allDates.map(d => dateMap[d] !== undefined ? dateMap[d] : null);\n\n return {\n label: prod.name,\n data,\n borderColor: colors[idx % colors.length],\n fill: false,\n tension: 0.3,\n spanGaps: true,\n pointRadius: 2\n };\n });\n\n const chartConfig = {\n type: 'line',\n data: { labels: allDates, datasets },\n options: {\n plugins: {\n title: { display: true, text: 'Price Tracker \\u2014 All Products', font: { size: 16 } },\n legend: { display: true, position: 'bottom' }\n },\n scales: {\n y: { title: { display: true, text: `Price (${currency})` }, beginAtZero: false },\n x: { title: { display: true, text: 'Date' } }\n }\n }\n };\n\n const dateRange = `${allDates[0]} \\u2014 ${allDates[allDates.length - 1]}`;\n const caption = `\\ud83d\\udcca *Price Tracker* (${products.length} product${products.length === 1 ? '' : 's'})\\n${dateRange}\\n${validItems.length} total data points`;\n\n return [{ json: { hasData: true, chatId, chartConfig, caption } }];\n}"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
6892,
7100
],
"id": "df100001-0001-0001-0001-000000000110",
"name": "Build Chart Data"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "check-has-data",
"leftValue": "={{ $json.hasData }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "equals"
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
7112,
7100
],
"id": "df100001-0001-0001-0001-000000000111",
"name": "Has Data?"
},
{
"parameters": {
"method": "POST",
"url": "https://quickchart.io/chart",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ chart: $json.chartConfig, width: 700, height: 450, format: 'png', version: '3' }) }}",
"options": {
"response": {
"response": {
"responseFormat": "file"
}
}
}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion"
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.
googleSheetsOAuth2ApiperplexityApitelegramApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Deal-Finder. Uses executeWorkflowTrigger, googleSheets, perplexity, httpRequest. Event-driven trigger; 49 nodes.
Source: https://github.com/runfish5/micro-services/blob/main/projects/n8n/10_steward/workflows/subworkflows/deal-finder.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.
This workflow provides a complete solution for handling Telegram Stars payments, invoicing and refunds using n8n. It automates the process of sending invoices, managing pre-checkout approvals, recordi
03 - Command Handler. Uses executeWorkflowTrigger, telegram, executeCommand, postgres. Event-driven trigger; 53 nodes.
checkProcess(old). Uses googleSheets, httpRequest, telegram, @n-octo-n/n8n-nodes-json-database. Event-driven trigger; 40 nodes.
checkProcess. Uses googleSheets, httpRequest, telegram, @n-octo-n/n8n-nodes-json-database. Event-driven trigger; 40 nodes.
This template monitors Google Drive folder for new files, extracts text from PDFs, images, text files, CSVs, and Google Docs., reads images with meta/llama-3.2-11b-vision-instruct, structures the resu