This workflow corresponds to n8n.io template #10216 — we link there as the canonical source.
This workflow follows the Google Sheets → HTTP Request 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": "YnD0kvM9AYMjYxmj",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "Multi-Site Product Price Monitor: Claude-Sonnet , Google Sheets & Telegram Alert",
"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": "306294b3-27b8-4821-9b04-0e8d210af07e",
"name": "\u23f0 Monitor Every 6 Hours",
"type": "n8n-nodes-base.scheduleTrigger",
"notes": "Triggers the workflow every 6 hours to check competitor data",
"position": [
-832,
352
],
"parameters": {
"rule": {
"interval": [
{
"field": "hours",
"hoursInterval": 6
}
]
}
},
"typeVersion": 1.2
},
{
"id": "8b934690-1e26-4810-820d-8ff785691adf",
"name": "Scrape a url and get its content",
"type": "@mendable/n8n-nodes-firecrawl.firecrawl",
"position": [
-1408,
96
],
"parameters": {
"url": "https://www.on.com/en-sg/shop/mens/shoes",
"operation": "scrape",
"scrapeOptions": {
"options": {
"formats": {
"format": [
{
"type": "json",
"prompt": "price of the shoe"
}
]
},
"headers": {}
}
},
"requestOptions": {}
},
"credentials": {
"firecrawlApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "90ed5eb7-6236-47e1-88c6-c5278efe24fa",
"name": "Message a model",
"type": "n8n-nodes-base.perplexity",
"position": [
-1568,
0
],
"parameters": {
"model": "sonar",
"options": {},
"messages": {
"message": [
{
"content": "Price of the shoe "
}
]
},
"requestOptions": {}
},
"credentials": {
"perplexityApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "e1e006de-d363-4e5a-adbd-77ffc901f42d",
"name": "Split Out",
"type": "n8n-nodes-base.splitOut",
"position": [
-1296,
0
],
"parameters": {
"options": {},
"fieldToSplitOut": "citations"
},
"typeVersion": 1
},
{
"id": "11fdf8bc-aa37-4f42-8bbe-8e16c47c8b47",
"name": "Get Results Apify",
"type": "n8n-nodes-base.httpRequest",
"position": [
-1216,
272
],
"parameters": {
"url": "https://api.apify.com/v2/acts/compass~google-maps-extractor/runs/last/dataset/items?token=YOUR_TOKEN_HERE API KEY",
"options": {},
"authentication": "genericCredentialType",
"genericAuthType": "httpBearerAuth"
},
"credentials": {
"httpBearerAuth": {
"name": "<your credential>"
}
},
"typeVersion": 4.2,
"alwaysOutputData": true
},
{
"id": "b6a20e25-d82a-493b-b305-066e043f6ba1",
"name": "Start Actor Apify",
"type": "n8n-nodes-base.httpRequest",
"position": [
-1616,
272
],
"parameters": {
"url": "https://api.apify.com/v2/acts/compass~google-maps-extractor/runs?token=YOUR_TOKEN_HERE API KEY",
"method": "POST",
"options": {},
"jsonBody": "{\n \"language\": \"en\",\n \"locationQuery\": \"worldwide\",\n \"maxCrawledPlacesPerSearch\": 50,\n \"searchStringsArray\": [\n \"shoe stores\",\n \"shoe shops\"\n ],\n \"skipClosedPlaces\": false\n}",
"sendBody": true,
"specifyBody": "json",
"authentication": "genericCredentialType",
"genericAuthType": "httpBearerAuth"
},
"credentials": {
"httpBearerAuth": {
"name": "<your credential>"
}
},
"typeVersion": 4.2,
"alwaysOutputData": true
},
{
"id": "47a1e71a-6bbf-4cfe-82f1-b46744d247ee",
"name": "Converts unstructured AI text into organized, usable data fields",
"type": "n8n-nodes-base.code",
"notes": "Parses and validates the AI extracted data",
"position": [
-576,
192
],
"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": "3e8d7348-ccef-4cb5-aca4-d5ed6f210199",
"name": "\ud83d\udd00 Merge Current with Historical1",
"type": "n8n-nodes-base.merge",
"notes": "Combines current scrape with historical data for comparison",
"position": [
-384,
224
],
"parameters": {
"mode": "combine",
"options": {},
"fieldsToMatchString": "rawResponse.message.content"
},
"typeVersion": 3
},
{
"id": "7e24cbab-31e4-47ba-97f2-9a7a71335905",
"name": "\ud83d\udd0d Detect Price & Stock Changes1",
"type": "n8n-nodes-base.code",
"notes": "Intelligent change detection with alert level classification",
"position": [
-208,
224
],
"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": "4b8b4578-e6aa-461a-ae0a-7289bd0bf7e3",
"name": "\ud83d\udcbe Update Historical Data1",
"type": "n8n-nodes-base.googleSheets",
"notes": "Saves current data to historical tracking sheet",
"position": [
-32,
320
],
"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": "28d2de55-ef62-40cd-a22b-a13eee3c2dc8",
"name": "When clicking \u2018Execute workflow\u2019",
"type": "n8n-nodes-base.manualTrigger",
"position": [
-1760,
144
],
"parameters": {},
"typeVersion": 1
},
{
"id": "62cf73d2-a328-463b-9ea5-0d3fa8e3ba56",
"name": "\ud83e\udd16 AI Extract Product Data using Claude-Sonnet 4.5",
"type": "@n8n/n8n-nodes-langchain.anthropic",
"position": [
-864,
192
],
"parameters": {
"modelId": {
"__rl": true,
"mode": "list",
"value": "claude-sonnet-4-5-20250929",
"cachedResultName": "claude-sonnet-4-5-20250929"
},
"options": {},
"messages": {
"values": [
{
"content": "You are an accurate e-commerce data parser. From the provided HTML, extract shoe details and output a pure JSON object\u2014no markdown or extra text.\n\nInclude the following fields:\n\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\nOutput only the JSON object\u2014no notes, comments, or formatting.\n"
}
]
}
},
"credentials": {
"anthropicApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "e5998861-2d29-4fdb-a5c0-445cf79ac50e",
"name": "\ud83d\udd00 Merge Current with Historical",
"type": "n8n-nodes-base.merge",
"notes": "Combines current scrape with historical data for comparison",
"position": [
-1024,
176
],
"parameters": {
"numberInputs": 3
},
"typeVersion": 3
},
{
"id": "6815d6c1-43c5-49a8-8f41-eafa0ee83363",
"name": "\ud83d\udcca Read Historical Data",
"type": "n8n-nodes-base.googleSheets",
"notes": "Loads previous scan data for comparison",
"position": [
-624,
352
],
"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": "0c300e39-74da-4947-ab75-1360158f48a1",
"name": "\ud83d\udcdd Log Alert Details",
"type": "n8n-nodes-base.googleSheets",
"notes": "Logs all alerts to separate tracking sheet",
"position": [
-48,
160
],
"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": "1833ee8f-13ca-40f3-a3ce-e7c09bbff5fc",
"name": "Send a text message",
"type": "n8n-nodes-base.telegram",
"position": [
112,
240
],
"parameters": {
"text": "The competitors\u2019 pricing has been finalized.",
"additionalFields": {}
},
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.2
},
{
"id": "1209134e-cb58-4525-bbcb-ffee2a890ddb",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
-2336,
-336
],
"parameters": {
"width": 2576,
"height": 880,
"content": "## Introduction\nAutomate price monitoring for e-commerce competitors\u2014ideal for retailers, analysts, and pricing teams. Scrapes competitor sites, \nextracts pricing/stock data via AI, detects changes, and sends instant alerts for dynamic pricing strategies.\n## How It Works\nScrapes competitor URLs via Firecrawl and Apify, extracts data with AI, detects price/stock changes, logs to Google Sheets, and sends Telegram alerts.\n## Workflow Template\nTrigger \u2192 Scrape URL \u2192 AI Extract \u2192 Parse \u2192 Merge Historical \u2192 Detect Changes \u2192 Update Sheets + Send Telegram Alert\n## Workflow Steps\n1. **Trigger & Scrape** \u2192 Manual/scheduled trigger \u2192 Firecrawl + Apify fetch competitor data\n2. **AI Processing** \u2192 Claude extracts product details \u2192 Parses and structures data\n3. **Change Detection** \u2192 Reads historical prices \u2192 Merges with current data \u2192 Identifies updates\n4. **Output** \u2192 Logs alerts to Sheets \u2192 Updates historical data \u2192 Sends Telegram notification\n## Setup Instructions\n**1. Firecrawl API**\nGet key from dashboard \u2192 Add to n8n\n**2. Apify API**\nGet key from console \u2192 Add to n8n \u2192 Configure actors\n**3. AI Model (Claude/OpenAI)**\nGet API key \u2192 Add to n8n\n**4. Google Sheets OAuth2**\nCreate OAuth2 in Google Cloud Console \u2192 Authorize in n8n \u2192 Enable API\n**5. Telegram Bot**\nCreate via BotFather \u2192 Get token & chat ID \u2192 Add to n8n\n**6. Spreadsheet Setup**\nCreate Sheet with required columns \u2192 Copy ID \u2192 Paste in workflow\n## Prerequisites\nSelf-hosted n8n, Firecrawl account, Apify account, Claude/OpenAI API key,\nGoogle account (Sheets OAuth2),Telegram bot\n## Customization\nAdd more URLs, adjust scraping intervals, change detection thresholds, switch to Slack/email alerts,\nintegrate databases\n## Benefits\nSaves 2+ hours daily, real-time tracking, automated alerts, historical analysis, multi-source scraping\n\n"
},
"typeVersion": 1
},
{
"id": "864886e9-21d0-402f-8f1f-46216a5e1373",
"name": "Pause the workflow to let the Apify scraping task finish",
"type": "n8n-nodes-base.wait",
"position": [
-1424,
272
],
"parameters": {
"amount": 25
},
"typeVersion": 1.1
},
{
"id": "5b798a21-b3e1-4fb4-816a-d32a63367933",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-608,
-304
],
"parameters": {
"color": 5,
"width": 352,
"height": 256,
"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": "7e9182a3-7a64-45a2-9632-3de5d2917fcd",
"connections": {
"Split Out": {
"main": [
[
{
"node": "\ud83d\udd00 Merge Current with Historical",
"type": "main",
"index": 0
}
]
]
},
"Message a model": {
"main": [
[
{
"node": "Split Out",
"type": "main",
"index": 0
}
]
]
},
"Get Results Apify": {
"main": [
[
{
"node": "\ud83d\udd00 Merge Current with Historical",
"type": "main",
"index": 2
}
]
]
},
"Start Actor Apify": {
"main": [
[
{
"node": "Pause the workflow to let the Apify scraping task finish",
"type": "main",
"index": 0
}
]
]
},
"\ud83d\udcdd Log Alert Details": {
"main": [
[
{
"node": "Send a text message",
"type": "main",
"index": 0
}
]
]
},
"\u23f0 Monitor Every 6 Hours": {
"main": [
[
{
"node": "\ud83d\udcca Read Historical Data",
"type": "main",
"index": 0
}
]
]
},
"\ud83d\udcca Read Historical Data": {
"main": [
[
{
"node": "\ud83d\udd00 Merge Current with Historical1",
"type": "main",
"index": 1
}
]
]
},
"\ud83d\udcbe Update Historical Data1": {
"main": [
[
{
"node": "Send a text message",
"type": "main",
"index": 0
}
]
]
},
"Scrape a url and get its content": {
"main": [
[
{
"node": "\ud83d\udd00 Merge Current with Historical",
"type": "main",
"index": 1
}
]
]
},
"\ud83d\udd00 Merge Current with Historical": {
"main": [
[
{
"node": "\ud83e\udd16 AI Extract Product Data using Claude-Sonnet 4.5",
"type": "main",
"index": 0
}
]
]
},
"\ud83d\udd0d Detect Price & Stock Changes1": {
"main": [
[
{
"node": "\ud83d\udcbe Update Historical Data1",
"type": "main",
"index": 0
},
{
"node": "\ud83d\udcdd Log Alert Details",
"type": "main",
"index": 0
}
]
]
},
"\ud83d\udd00 Merge Current with Historical1": {
"main": [
[
{
"node": "\ud83d\udd0d Detect Price & Stock Changes1",
"type": "main",
"index": 0
}
]
]
},
"When clicking \u2018Execute workflow\u2019": {
"main": [
[
{
"node": "Message a model",
"type": "main",
"index": 0
},
{
"node": "Scrape a url and get its content",
"type": "main",
"index": 0
},
{
"node": "Start Actor Apify",
"type": "main",
"index": 0
}
]
]
},
"\ud83e\udd16 AI Extract Product Data using Claude-Sonnet 4.5": {
"main": [
[
{
"node": "Converts unstructured AI text into organized, usable data fields",
"type": "main",
"index": 0
}
]
]
},
"Pause the workflow to let the Apify scraping task finish": {
"main": [
[
{
"node": "Get Results Apify",
"type": "main",
"index": 0
}
]
]
},
"Converts unstructured AI text into organized, usable data fields": {
"main": [
[
{
"node": "\ud83d\udd00 Merge Current with Historical1",
"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.
anthropicApifirecrawlApigoogleSheetsOAuth2ApihttpBearerAuthperplexityApitelegramApi
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. Scrapes competitor sites, extracts pricing/stock data via AI, detects changes, and sends instant alerts for dynamic pricing strategies.
Source: https://n8n.io/workflows/10216/ — 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.
Turn any Amazon India product URL into a fully-edited 10-second lifestyle video and auto-publish it to Instagram, Facebook, X (Twitter), LinkedIn, YouTube, and Threads — with platform-optimized captio
Schedule Trigger runs every 6 hours (customizable) Apify Scraper fetches Upwork jobs matching your criteria Deduplication filters out jobs you've already seen AI Scoring (GPT-4) evaluates fit, client
Women creators, homemakers-turned-entrepreneurs, and feminine lifestyle brands who want a graceful, low-lift way to keep an eye on competitor content and spark weekly ideas.
This workflow is Part 2 of the HR Client Acquisition system and builds on the lead discovery pipeline from the previous workflow:
Visual Regression Testing With Apify And Ai Vision Model. Uses googleDrive, lmChatGoogleGemini, outputParserStructured, stickyNote. Scheduled trigger; 34 nodes.