AutomationFlowsData & Sheets › Aggregate Commercial Property Listings with Scrapegraphai, Baserow and Teams

Aggregate Commercial Property Listings with Scrapegraphai, Baserow and Teams

Byvinci-king-01 @vinci-king-01 on n8n.io

⚠️ COMMUNITY TEMPLATE DISCLAIMER: This is a community-contributed template that uses ScrapeGraphAI (a community node). Please ensure you have the ScrapeGraphAI community node installed in your n8n instance before using this template.

Cron / scheduled trigger★★★★☆ complexity24 nodesN8N Nodes ScrapegraphaiBaserowMicrosoft Teams
Data & Sheets Trigger: Cron / scheduled Nodes: 24 Complexity: ★★★★☆ Added:

This workflow corresponds to n8n.io template #12233 — we link there as the canonical source.

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 →

Download .json
{
  "id": "y0Yk7da21T4u9zlp",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Property Listing Aggregator with Microsoft Teams and Baserow",
  "tags": [],
  "nodes": [
    {
      "id": "4019642e-afcb-4f3b-8e5d-0b41b291f64c",
      "name": "Workflow Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -752,
        192
      ],
      "parameters": {
        "width": 528,
        "height": 704,
        "content": "## How it works\n\nThis workflow checks multiple commercial-real-estate portals once a week and delivers a consolidated stream of fresh or updated listings right inside a Microsoft Teams channel. A schedule trigger starts the run, then a Code node provides the list of search URLs you want to monitor. The list is split so each page can be scraped in parallel by ScrapeGraphAI, which returns structured JSON for every property it finds. A Merge node combines the separate streams and a second Code node normalises the data into a single flat list of listings. Each listing is compared against what already lives in Baserow; brand-new rows are inserted while changed rows are updated. Whenever a create or update happens, a short HTML message is crafted and pushed to Teams so stakeholders stay informed without leaving chat.\n\n## Setup steps\n\n1. Add your ScrapeGraphAI API credential.\n2. Put the real estate search URLs in the \u201cPrepare URL List\u201d Code node.\n3. Create a Baserow database and note the **Application ID** and **Table ID**.\n4. Map each Baserow field to the correct property in the \u201cCreate/Update Row\u201d nodes.\n5. Connect a Microsoft Teams OAuth2 credential and supply your Team + Channel IDs.\n6. Enable the workflow and run once manually to populate initial rows.\n7. Leave it active to receive weekly space availability alerts."
      },
      "typeVersion": 1
    },
    {
      "id": "44af3dd6-f701-4bd4-84d5-0de9521fbe1e",
      "name": "Section \u2013 Trigger & URL Prep",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -160,
        80
      ],
      "parameters": {
        "color": 7,
        "width": 530,
        "height": 718,
        "content": "## Trigger & URL Preparation\n\nA weekly **Schedule Trigger** kicks everything off so you never have to remember to run the workflow. Adjust the interval to suit your market\u2019s pace\u2014weekly is a good default for commercial space. The adjacent **Prepare URL List** Code node is the only place you typically edit when you want to target new brokers or alter search filters. It returns one item per URL so downstream nodes can process each website independently. Placing URLs in code keeps them version-controlled and prevents accidental changes in the UI. Remember to keep your list focused; many large portals paginate results aggressively and you can hit both their rate limits and your own ScrapeGraphAI quota if you paste dozens of links here. Stick to broad category or city search pages and let the AI handle pagination when needed."
      },
      "typeVersion": 1
    },
    {
      "id": "efd5c267-c144-435b-85d9-0ff60bd27e91",
      "name": "Section \u2013 Parallel Scraping",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        432,
        128
      ],
      "parameters": {
        "color": 7,
        "width": 562,
        "height": 670,
        "content": "## Parallel Scraping\n\nThe **Split in Batches** node takes the URL items one at a time but allows asynchronous execution so multiple pages can be scraped in parallel, shortening total runtime. **ScrapeGraphAI** does the heavy lifting: it renders each page (JavaScript included) and extracts key fields using your natural-language prompt. Because every site returns slightly different HTML, the AI\u2019s semantic approach is far more resilient than brittle CSS selectors. Output is a JSON object with a `listings` array. The **Merge (Combine)** node reunites the individual scrape results, ensuring that the following processing stage sees one consolidated data stream. If you add additional sources later, you only extend the URL list\u2014no structural workflow change needed."
      },
      "typeVersion": 1
    },
    {
      "id": "a5c0d10f-53d6-460e-a8aa-e48377d5dcab",
      "name": "Section \u2013 Normalisation & Flattening",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1024,
        128
      ],
      "parameters": {
        "color": 7,
        "width": 514,
        "height": 670,
        "content": "## Normalisation & Flattening\n\nDifferent portals phrase fields differently\u2014some use `rent`, some `price`, others hide size in text. The **Normalise Listings** Code node flattens every raw listing into a predictable schema: `listing_id`, `address`, `price`, `size_sqft`, `listing_url`, broker details, availability date, and a timestamp. This guarantees that Baserow fields stay tidy and that downstream comparison logic works reliably. It also stamps each row with the source URL so you can trace any weird value back to its origin during audits. Invalid or missing numbers are converted to `null` rather than empty strings, making later numeric filtering easier. The node finally returns one item per listing\u2014perfect for looping into storage operations."
      },
      "typeVersion": 1
    },
    {
      "id": "b0ee79e1-8f06-4500-8703-64e9e1b7ea4b",
      "name": "Section \u2013 Deduplication & Storage",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1568,
        128
      ],
      "parameters": {
        "color": 7,
        "width": 514,
        "height": 670,
        "content": "## Deduplication & Storage\n\nEach listing item first queries Baserow with an exact-match filter on `listing_id`. If nothing comes back, we create a brand-new row. When a row exists, a small Code node compares price, size, and availability to decide whether an update is warranted, preventing noisy \u201achange\u2019 notifications when nothing meaningful moved. Both **Create Row** and **Update Row** nodes respect your chosen field order so the database remains human-readable. Using Baserow keeps your stack open-source and offers a simple REST API if you want to share the data with BI tools or embed it in a website later. All write operations use credentials\u2014no keys in plain sight\u2014so you pass the security review on day one."
      },
      "typeVersion": 1
    },
    {
      "id": "aac64391-d5aa-44a2-ac1f-4eaa2a542a46",
      "name": "Section \u2013 Teams Notifications",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2128,
        176
      ],
      "parameters": {
        "color": 7,
        "width": 802,
        "height": 734,
        "content": "## Teams Notifications\n\nBusiness users rarely check databases, so actionable alerts land straight in Microsoft Teams. Two short **Set** nodes craft HTML messages\u2014one for new listings, one for updates\u2014highlighting address, price and size at a glance. The **Microsoft Teams** nodes then post to your chosen channel using OAuth2 credentials, which means users see the bot name instead of a generic webhook. Hyperlinks drive recipients back to the original broker page so they can call or email immediately. If you later need silent mode you can simply disable these connections without stopping the rest of the workflow, proving why separating storage and notification logic is worth the extra nodes."
      },
      "typeVersion": 1
    },
    {
      "id": "f8d03eae-3358-45e0-bb77-da26a38ea657",
      "name": "Weekly Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -144,
        384
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "weeks"
            }
          ]
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "85b31064-b13d-48f6-922f-0ef2442aed95",
      "name": "Prepare URL List",
      "type": "n8n-nodes-base.code",
      "position": [
        64,
        384
      ],
      "parameters": {
        "jsCode": "const urls = [\n  \"https://www.commercialsite1.com/office-space/xyz\",\n  \"https://www.commercialsite2.com/listings/xyz\",\n  \"https://brokerportal.example.com/spaces?city=xyz&type=office\"\n];\nreturn urls.map(url => ({ json: { url } }));"
      },
      "typeVersion": 2
    },
    {
      "id": "6b1d6abb-7963-463a-ac5a-92396cbb32cf",
      "name": "Split URLs",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        272,
        384
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "d968b929-d7a7-4ca7-af1f-45d2f61bb892",
      "name": "Scrape Listings",
      "type": "n8n-nodes-scrapegraphai.scrapegraphAi",
      "position": [
        496,
        544
      ],
      "parameters": {
        "userPrompt": "Extract all commercial real estate listings from this page. Return JSON with a top-level key \\\"listings\\\" which is an array of objects, each containing: id, address, price, size_sqft, listing_url, broker_name, broker_phone, availability_date. Ensure numeric values are numbers and dates use ISO 8601.",
        "websiteUrl": "={{ $json.url }}"
      },
      "typeVersion": 1
    },
    {
      "id": "8c5b3453-6e22-437c-bd79-76f625015048",
      "name": "Collect Listings",
      "type": "n8n-nodes-base.merge",
      "position": [
        656,
        560
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "mergeByFields": {
          "values": [
            {}
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "e23f0bc1-9fc0-43da-a075-f68acc306b54",
      "name": "Normalise Listings",
      "type": "n8n-nodes-base.code",
      "position": [
        816,
        544
      ],
      "parameters": {
        "jsCode": "// Flatten and normalise listings\nconst items = $input.all();\nlet listings = [];\nfor (const item of items) {\n  const sourceUrl = item.json.url || item.json.source || '';\n  const results = item.json.listings || [];\n  for (const entry of results) {\n    listings.push({\n      json: {\n        listing_id: entry.id || entry.listing_id || entry.listing_url || entry.url,\n        address: entry.address || '',\n        price: entry.price || null,\n        size_sqft: entry.size_sqft || null,\n        listing_url: entry.listing_url || entry.url || '',\n        broker_name: entry.broker_name || '',\n        broker_phone: entry.broker_phone || '',\n        availability_date: entry.availability_date || null,\n        source: sourceUrl,\n        scraped_at: new Date().toISOString()\n      }\n    });\n  }\n}\nreturn listings;"
      },
      "typeVersion": 2
    },
    {
      "id": "5c0d5533-d5a4-4fcc-a61c-36eba4aa063f",
      "name": "Loop Listings",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        1072,
        496
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "52da4f99-1c45-4972-938c-0cb03a8adcfa",
      "name": "Check Existing (Baserow List)",
      "type": "n8n-nodes-base.baserow",
      "position": [
        1232,
        480
      ],
      "parameters": {
        "tableId": "={{ $env.BASEROW_TABLE_ID || '1' }}",
        "operation": "list"
      },
      "typeVersion": 1
    },
    {
      "id": "64e13c3c-2524-46d6-a50a-284b2253de58",
      "name": "Merge Listing & Result",
      "type": "n8n-nodes-base.merge",
      "position": [
        1376,
        480
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "mergeByFields": {
          "values": [
            {}
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "57bdb17e-496c-4501-becf-8ae991c284d9",
      "name": "Determine Action",
      "type": "n8n-nodes-base.code",
      "position": [
        1616,
        528
      ],
      "parameters": {
        "jsCode": "const [listingData, baserowResponse] = $input.all().map(i => i.json);\nconst existing = Array.isArray(baserowResponse.results) ? baserowResponse.results : [];\nlet action = 'create';\nlet rowId = null;\nif (existing.length > 0) {\n  rowId = existing[0].id;\n  const changed = existing[0].price !== listingData.price || existing[0].size_sqft !== listingData.size_sqft || existing[0].availability_date !== listingData.availability_date;\n  action = changed ? 'update' : 'skip';\n}\nreturn [{ json: { ...listingData, _action: action, _rowId: rowId } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "797e28cc-e1c1-4595-bf02-8524a3076365",
      "name": "Need Create?",
      "type": "n8n-nodes-base.if",
      "position": [
        1792,
        528
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "string": [
            {
              "value1": "={{ $json._action }}",
              "value2": "create",
              "operation": "equals"
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "77b6b233-d32d-4250-a207-f16e73352f7b",
      "name": "Create Row",
      "type": "n8n-nodes-base.baserow",
      "position": [
        2320,
        416
      ],
      "parameters": {
        "tableId": "={{ $env.BASEROW_TABLE_ID || '1' }}",
        "operation": "create"
      },
      "typeVersion": 1
    },
    {
      "id": "d0e7cbe5-805b-4f22-b8c4-d11f525c0dfb",
      "name": "Create Message",
      "type": "n8n-nodes-base.set",
      "position": [
        2496,
        400
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "75550ae7-12be-4d66-83a4-46915385d4fb",
      "name": "Teams \u2013 New Listing",
      "type": "n8n-nodes-base.microsoftTeams",
      "position": [
        2688,
        416
      ],
      "parameters": {
        "chatId": {
          "__rl": true,
          "mode": "list",
          "value": ""
        },
        "message": "={{ $json.message }}",
        "options": {},
        "resource": "chatMessage",
        "contentType": "html"
      },
      "typeVersion": 2
    },
    {
      "id": "d31b9b56-da03-4848-878f-c903b42fd4f0",
      "name": "Need Update?",
      "type": "n8n-nodes-base.if",
      "position": [
        1952,
        528
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "string": [
            {
              "value1": "={{ $json._action }}",
              "value2": "update",
              "operation": "equals"
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "caa31adb-d387-4aa3-a54f-8096d6bfd2b5",
      "name": "Update Row",
      "type": "n8n-nodes-base.baserow",
      "position": [
        2208,
        688
      ],
      "parameters": {
        "rowId": "={{ $json._rowId }}",
        "tableId": "={{ $env.BASEROW_TABLE_ID || '1' }}",
        "operation": "update"
      },
      "typeVersion": 1
    },
    {
      "id": "9f1de02b-bc08-4634-a669-297e5372ba56",
      "name": "Update Message",
      "type": "n8n-nodes-base.set",
      "position": [
        2416,
        688
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "83e0771c-2ba2-47df-a330-94bb3eb9d085",
      "name": "Teams \u2013 Update",
      "type": "n8n-nodes-base.microsoftTeams",
      "position": [
        2672,
        688
      ],
      "parameters": {
        "chatId": {
          "__rl": true,
          "mode": "list",
          "value": ""
        },
        "message": "={{ $json.message }}",
        "options": {},
        "resource": "chatMessage",
        "contentType": "html"
      },
      "typeVersion": 2
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "70809248-7642-4baf-b0dc-436bb7a368aa",
  "connections": {
    "Create Row": {
      "main": [
        [
          {
            "node": "Create Message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split URLs": {
      "main": [
        [
          {
            "node": "Scrape Listings",
            "type": "main",
            "index": 0
          },
          {
            "node": "Collect Listings",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Update Row": {
      "main": [
        [
          {
            "node": "Update Message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Need Create?": {
      "main": [
        [
          {
            "node": "Create Row",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Need Update?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Need Update?": {
      "main": [
        [
          {
            "node": "Update Row",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Listings": {
      "main": [
        [
          {
            "node": "Check Existing (Baserow List)",
            "type": "main",
            "index": 0
          },
          {
            "node": "Merge Listing & Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Message": {
      "main": [
        [
          {
            "node": "Teams \u2013 New Listing",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Update Message": {
      "main": [
        [
          {
            "node": "Teams \u2013 Update",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Weekly Trigger": {
      "main": [
        [
          {
            "node": "Prepare URL List",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Scrape Listings": {
      "main": [
        [
          {
            "node": "Collect Listings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Collect Listings": {
      "main": [
        [
          {
            "node": "Normalise Listings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Determine Action": {
      "main": [
        [
          {
            "node": "Need Create?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare URL List": {
      "main": [
        [
          {
            "node": "Split URLs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalise Listings": {
      "main": [
        [
          {
            "node": "Loop Listings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge Listing & Result": {
      "main": [
        [
          {
            "node": "Determine Action",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Existing (Baserow List)": {
      "main": [
        [
          {
            "node": "Merge Listing & Result",
            "type": "main",
            "index": 1
          }
        ]
      ]
    }
  }
}
Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

⚠️ COMMUNITY TEMPLATE DISCLAIMER: This is a community-contributed template that uses ScrapeGraphAI (a community node). Please ensure you have the ScrapeGraphAI community node installed in your n8n instance before using this template.

Source: https://n8n.io/workflows/12233/ — original creator credit. Request a take-down →

More Data & Sheets workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

Data & Sheets

⚠️ COMMUNITY TEMPLATE DISCLAIMER: This is a community-contributed template that uses ScrapeGraphAI (a community node). Please ensure you have the ScrapeGraphAI community node installed in your n8n ins

N8N Nodes Scrapegraphai, HTTP Request, Notion +1
Data & Sheets

Collects crypto and/or stock market headlines from multiple sources: CoinDesk, CoinTelegraph, Google News, and X (via an RSS proxy). Normalizes all items into a consistent structure with fields like ,

RSS Feed Read, HTTP Request
Data & Sheets

This workflow pulls news articles from NewsAPI, Mediastack, and CurrentsAPI on a scheduled basis.

Noco Db, HTTP Request
Data & Sheets

I prepared a detailed guide that showed the whole process of integrating the Binance API and storing data in Airtable to manage funding statements associated with tokens in a wallet.

Crypto, HTTP Request, Airtable
Data & Sheets

Stop wasting hours on manual dialing and listening to ringtones. This workflow transforms your Airtable into a high-velocity AI Call Center using Vapi AI**.

Airtable, HTTP Request