{
  "id": "y0Yk7da21T4u9zlp",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Property Listing Aggregator with Mailchimp and Notion",
  "tags": [],
  "nodes": [
    {
      "id": "a214ca2e-1537-4fd8-9716-ec86aa74424d",
      "name": "Start Workflow",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [
        560,
        288
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "a7185b80-4df8-467e-9a77-84d1824c2f92",
      "name": "Prepare Target URLs",
      "type": "n8n-nodes-base.code",
      "position": [
        784,
        288
      ],
      "parameters": {
        "jsCode": "// Replace these example URLs with the commercial real-estate pages you actually want to monitor.\n// You can return as many items as you like \u2013 each will be scraped in turn.\nreturn [\n  { json: { url: 'https://example-commercial1.com/listings' } },\n  { json: { url: 'https://example-commercial2.com/properties' } }\n];"
      },
      "typeVersion": 2
    },
    {
      "id": "66e04716-64a8-42ef-bfc7-0389682881de",
      "name": "Loop URLs",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        1072,
        288
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "85a66c8c-1c28-4303-b807-532fb5bbf0b0",
      "name": "Scrape Listing Page",
      "type": "n8n-nodes-scrapegraphai.scrapegraphAi",
      "position": [
        1264,
        272
      ],
      "parameters": {
        "userPrompt": "Extract every commercial real-estate listing on this page and output an array called \"listings\". For each listing include: address, city, state, zip, price, size_sqft, availability_status, contact_name, phone, listing_url. Return ONLY valid JSON.",
        "websiteUrl": "={{ $json.url }}"
      },
      "typeVersion": 1
    },
    {
      "id": "6c3878bc-e437-4c03-8d57-a85def170968",
      "name": "Flatten Listings",
      "type": "n8n-nodes-base.code",
      "position": [
        1440,
        272
      ],
      "parameters": {
        "jsCode": "// Flatten the \"listings\" array so that each property becomes its own item.\nconst output = [];\nconst now = new Date().toISOString();\nfor (const item of $input.all()) {\n  const listings = item.json.listings || [];\n  for (const listing of listings) {\n    output.push({ json: { ...listing, scrapedAt: now } });\n  }\n}\nreturn output;"
      },
      "typeVersion": 2
    },
    {
      "id": "f221bf3b-2bf9-4e72-97c6-2bb373567c02",
      "name": "Loop Listings",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        1584,
        272
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "b6e42eea-26a2-45f0-81f5-b3db4267345e",
      "name": "Validate & Enrich",
      "type": "n8n-nodes-base.code",
      "position": [
        1856,
        336
      ],
      "parameters": {
        "jsCode": "/* Basic validation & enrichment */\ntry {\n  const item = $input.first().json;\n  if (!item.address || !item.price || !item.size_sqft) {\n    throw new Error('Missing critical fields');\n  }\n  const cleanPrice = parseFloat(String(item.price).replace(/[^0-9.]/g, ''));\n  const cleanSize = parseFloat(String(item.size_sqft).replace(/[^0-9.]/g, ''));\n  item.price = cleanPrice;\n  item.size_sqft = cleanSize;\n  item.pricePerSqft = cleanPrice && cleanSize ? +(cleanPrice / cleanSize).toFixed(2) : null;\n  item.validated = true;\n  return [{ json: item }];\n} catch (e) {\n  return [{ json: { error: true, message: e.message } }];\n}"
      },
      "typeVersion": 2
    },
    {
      "id": "678768c9-3cdc-4a15-b00f-d1ccfdb8df07",
      "name": "Has Error?",
      "type": "n8n-nodes-base.if",
      "position": [
        2000,
        336
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "conditions": [
            {
              "operator": {
                "type": "boolean",
                "operation": "true"
              },
              "leftValue": "={{ $json.error }}"
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "8129bff0-a3a7-4ece-b22b-5baa6d87e656",
      "name": "Log Error",
      "type": "n8n-nodes-base.code",
      "position": [
        2144,
        320
      ],
      "parameters": {
        "jsCode": "// Simply log or store the error somewhere else if you like.\nconsole.error('Listing failed validation', $json.message);\nreturn items;"
      },
      "typeVersion": 2
    },
    {
      "id": "3cbd1207-b0ad-430b-b0ed-ed870e72bf3c",
      "name": "Affordable?",
      "type": "n8n-nodes-base.if",
      "position": [
        2272,
        336
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "conditions": [
            {
              "operator": {
                "type": "number",
                "operation": "smaller"
              },
              "leftValue": "={{ $json.pricePerSqft }}",
              "rightValue": 30
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "765285a4-d225-4f09-aaee-6e3910773dbc",
      "name": "Save to Notion",
      "type": "n8n-nodes-base.notion",
      "position": [
        2560,
        336
      ],
      "parameters": {
        "pageId": {
          "__rl": true,
          "mode": "url",
          "value": ""
        },
        "options": {}
      },
      "typeVersion": 2
    },
    {
      "id": "cae964c1-b835-4c07-b209-0d7fc1dc6919",
      "name": "Prepare Mailchimp Content",
      "type": "n8n-nodes-base.set",
      "position": [
        2784,
        336
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "ee0c1f66-edbd-4126-981b-b964dd5e28cc",
      "name": "Create Campaign",
      "type": "n8n-nodes-base.mailchimp",
      "position": [
        2976,
        352
      ],
      "parameters": {
        "resource": "campaign",
        "operation": "create"
      },
      "typeVersion": 1
    },
    {
      "id": "b2cd1c5a-289e-4e37-8da2-73c25c8dbbb0",
      "name": "Set Campaign Content",
      "type": "n8n-nodes-base.mailchimp",
      "position": [
        3184,
        352
      ],
      "parameters": {
        "resource": "campaign",
        "operation": "setContent"
      },
      "typeVersion": 1
    },
    {
      "id": "a575f999-74b0-4313-8aee-2116f3423a55",
      "name": "Send Campaign",
      "type": "n8n-nodes-base.mailchimp",
      "position": [
        3392,
        352
      ],
      "parameters": {
        "resource": "campaign",
        "operation": "send",
        "campaignId": "={{ $json.id }}"
      },
      "typeVersion": 1
    },
    {
      "id": "46332a5a-a78a-4253-8d9c-bb5e6a8c7980",
      "name": "Workflow Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -32,
        -208
      ],
      "parameters": {
        "width": 550,
        "height": 738,
        "content": "## How it works\n\nThis workflow lets you keep an eye on commercial real-estate opportunities without manual searching. Start the workflow manually any time you need an update. The Code node lists target URLs, which are scraped one by one with ScrapeGraphAI. The scraped listings are flattened into individual items, validated, and enriched with a calculated price-per-square-foot value. Listings that fail validation are logged and discarded. Affordable properties (price per square foot below your threshold) are saved to Notion so you have a searchable database of options. Each new qualifying listing also triggers a Mailchimp campaign that immediately emails your subscribers with the property details\u2014all fully automated after you hit \u201cExecute\u201d.\n\n## Setup steps\n\n1. Add your ScrapeGraphAI API credentials\n2. Replace the example URLs in **Prepare Target URLs**\n3. Enter your Notion database ID in **Save to Notion**\n4. Supply your Mailchimp List ID and API credentials\n5. Adjust the affordability threshold in **Affordable?**\n6. Test the workflow with sample pages\n7. Share the Mailchimp signup form with interested business owners"
      },
      "typeVersion": 1
    },
    {
      "id": "3724941c-2536-42c1-9b6d-0567403ca6f2",
      "name": "Section \u2013 Trigger & Config",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        544,
        -112
      ],
      "parameters": {
        "color": 7,
        "width": 450,
        "height": 638,
        "content": "## Trigger & Configuration\n\nThis cluster kicks everything off. The **Start Workflow** manual trigger lets you run checks whenever you like\u2014perfect during development or for ad-hoc research. Immediately after, **Prepare Target URLs** outputs a list of commercial real-estate pages you want to monitor. Because the Code node returns one item per URL, we introduce **Loop URLs** to handle each address in sequence. Keeping configuration in one node makes the workflow easy to maintain; when a broker changes their page URL you edit a single array rather than hunting through multiple nodes. The Split-in-Batches node also gives fine-grained rate-control\u2014adjust batch size or add delays if a site complains about rapid requests. Together these three nodes form a clean, reusable entry point that separates configuration from business logic, ensuring you or a non-technical teammate can update targets without touching the rest of the automation."
      },
      "typeVersion": 1
    },
    {
      "id": "0ff7ae1f-c470-4d89-a800-f72a1d2f7f69",
      "name": "Section \u2013 Scraping",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1040,
        -96
      ],
      "parameters": {
        "color": 7,
        "width": 754,
        "height": 606,
        "content": "## Smart Scraping\n\nThe heart of the workflow lives here. **Scrape Listing Page** uses ScrapeGraphAI\u2019s natural-language prompt to pull structured data from unpredictable broker sites\u2014no brittle CSS selectors needed. After scraping, **Flatten Listings** normalises the response by turning the AI-generated array into individual n8n items so downstream logic can act on a single property at a time. This design isolates site-specific variability: if a website redesigns, you typically only tweak the prompt rather than rewrite parsing code. Because the scrape might return dozens of listings at once, we introduce **Loop Listings** to batch-process each property. Splitting here also unlocks parallel execution in future versions, keeping run-times low even when you monitor many markets. Finally, everything flows neatly into validation, ensuring bad or incomplete records are caught early."
      },
      "typeVersion": 1
    },
    {
      "id": "579a4d9c-2bd7-4d5d-accf-076b4ed85d53",
      "name": "Section \u2013 Validation & Rules",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1824,
        -80
      ],
      "parameters": {
        "color": 7,
        "width": 594,
        "height": 622,
        "content": "## Data Validation & Filtering\n\nQuality beats quantity when you are hunting for serious commercial space. **Validate & Enrich** cleans numeric fields, calculates price-per-square-foot, and flags missing data. The next **Has Error?** gate drops anything that fails validation, preventing garbage from polluting your Notion workspace or bothering subscribers. After that, **Affordable?** enforces your investment criteria\u2014in this template a default threshold of $30/ft\u00b2. Change the value to match your market. You\u2019ll notice two small Code nodes\u2014**Log Error** and **Skip Non-Qualifying**\u2014that safely end branches without breaking the primary execution chain. These seemingly minor safeguards make troubleshooting easier and stop accidental loops that could overload external services. Together, this block guarantees that only clean, actionable opportunities reach the storage and notification layers."
      },
      "typeVersion": 1
    },
    {
      "id": "9694527d-c823-4af4-a3d2-9c11e58e6979",
      "name": "Section \u2013 Storage & Notification",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2448,
        -80
      ],
      "parameters": {
        "color": 7,
        "width": 1122,
        "height": 638,
        "content": "## Storage & Alerts\n\nOnce a listing clears validation, **Save to Notion** creates a new database entry that your whole team can browse, filter, or comment on. The property becomes part of a living knowledge base\u2014no spreadsheets required. Right after storage, **Prepare Mailchimp Content** crafts an HTML snippet using n8n expressions, embedding the address, size, and price into a ready-to-send template. The trio of Mailchimp nodes then automates outreach: **Create Campaign** spins up a one-off email, **Set Campaign Content** injects the HTML, and **Send Campaign** fires the alert to everyone subscribed to your list. Separating these steps keeps your email history in Mailchimp for compliance while still giving you complete control over content. Need SMS later? Swap out Mailchimp for Twilio here without touching the earlier sections\u2014another perk of modular design."
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "80e2f02b-d6cd-4970-9a24-1b76e4584e7a",
  "connections": {
    "Log Error": {
      "main": [
        [
          {
            "node": "Affordable?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop URLs": {
      "main": [
        [
          {
            "node": "Scrape Listing Page",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has Error?": {
      "main": [
        [
          {
            "node": "Log Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Affordable?": {
      "main": [
        [
          {
            "node": "Save to Notion",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Listings": {
      "main": [
        [
          {
            "node": "Validate & Enrich",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save to Notion": {
      "main": [
        [
          {
            "node": "Prepare Mailchimp Content",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Start Workflow": {
      "main": [
        [
          {
            "node": "Prepare Target URLs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Campaign": {
      "main": [
        [
          {
            "node": "Set Campaign Content",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flatten Listings": {
      "main": [
        [
          {
            "node": "Loop Listings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate & Enrich": {
      "main": [
        [
          {
            "node": "Has Error?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Target URLs": {
      "main": [
        [
          {
            "node": "Loop URLs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Scrape Listing Page": {
      "main": [
        [
          {
            "node": "Flatten Listings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Campaign Content": {
      "main": [
        [
          {
            "node": "Send Campaign",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Mailchimp Content": {
      "main": [
        [
          {
            "node": "Create Campaign",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}