{
  "name": "Saleroom Lot Scraper",
  "nodes": [
    {
      "parameters": {
        "event": "pageUpdatedInDatabase",
        "databaseId": {
          "__rl": true,
          "value": "REPLACE_WITH_YOUR_DATABASE_ID",
          "mode": "id"
        },
        "pollTimes": {
          "item": [
            {
              "mode": "everyMinute"
            }
          ]
        },
        "simple": true,
        "options": {}
      },
      "id": "a1000000-0000-0000-0000-000000000001",
      "name": "Notion Trigger",
      "type": "n8n-nodes-base.notionTrigger",
      "typeVersion": 1,
      "position": [
        0,
        0
      ],
      "credentials": {
        "notionApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "leftValue": "={{ $json['Lot URL'] }}",
              "rightValue": "",
              "operator": {
                "type": "string",
                "operation": "notEmpty",
                "singleValue": true
              }
            },
            {
              "leftValue": "={{ $json.Status }}",
              "rightValue": "Done",
              "operator": {
                "type": "string",
                "operation": "notEquals"
              }
            },
            {
              "leftValue": "={{ $json.Status }}",
              "rightValue": "Error",
              "operator": {
                "type": "string",
                "operation": "notEquals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "a1000000-0000-0000-0000-000000000002",
      "name": "Needs Scraping?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        220,
        0
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://REPLACE-WITH-YOUR-SCRAPER.onrender.com/scrape",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "X-API-Key",
              "value": "REPLACE_WITH_YOUR_SCRAPER_API_KEY"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ url: $json['Lot URL'] }) }}",
        "options": {
          "timeout": 300000
        }
      },
      "id": "a1000000-0000-0000-0000-000000000003",
      "name": "Scrape Lot",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        440,
        0
      ],
      "retryOnFail": true,
      "maxTries": 2,
      "waitBetweenTries": 5000,
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "jsCode": "// \"Build Notion Payloads\" Code node (n8n, Run Once for All Items).\n// Input: the Scrape Lot response. Output: one item PER BLOCK BATCH (Notion\n// caps block appends at 100/request), each carrying the same pageId and\n// propertiesPayload. The \"Update Properties\" node must have Settings ->\n// \"Execute Once\" = ON; \"Write Page Body\" runs once per item (per batch).\n\nconst scraped = $input.first().json;\nconst pageId = $('Notion Trigger').first().json.id;\n\n// Notion caps each rich_text item at 2000 chars \u2014 chunk long text.\nconst chunk = (s, size = 1900) => {\n  const out = [];\n  for (let i = 0; i < s.length; i += size) out.push(s.slice(i, i + size));\n  return out;\n};\nconst rt = (s) => ({ type: 'text', text: { content: s } });\n\n// Inline markdown: supports **bold** only (enough for the report format).\nconst rich = (s) => {\n  const parts = [];\n  s.split('**').forEach((seg, i) => {\n    if (!seg) return;\n    for (const piece of chunk(seg)) {\n      const item = { type: 'text', text: { content: piece } };\n      if (i % 2 === 1) item.annotations = { bold: true };\n      parts.push(item);\n    }\n  });\n  return parts.length ? parts : [rt(' ')];\n};\n\nconst paragraphs = (s) => chunk(s).map((c) => ({\n  object: 'block', type: 'paragraph', paragraph: { rich_text: [rt(c)] },\n}));\nconst divider = () => ({ object: 'block', type: 'divider', divider: {} });\nconst heading = (level, s) => ({\n  object: 'block', type: `heading_${level}`, [`heading_${level}`]: { rich_text: rich(s) },\n});\n\n// Line-based markdown -> Notion blocks for the AI report.\nconst mdToBlocks = (md) => {\n  const blocks = [];\n  for (const raw of md.split('\\n')) {\n    const line = raw.trim();\n    if (!line) continue;\n    if (/^-{3,}$/.test(line)) blocks.push(divider());\n    else if (line.startsWith('### ')) blocks.push(heading(3, line.slice(4)));\n    else if (line.startsWith('## ')) blocks.push(heading(2, line.slice(3)));\n    else if (line.startsWith('# ')) blocks.push(heading(1, line.slice(2)));\n    else if (/^[-\u2022*] /.test(line)) blocks.push({\n      object: 'block', type: 'bulleted_list_item',\n      bulleted_list_item: { rich_text: rich(line.slice(2)) },\n    });\n    else if (line.startsWith('> ')) blocks.push({\n      object: 'block', type: 'quote', quote: { rich_text: rich(line.slice(2)) },\n    });\n    else blocks.push({\n      object: 'block', type: 'paragraph', paragraph: { rich_text: rich(line) },\n    });\n  }\n  return blocks;\n};\n\n// ---------- Database properties ----------\nconst properties = {\n  Name: { title: [rt(scraped.title || `Lot ${scraped.lotNumber || '?'} \u2014 ${scraped.auctionHouse || 'Unknown'}`)] },\n  Status: { select: { name: 'Done' } },\n};\nif (scraped.lotNumber) {\n  properties['Lot Number'] = { rich_text: [rt(String(scraped.lotNumber))] };\n}\nif (scraped.auctionHouse) {\n  // Select option names cannot contain commas\n  properties['Auction House'] = { select: { name: scraped.auctionHouse.replace(/,/g, ' ').slice(0, 100) } };\n}\nif (/^\\d{4}-\\d{2}-\\d{2}$/.test(scraped.auctionDate || '')) {\n  properties['Auction Date'] = { date: { start: scraped.auctionDate } };\n}\nif (scraped.imageUrl) {\n  properties['Photo'] = { files: [{ type: 'external', name: 'Lot photo', external: { url: scraped.imageUrl } }] };\n}\n\n// ---------- Page body blocks ----------\nconst blocks = [];\n\nblocks.push({\n  object: 'block', type: 'callout',\n  callout: {\n    icon: { type: 'emoji', emoji: '\ud83d\udd28' },\n    color: 'gray_background',\n    rich_text: [rt([\n      `Lot ${scraped.lotNumber || '\u2014'}`,\n      scraped.auctionHouse,\n      scraped.auctionDate,\n    ].filter(Boolean).join('   \u2022   '))],\n  },\n});\nblocks.push(divider());\n\n// --- AI buy-analysis report (the main dossier) ---\nif (scraped.aiReport) {\n  blocks.push(heading(1, '\ud83e\udd16 AI Buy Analysis'));\n  blocks.push(...mdToBlocks(scraped.aiReport));\n  blocks.push(divider());\n} else if (scraped.aiReportError) {\n  blocks.push({\n    object: 'block', type: 'callout',\n    callout: { icon: { type: 'emoji', emoji: '\u26a0\ufe0f' }, color: 'red_background', rich_text: [rt(scraped.aiReportError)] },\n  });\n  blocks.push(divider());\n}\n\n// --- Photos ---\nconst gallery = (scraped.imageUrls && scraped.imageUrls.length ? scraped.imageUrls : [scraped.imageUrl])\n  .filter(Boolean).slice(0, 4);\nif (gallery.length) {\n  blocks.push(heading(2, '\ud83d\uddbc\ufe0f Lot Photos'));\n  for (const url of gallery) {\n    blocks.push({ object: 'block', type: 'image', image: { type: 'external', external: { url } } });\n  }\n  blocks.push(divider());\n}\n\n// --- Original catalogue text ---\nif (scraped.description) {\n  blocks.push(heading(2, '\ud83d\udcdd Catalogue Description'));\n  blocks.push(...paragraphs(scraped.description));\n}\nblocks.push(heading(2, '\ud83d\udd0d Published Condition Text'));\nif (scraped.condition) {\n  const parts = chunk(scraped.condition);\n  blocks.push({\n    object: 'block', type: 'callout',\n    callout: { icon: { type: 'emoji', emoji: '\u26a0\ufe0f' }, color: 'yellow_background', rich_text: [rt(parts[0])] },\n  });\n  for (const extra of parts.slice(1)) {\n    blocks.push({ object: 'block', type: 'paragraph', paragraph: { rich_text: [rt(extra)] } });\n  }\n} else {\n  blocks.push({\n    object: 'block', type: 'callout',\n    callout: { icon: { type: 'emoji', emoji: '\u2139\ufe0f' }, color: 'gray_background', rich_text: [rt('No condition report published for this lot.')] },\n  });\n}\nblocks.push(divider());\nblocks.push({ object: 'block', type: 'bookmark', bookmark: { url: scraped.sourceUrl } });\n\n// ---------- Batch (Notion: max 100 blocks per append request) ----------\nconst BATCH = 90;\nconst items = [];\nfor (let i = 0; i < blocks.length; i += BATCH) {\n  items.push({ json: {\n    pageId,\n    propertiesPayload: { properties },\n    blocksPayload: { children: blocks.slice(i, i + BATCH) },\n  } });\n}\nreturn items;\n"
      },
      "id": "a1000000-0000-0000-0000-000000000004",
      "name": "Build Notion Payloads",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        660,
        -80
      ]
    },
    {
      "parameters": {
        "method": "PATCH",
        "url": "=https://api.notion.com/v1/pages/{{ $json.pageId }}",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "notionApi",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Notion-Version",
              "value": "2022-06-28"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($json.propertiesPayload) }}",
        "options": {}
      },
      "id": "a1000000-0000-0000-0000-000000000005",
      "name": "Update Properties",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        880,
        -80
      ],
      "credentials": {
        "notionApi": {
          "name": "<your credential>"
        }
      },
      "executeOnce": true
    },
    {
      "parameters": {
        "method": "PATCH",
        "url": "=https://api.notion.com/v1/blocks/{{ $('Build Notion Payloads').first().json.pageId }}/children",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "notionApi",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Notion-Version",
              "value": "2022-06-28"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($('Build Notion Payloads').first().json.blocksPayload) }}",
        "options": {}
      },
      "id": "a1000000-0000-0000-0000-000000000006",
      "name": "Write Page Body",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1100,
        -80
      ],
      "credentials": {
        "notionApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "method": "PATCH",
        "url": "=https://api.notion.com/v1/pages/{{ $('Notion Trigger').first().json.id }}",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "notionApi",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Notion-Version",
              "value": "2022-06-28"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ properties: { Status: { select: { name: 'Error' } } } }) }}",
        "options": {}
      },
      "id": "a1000000-0000-0000-0000-000000000007",
      "name": "Mark Error",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        660,
        120
      ],
      "credentials": {
        "notionApi": {
          "name": "<your credential>"
        }
      }
    }
  ],
  "connections": {
    "Notion Trigger": {
      "main": [
        [
          {
            "node": "Needs Scraping?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Needs Scraping?": {
      "main": [
        [
          {
            "node": "Scrape Lot",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    },
    "Scrape Lot": {
      "main": [
        [
          {
            "node": "Build Notion Payloads",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Mark Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Notion Payloads": {
      "main": [
        [
          {
            "node": "Update Properties",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Update Properties": {
      "main": [
        [
          {
            "node": "Write Page Body",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "active": false
}