AutomationFlowsAI & RAG › Extract Real Estate Listings with Snapshot Site, Openai, and Google Sheets

Extract Real Estate Listings with Snapshot Site, Openai, and Google Sheets

BySnapshot Site @snapshotsite on n8n.io

This workflow runs daily, reads real estate listing URLs from Google Sheets, captures rendered HTML with Snapshot Site, extracts structured listing details using OpenAI via n8n’s Information Extractor, and writes deduplicated results back to Google Sheets while logging capture…

Cron / scheduled trigger★★★★☆ complexityAI-powered17 nodesGoogle SheetsN8N Nodes Snapshot SiteInformation ExtractorOpenAI Chat
AI & RAG Trigger: Cron / scheduled Nodes: 17 Complexity: ★★★★☆ AI nodes: yes Added:

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

This workflow follows the Google Sheets → Informationextractor 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 →

Download .json
{
  "name": "Real Estate Listings Extractor \u2192 Google Sheets",
  "nodes": [
    {
      "id": "sticky-doc",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -880,
        -460
      ],
      "parameters": {
        "width": 480,
        "height": 880,
        "content": "## Real Estate Listings Extractor \u2192 Google Sheets\n\n### How it works\n\nThis workflow runs daily and reads a list of property listing page URLs from a Google Sheet. For each URL, it captures the fully-rendered page HTML with Snapshot Site, cleans it, and passes it to an AI Information Extractor that pulls out structured listing fields. Extracted listings are appended to a results sheet (deduplicated by listing URL); comparison failures are logged to an errors sheet instead of stopping the run.\n\n### Compliance note\n\nOnly extract data from property listing pages you're allowed to process \u2014 check the source site's terms of service and robots directives before pointing this workflow at a new portal.\n\n### Setup steps\n\n- Install the `n8n-nodes-snapshot-site` community node (Settings \u2192 Community nodes \u2192 Install \u2192 `n8n-nodes-snapshot-site`) \u2014 verified by n8n.\n- Add a Snapshot Site credential, a Google Sheets credential, and an AI chat model credential (e.g. OpenAI) for the Information Extractor.\n- Create a source sheet with a `url` column (one listing page per row) and a results sheet with columns matching the extracted fields (see the workflow README for the full list).\n- Paste this exact header row into row 1 of the **Listings** tab (tab-separated, one column per cell): `listingTitle\tpropertyType\tsaleOrRent\tprice\tcurrency\tsurface\tlandSize\trooms\tbedrooms\tbathrooms\taddress\tpostalCode\tcity\tcountry\tenergyRating\tagency\tpublicationDate\tlistingUrl\timageUrl\tdescription`\n- Paste this header row into row 1 of the **Extraction Errors** tab: `url\tmessage\toccurredAt`\n- After importing, open the two Google Sheets write nodes (Log Extraction Error, Save Listings) and switch Mapping Column Mode from \"Map Each Column Manually\" to \"Map Automatically\" \u2014 it always imports blank regardless of the JSON, but since the sheet headers above already match the incoming field names, this needs no manual typing.\n- Point the two Google Sheets nodes at your spreadsheet and adjust the schedule.\n\n### Customization\n\nAdd more source columns (e.g. a `neighborhood` tag), swap the AI model, or point the same Clean HTML \u2192 Extractor pattern at a different page type \u2014 this is the base pattern for every extraction workflow in this collection."
      },
      "typeVersion": 1
    },
    {
      "id": "sticky-zone-loop",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -360,
        -180
      ],
      "parameters": {
        "color": 7,
        "width": 820,
        "height": 380,
        "content": "## Read sources and loop\n\nReads every property listing page URL from the source sheet and processes them one at a time."
      },
      "typeVersion": 1
    },
    {
      "id": "sticky-zone-capture",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        500,
        -220
      ],
      "parameters": {
        "color": 7,
        "width": 780,
        "height": 720,
        "content": "## Capture and clean\n\nCaptures the fully-rendered HTML, checks for API errors, and strips scripts/styles before the AI step."
      },
      "typeVersion": 1
    },
    {
      "id": "sticky-zone-extract",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1340,
        -180
      ],
      "parameters": {
        "color": 7,
        "width": 780,
        "height": 600,
        "content": "## Extract and save\n\nExtracts structured listing fields with AI, splits the array into individual rows, and appends them to the results sheet."
      },
      "typeVersion": 1
    },
    {
      "id": "schedule-trigger",
      "name": "When Every Day at 2PM",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -280,
        60
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "days",
              "daysInterval": 1,
              "triggerAtHour": 14
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "read-property-sources",
      "name": "Read Property Sources",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        0,
        60
      ],
      "parameters": {
        "resource": "sheet",
        "operation": "read",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Property Sources"
        },
        "documentId": {
          "__rl": true,
          "mode": "url",
          "value": "https://docs.google.com/spreadsheets/d/PLACEHOLDER/edit"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "loop-over-sources",
      "name": "Loop Over Property Sources",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        280,
        60
      ],
      "parameters": {
        "options": {},
        "batchSize": 1
      },
      "typeVersion": 3
    },
    {
      "id": "all-sources-processed",
      "name": "All Sources Processed",
      "type": "n8n-nodes-base.noOp",
      "position": [
        560,
        -100
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "get-page-html",
      "name": "Get Page HTML",
      "type": "n8n-nodes-snapshot-site.snapshotSite",
      "onError": "continueErrorOutput",
      "maxTries": 3,
      "position": [
        560,
        140
      ],
      "parameters": {
        "url": "={{ $json.url }}",
        "operation": "screenshot",
        "additionalFields": {
          "delay": 5,
          "format": "html",
          "fullSize": true
        }
      },
      "credentials": {
        "snapshotSiteApi": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "typeVersion": 1,
      "waitBetweenTries": 5000
    },
    {
      "id": "if-api-error",
      "name": "Check for API Error",
      "type": "n8n-nodes-base.if",
      "position": [
        840,
        140
      ],
      "parameters": {
        "conditions": {
          "boolean": [
            {
              "value1": "={{ $json.error === true }}",
              "value2": true
            }
          ]
        }
      },
      "typeVersion": 1
    },
    {
      "id": "build-error-record",
      "name": "Build Error Record",
      "type": "n8n-nodes-base.set",
      "position": [
        840,
        340
      ],
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "name": "url",
              "type": "string",
              "value": "={{ $('Loop Over Property Sources').item.json.url }}"
            },
            {
              "name": "message",
              "type": "string",
              "value": "={{ $json.error?.message || $json.message }}"
            },
            {
              "name": "occurredAt",
              "type": "string",
              "value": "={{ $now.toISO() }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "log-extraction-error",
      "name": "Log Extraction Error",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        1120,
        340
      ],
      "parameters": {
        "columns": {
          "value": {},
          "schema": [],
          "mappingMode": "autoMapInputData",
          "matchingColumns": []
        },
        "resource": "sheet",
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Extraction Errors"
        },
        "documentId": {
          "__rl": true,
          "mode": "url",
          "value": "https://docs.google.com/spreadsheets/d/PLACEHOLDER/edit"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "clean-html",
      "name": "Clean HTML",
      "type": "n8n-nodes-base.code",
      "position": [
        1120,
        60
      ],
      "parameters": {
        "jsCode": "const MAX_CHARS = 100000;\nconst raw = $input.item.json.html || '';\nlet cleaned = raw\n  .replace(/<script[\\s\\S]*?<\\/script>/gi, '')\n  .replace(/<style[\\s\\S]*?<\\/style>/gi, '')\n  .replace(/<svg[\\s\\S]*?<\\/svg>/gi, '')\n  .replace(/<!--[\\s\\S]*?-->/g, '')\n  .replace(/\\s+/g, ' ')\n  .trim();\nif (cleaned.length > MAX_CHARS) {\n  cleaned = cleaned.slice(0, MAX_CHARS);\n}\nreturn [{ json: { url: $input.item.json.url, cleanedHtml: cleaned } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "extract-listing-fields",
      "name": "Extract Listing Fields",
      "type": "@n8n/n8n-nodes-langchain.informationExtractor",
      "position": [
        1400,
        60
      ],
      "parameters": {
        "text": "={{ $json.cleanedHtml }}",
        "schemaType": "manual",
        "inputSchema": "{\n  \"type\": \"object\",\n  \"properties\": {\n    \"listings\": {\n      \"type\": \"array\",\n      \"items\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"listingTitle\": { \"type\": \"string\" },\n          \"propertyType\": { \"type\": \"string\" },\n          \"saleOrRent\": { \"type\": \"string\" },\n          \"price\": { \"type\": \"string\" },\n          \"currency\": { \"type\": \"string\" },\n          \"surface\": { \"type\": \"string\" },\n          \"landSize\": { \"type\": \"string\" },\n          \"rooms\": { \"type\": \"string\" },\n          \"bedrooms\": { \"type\": \"string\" },\n          \"bathrooms\": { \"type\": \"string\" },\n          \"address\": { \"type\": \"string\" },\n          \"postalCode\": { \"type\": \"string\" },\n          \"city\": { \"type\": \"string\" },\n          \"country\": { \"type\": \"string\" },\n          \"energyRating\": { \"type\": \"string\" },\n          \"agency\": { \"type\": \"string\" },\n          \"publicationDate\": { \"type\": \"string\" },\n          \"listingUrl\": { \"type\": \"string\" },\n          \"imageUrl\": { \"type\": \"string\" },\n          \"description\": { \"type\": \"string\" }\n        }\n      }\n    }\n  }\n}"
      },
      "typeVersion": 1
    },
    {
      "id": "extraction-model",
      "name": "Extraction Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "position": [
        1400,
        260
      ],
      "parameters": {
        "model": "gpt-4o-mini",
        "options": {}
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "split-listings",
      "name": "Split Listings",
      "type": "n8n-nodes-base.splitOut",
      "position": [
        1680,
        60
      ],
      "parameters": {
        "fieldToSplitOut": "output.listings"
      },
      "typeVersion": 1
    },
    {
      "id": "save-listings",
      "name": "Save Listings",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        1960,
        60
      ],
      "parameters": {
        "columns": {
          "value": {},
          "schema": [],
          "mappingMode": "autoMapInputData",
          "matchingColumns": [
            "listingUrl"
          ]
        },
        "resource": "sheet",
        "operation": "appendOrUpdate",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Listings"
        },
        "documentId": {
          "__rl": true,
          "mode": "url",
          "value": "https://docs.google.com/spreadsheets/d/PLACEHOLDER/edit"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.5
    }
  ],
  "active": false,
  "settings": {
    "timezone": "UTC",
    "executionOrder": "v1",
    "executionTimeout": 300,
    "saveManualExecutions": true
  },
  "connections": {
    "Clean HTML": {
      "main": [
        [
          {
            "node": "Extract Listing Fields",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Page HTML": {
      "main": [
        [
          {
            "node": "Check for API Error",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Build Error Record",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save Listings": {
      "main": [
        [
          {
            "node": "Loop Over Property Sources",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split Listings": {
      "main": [
        [
          {
            "node": "Save Listings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extraction Model": {
      "ai_languageModel": [
        [
          {
            "node": "Extract Listing Fields",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Build Error Record": {
      "main": [
        [
          {
            "node": "Log Extraction Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check for API Error": {
      "main": [
        [
          {
            "node": "Build Error Record",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Clean HTML",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Log Extraction Error": {
      "main": [
        [
          {
            "node": "Loop Over Property Sources",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Property Sources": {
      "main": [
        [
          {
            "node": "Loop Over Property Sources",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When Every Day at 2PM": {
      "main": [
        [
          {
            "node": "Read Property Sources",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Listing Fields": {
      "main": [
        [
          {
            "node": "Split Listings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Over Property Sources": {
      "main": [
        [
          {
            "node": "All Sources Processed",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Get Page HTML",
            "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.

Pro

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

About this workflow

This workflow runs daily, reads real estate listing URLs from Google Sheets, captures rendered HTML with Snapshot Site, extracts structured listing details using OpenAI via n8n’s Information Extractor, and writes deduplicated results back to Google Sheets while logging capture…

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

More AI & RAG workflows → · Browse all categories →

Related workflows

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

AI & RAG

This workflow runs daily, reads business-directory page URLs from Google Sheets, captures fully rendered HTML via Snapshot Site, uses OpenAI (via n8n’s Information Extractor) to extract structured bus

Google Sheets, N8N Nodes Snapshot Site, Information Extractor +1
AI & RAG

This workflow runs daily, captures fully rendered careers pages with Snapshot Site, uses an OpenAI chat model to extract structured job-offer fields, and writes the results to Google Sheets while logg

Google Sheets, N8N Nodes Snapshot Site, Information Extractor +1
AI & RAG

Sign up for Decodo — get better pricing here

@Decodo/N8N Nodes Decodo, Information Extractor, OpenAI Chat +4
AI & RAG

Eliminate 90% of manual work in procurement by automating quote requests, response tracking, price extraction, and supplier follow-ups. This complete automation handles everything from sending persona

Gmail, Google Sheets, Information Extractor +2
AI & RAG

Transform your Gmail sent folder into a comprehensive, enriched contact database automatically. This workflow processes hundreds or thousands of sent emails, extracting and enriching contact informati

HTTP Request, @Brave/N8N Nodes Brave Search, Gmail +3