{
  "id": "y0Yk7da21T4u9zlp",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Product Price Monitor with Pushover and Baserow",
  "tags": [],
  "nodes": [
    {
      "id": "2c7a5425-7dcd-4c4f-8f73-4094f8d4d54d",
      "name": "Workflow Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -288,
        -288
      ],
      "parameters": {
        "width": 550,
        "height": 818,
        "content": "## How it works\n\nThis webhook-driven workflow lets you fire price checks from any system, curl call, or no-code frontend. When the webhook receives a payload, it immediately loads a list of seasonal products that you define in the \u201cProduct Configuration\u201d code node. Each product is processed in its own mini-loop so n8n can scale to dozens of URLs without hitting rate limits. ScrapeGraphAI visits every page and extracts the current price, name, and availability with an AI prompt so you never need brittle CSS selectors. A lightweight HTTP call then enriches those raw numbers with historical averages to calculate the exact percentage difference. After validation the workflow stores every record in Baserow for reporting and, if a price drop crosses your threshold, pushes an actionable notification to your phone via Pushover.\n\n## Setup steps\n\n1. Add ScrapeGraphAI API credentials  \n2. Add Baserow API credentials and table ID  \n3. Add your Pushover user and app tokens  \n4. Edit the \u201cProduct Configuration\u201d code node with your URLs and thresholds  \n5. Optionally change the enrichment API endpoint in the HTTP node  \n6. Copy the webhook URL and trigger it with an empty POST to test  \n7. Enable the workflow"
      },
      "typeVersion": 1
    },
    {
      "id": "12c92554-c60b-41d5-b93d-090579ea4c9f",
      "name": "Section \u2013 Trigger & Config",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        400,
        -288
      ],
      "parameters": {
        "color": 7,
        "width": 546,
        "height": 798,
        "content": "## Trigger & Configuration\n\nThe nodes under this note prepare everything the flow needs before any scraping happens. The Webhook Trigger exposes a public endpoint you can hit from a scheduler, another n8n workflow, or even Postman. The nearby \u201cProduct Configuration\u201d code node keeps the product list, price-drop threshold, and any other metadata in one place. It returns one item per SKU so downstream nodes stay simple and stateless. Need to add or remove an item? Just update this single node\u2014no fiddling with URLs in multiple places. Centralising setup here shortens onboarding time for team-mates and keeps business logic totally separate from scraping and storage tasks."
      },
      "typeVersion": 1
    },
    {
      "id": "bd2d56a7-7b32-4220-9b5c-0521c72f4d47",
      "name": "Section \u2013 Scraping",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        992,
        -288
      ],
      "parameters": {
        "color": 7,
        "width": 450,
        "height": 798,
        "content": "## Scraping\n\nThis cluster handles live data retrieval using ScrapeGraphAI. The Split In Batches node feeds one product at a time to respect rate limits and reduce the chance of anti-bot blocks. ScrapeGraphAI then loads each product page and, via a natural-language prompt, extracts the title, numeric price, currency, and availability flag. Because it relies on semantic understanding rather than brittle selectors, the scraper keeps working even when the retailer adds holiday banners or minor layout tweaks. The resulting JSON is clean, well-structured, and ready for enrichment in the next section."
      },
      "typeVersion": 1
    },
    {
      "id": "376a02e1-eb4c-4628-99e4-6086bc3ae48a",
      "name": "Section \u2013 Enrichment",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1472,
        -272
      ],
      "parameters": {
        "color": 7,
        "width": 450,
        "height": 782,
        "content": "## Enrichment\n\nRaw numbers become insights when you add context. The HTTP Request node calls an external pricing API\u2014swap this for your preferred endpoint\u2014to fetch the product\u2019s 90-day average price. The Merge node then stitches together live and historical data so subsequent calculations have everything in one object. Fetching history on-demand keeps the workflow stateless and your database lean. Because enrichment happens immediately after scraping, every later node only deals with fully-formed, context-rich items, simplifying logic and making future expansions\u2014like currency conversion or competitor comparisons\u2014straightforward."
      },
      "typeVersion": 1
    },
    {
      "id": "02fd80a0-cc90-413d-9c17-933f9c5892f5",
      "name": "Section \u2013 Storage & Validation",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1952,
        -272
      ],
      "parameters": {
        "color": 7,
        "width": 450,
        "height": 798,
        "content": "## Storage & Validation\n\nThe \u201cTransform & Enrich Data\u201d code node standardises field names, converts strings to proper numbers, calculates the percentage drop, and stamps each record with an ISO timestamp. Only validated, well-structured objects move forward, keeping your database pristine. The Baserow node persists every record so analysts\u2014and even non-technical teammates\u2014can slice, dice, or export the data from a friendly spreadsheet interface. Storing every scrape, successful or not, provides an auditable history that helps debug anomalies or prove pricing trends when negotiating with suppliers."
      },
      "typeVersion": 1
    },
    {
      "id": "9daa18eb-c090-4128-9436-b315e91db091",
      "name": "Section \u2013 Notifications",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2448,
        -272
      ],
      "parameters": {
        "color": 7,
        "width": 450,
        "height": 782,
        "content": "## Alerts & Error Handling\n\nTwo separate paths keep the noise down while ensuring you never miss an important event. When the calculated price drop exceeds your threshold, the workflow formats a concise alert and pushes it to your phone via Pushover. A second path captures scraping errors\u2014such as missing price data or page time-outs\u2014and sends a distinct high-priority notification. Splitting alerts like this lets you ignore routine activity and focus on genuine issues or opportunities. Because both messages use the same credential block, you configure Pushover once and the workflow handles everything else automatically."
      },
      "typeVersion": 1
    },
    {
      "id": "9ffea5c4-20db-498b-9567-006274787429",
      "name": "Price Monitor Webhook",
      "type": "n8n-nodes-base.webhook",
      "position": [
        416,
        64
      ],
      "parameters": {
        "path": "product-price-monitor",
        "options": {},
        "httpMethod": "POST"
      },
      "typeVersion": 1
    },
    {
      "id": "8600f836-a66b-4a46-8776-441bb272835d",
      "name": "Product Configuration",
      "type": "n8n-nodes-base.code",
      "position": [
        624,
        64
      ],
      "parameters": {
        "jsCode": "const products = [\n  { name: 'Winter Jacket', url: 'https://example.com/winter-jacket', thresholdPercentage: 10 },\n  { name: 'Snow Boots', url: 'https://example.com/snow-boots', thresholdPercentage: 10 }\n];\nreturn products.map(p => ({ json: p }));"
      },
      "typeVersion": 2
    },
    {
      "id": "8fcdbc1f-c8ce-4b02-aba3-fb947ce8019b",
      "name": "Loop Products",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        816,
        64
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "73e65849-2bd0-49c2-9d60-bb8bb55358df",
      "name": "Scrape Product Data",
      "type": "n8n-nodes-scrapegraphai.scrapegraphAi",
      "position": [
        1024,
        64
      ],
      "parameters": {
        "userPrompt": "Extract the product title as \\\"name\\\", numeric price as \\\"price\\\", currency as \\\"currency\\\", and availability text as \\\"availability\\\". Respond as JSON.",
        "websiteUrl": "={{ $json.url }}"
      },
      "typeVersion": 1
    },
    {
      "id": "f59fd8d2-8926-4fb8-9aae-21ccb8dc9f06",
      "name": "Valid Price Check",
      "type": "n8n-nodes-base.if",
      "position": [
        1216,
        64
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "number": [
            {
              "value1": "={{ $json.price }}",
              "value2": 0,
              "operation": "larger"
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "ccff1ea2-2a8d-4895-9448-b8bf83f1e028",
      "name": "Fetch Historical Data",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1520,
        192
      ],
      "parameters": {
        "url": "={{ 'https://api.pricingexample.com/history?name=' + encodeURIComponent($json.name) }}",
        "options": {}
      },
      "typeVersion": 4
    },
    {
      "id": "ca7b92e4-c98d-4494-a3d4-f60956b0668c",
      "name": "Combine Scrape & History",
      "type": "n8n-nodes-base.merge",
      "position": [
        1648,
        192
      ],
      "parameters": {
        "mode": "mergeByIndex",
        "options": {}
      },
      "typeVersion": 2
    },
    {
      "id": "719828e5-438e-4beb-b716-7a1d04352480",
      "name": "Transform & Enrich Data",
      "type": "n8n-nodes-base.code",
      "position": [
        1792,
        192
      ],
      "parameters": {
        "jsCode": "const items = $input.all();\nreturn items.map(item => {\n  const current = Number(item.json.price);\n  const average = Number(item.json.averagePrice || item.json.average_price || 0);\n  const drop = average > 0 ? ((average - current) / average) * 100 : 0;\n  return {\n    json: {\n      ...item.json,\n      averagePrice: average,\n      changePercent: Number(drop.toFixed(2)),\n      timestamp: new Date().toISOString(),\n      thresholdPercentage: item.json.thresholdPercentage || 10\n    }\n  };\n});"
      },
      "typeVersion": 2
    },
    {
      "id": "36d847a1-cb58-4582-8c2f-110b914b77e5",
      "name": "Price Drop?",
      "type": "n8n-nodes-base.if",
      "position": [
        2192,
        176
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "number": [
            {
              "value1": "={{ $json.changePercent }}",
              "value2": "={{ $json.thresholdPercentage }}",
              "operation": "larger"
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "9fcd677f-6e4f-4057-ab69-c7dd263952a3",
      "name": "Prepare Alert Message",
      "type": "n8n-nodes-base.set",
      "position": [
        2528,
        224
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "dd463c6b-a9ad-423f-8783-5c1b89e8c3b6",
      "name": "Send Pushover Alert",
      "type": "n8n-nodes-base.pushover",
      "position": [
        2720,
        256
      ],
      "parameters": {
        "message": "={{ $json.message }}",
        "priority": 0,
        "additionalFields": {}
      },
      "typeVersion": 1
    },
    {
      "id": "66ff0592-ff37-43d9-a568-0332adeff9be",
      "name": "Prepare Error Message",
      "type": "n8n-nodes-base.set",
      "position": [
        1568,
        352
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "6c21cd35-b154-40f0-8f9f-35f397c04489",
      "name": "Send Error Alert",
      "type": "n8n-nodes-base.pushover",
      "position": [
        1760,
        336
      ],
      "parameters": {
        "message": "={{ $json.message }}",
        "priority": 1,
        "additionalFields": {}
      },
      "typeVersion": 1
    },
    {
      "id": "4c8337b9-4ebb-45d1-9f95-daf7fa2aec26",
      "name": "Insert rows in a table",
      "type": "n8n-nodes-base.postgres",
      "position": [
        2016,
        176
      ],
      "parameters": {
        "table": {
          "__rl": true,
          "mode": "list",
          "value": ""
        },
        "schema": {
          "__rl": true,
          "mode": "list",
          "value": "public"
        }
      },
      "typeVersion": 2.6
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "eb929612-a6ee-4512-a090-5fa9112d25cf",
  "connections": {
    "Price Drop?": {
      "main": [
        [
          {
            "node": "Prepare Alert Message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Products": {
      "main": [
        [
          {
            "node": "Scrape Product Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Valid Price Check": {
      "main": [
        [
          {
            "node": "Fetch Historical Data",
            "type": "main",
            "index": 0
          },
          {
            "node": "Combine Scrape & History",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Prepare Error Message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Scrape Product Data": {
      "main": [
        [
          {
            "node": "Valid Price Check",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Historical Data": {
      "main": [
        [
          {
            "node": "Combine Scrape & History",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Prepare Alert Message": {
      "main": [
        [
          {
            "node": "Send Pushover Alert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Error Message": {
      "main": [
        [
          {
            "node": "Send Error Alert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Price Monitor Webhook": {
      "main": [
        [
          {
            "node": "Product Configuration",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Product Configuration": {
      "main": [
        [
          {
            "node": "Loop Products",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Insert rows in a table": {
      "main": [
        [
          {
            "node": "Price Drop?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Transform & Enrich Data": {
      "main": [
        [
          {
            "node": "Insert rows in a table",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Combine Scrape & History": {
      "main": [
        [
          {
            "node": "Transform & Enrich Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}