{
  "nodes": [
    {
      "id": "c7c5f8e8-36d4-40a1-92a1-13035bb51e22",
      "name": "\ud83d\udccb Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        0,
        0
      ],
      "parameters": {
        "width": 540,
        "height": 668,
        "content": "## List properties instantly with UploadToURL, OpenAI Vision, and WordPress\nThe Problem: Real estate agents lose hours manually uploading property photos, writing descriptions, and syncing data between their website and internal MLS.\nThe Solution: An \"upload-and-forget\" pipeline that hosts photos via UploadToURL, uses Vision AI to write professional listings, and publishes to WordPress and Airtable in parallel.\n\n\u2699\ufe0f How it Works\nWebhook: Receives a property photo (Binary or URL) and address metadata.\n\nUploadToURL: Hosts the file and returns a public CDN link for the listing.\n\nGPT-4o Vision: Analyzes the room type, condition, and features to write a description.\n\nParallel Publish: Simultaneously creates a WordPress draft and an Airtable MLS record.\n\n\ud83d\udd10 Credentials & Setup\nNode: Install n8n-nodes-uploadtourl via Community Nodes.\n\nAPIs: UploadToURL, OpenAI (Vision), WordPress, and Airtable.\n\nVariables: Set WP_BASE_URL and AIRTABLE_BASE_ID"
      },
      "typeVersion": 1
    },
    {
      "id": "18028c15-b09a-4227-afc0-25e619e0b7dd",
      "name": "Section 1 \u2014 Upload & Vision",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        640,
        672
      ],
      "parameters": {
        "color": 7,
        "width": 1160,
        "height": 605,
        "content": "## 1 \u2014 Upload & Vision analysis\n\n**Webhook \u2192 Validate \u2192 Has URL? \u2192 Upload to URL (\u00d72) \u2192 Extract CDN URL \u2192 GPT-4o Vision \u2192 Parse Vision**\n\nValidates `listingId`, `address`, and file type. UploadToURL hosts via the native community node. Vision prompt requests MLS-grade output: room type, 1\u201310 condition score, pricing signals, feature tags, and a ready-to-publish room description."
      },
      "typeVersion": 1
    },
    {
      "id": "8d117d6f-588b-43b9-afc3-fbaf1f0d7d43",
      "name": "Section 2 \u2014 Parallel Publish",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1824,
        560
      ],
      "parameters": {
        "color": 7,
        "width": 296,
        "height": 659,
        "content": "## 2 \u2014 Parallel publish\n\n**WordPress Upsert + Airtable Upsert (run simultaneously)**\n\nBoth branches receive the same enriched payload and run in parallel. WordPress creates or updates a draft post keyed on `listingId` in post meta. Airtable creates or patches the MLS record keyed on `Listing ID`. Neither branch waits for the other \u2014 both feed into the merge node."
      },
      "typeVersion": 1
    },
    {
      "id": "74915269-7e8c-482d-9c36-76a6aea3adff",
      "name": "Section 3 \u2014 Merge & Notify",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2160,
        672
      ],
      "parameters": {
        "color": 7,
        "width": 744,
        "height": 537,
        "content": "## 3 \u2014 Merge & notify\n\n**Merge \u2192 Build Response \u2192 Telegram \u2192 Respond to Webhook**\n\nA Merge node (mode: combine) waits for both WordPress and Airtable to complete, then assembles a unified response with both platform IDs and URLs. Telegram sends the agent a message with links to both the WordPress draft and the Airtable MLS record."
      },
      "typeVersion": 1
    },
    {
      "id": "84049e24-09cd-4b16-bc39-81682181df61",
      "name": "Webhook - Receive Property Photo",
      "type": "n8n-nodes-base.webhook",
      "position": [
        672,
        960
      ],
      "parameters": {
        "path": "property-photo-publish",
        "options": {
          "allowedOrigins": "*"
        },
        "httpMethod": "POST",
        "responseMode": "responseNode"
      },
      "typeVersion": 2
    },
    {
      "id": "7e283ac8-8198-4adb-8a55-4ea15d9702f7",
      "name": "Validate Payload",
      "type": "n8n-nodes-base.code",
      "position": [
        816,
        960
      ],
      "parameters": {
        "jsCode": "const body = $input.first().json.body || $input.first().json;\n\n// \u2500\u2500 Required fields \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst listingId = String(body.listingId || '').trim();\nif (!listingId) throw new Error('listingId is required (e.g. LST-2025-0042).');\n\nconst address = String(body.address || '').trim();\nif (!address) throw new Error('address is required.');\n\nif (!body.fileUrl && !body.filename) {\n  throw new Error('Provide fileUrl (remote photo) or filename (binary upload).');\n}\n\n// \u2500\u2500 File type validation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst filename = body.filename ||\n  body.fileUrl?.split('?')[0].split('/').pop() ||\n  'photo.jpg';\nconst ext = filename.split('.').pop()?.toLowerCase() || 'jpg';\nconst allowedExts = ['jpg', 'jpeg', 'png', 'webp', 'heic', 'mp4', 'mov'];\nif (!allowedExts.includes(ext)) {\n  throw new Error(`File type .${ext} not allowed. Accepted: ${allowedExts.join(', ')}`);\n}\nconst isVideo = ['mp4', 'mov'].includes(ext);\n\n// \u2500\u2500 Price normalisation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst price = parseFloat(String(body.price || '0').replace(/[^0-9.]/g, '')) || null;\n\n// \u2500\u2500 Photo label: allow agent to tag what room this is \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst photoLabel = String(body.photoLabel || '').trim() || null;\n\n// \u2500\u2500 Structured filename for CDN \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst safeAddress = address.replace(/[^a-zA-Z0-9]/g, '-').slice(0, 40).toLowerCase();\nconst structuredFilename = `${listingId}_${safeAddress}_${Date.now()}.${ext}`;\n\nconst s = v => String(v || '').trim().slice(0, 500);\n\nreturn [{\n  json: {\n    listingId,\n    address,\n    price,\n    propertyType: s(body.propertyType) || 'Residential',\n    bedrooms: parseInt(body.bedrooms, 10) || null,\n    bathrooms: parseFloat(body.bathrooms) || null,\n    sqft: parseInt(body.sqft, 10) || null,\n    photoLabel,\n    isVideo,\n    ext,\n    filename,\n    structuredFilename,\n    fileUrl: body.fileUrl || null,\n    agentName: s(body.agentName) || 'Unknown Agent',\n    agentTelegram: s(body.agentTelegram),\n    notes: s(body.notes),\n    receivedAt: new Date().toISOString()\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "cf86ebd3-a3ac-4cef-abff-22feb7dcae25",
      "name": "Has Remote URL?",
      "type": "n8n-nodes-base.if",
      "position": [
        944,
        960
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "caseSensitive": false,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "cond-url",
              "operator": {
                "type": "string",
                "operation": "notEmpty"
              },
              "leftValue": "={{ $json.fileUrl }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "b676b860-df5a-4bf8-bcf3-577de8f51d92",
      "name": "Upload to URL - Remote",
      "type": "n8n-nodes-uploadtourl.uploadToUrl",
      "position": [
        1104,
        864
      ],
      "parameters": {
        "operation": "uploadFile"
      },
      "credentials": {
        "uploadToUrlApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "34659554-53c2-4765-b445-8d5e29d15902",
      "name": "Upload to URL - Binary",
      "type": "n8n-nodes-uploadtourl.uploadToUrl",
      "position": [
        1104,
        1024
      ],
      "parameters": {
        "operation": "uploadFile"
      },
      "credentials": {
        "uploadToUrlApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "77ec08f8-209f-46af-a6b0-d25cd2b42ff8",
      "name": "Extract CDN URL",
      "type": "n8n-nodes-base.code",
      "position": [
        1264,
        960
      ],
      "parameters": {
        "jsCode": "const uploadResp = $input.first().json;\nconst meta = $('Validate Payload').first().json;\n\nconst cdnUrl =\n  uploadResp.url ||\n  uploadResp.link ||\n  uploadResp.data?.url ||\n  uploadResp.file?.url ||\n  uploadResp.shortUrl;\n\nif (!cdnUrl) {\n  throw new Error('UploadToURL returned no public URL. Raw: ' + JSON.stringify(uploadResp).slice(0, 300));\n}\n\nreturn [{\n  json: {\n    ...meta,\n    cdnUrl: cdnUrl.replace(/^http:\\/\\//, 'https://'),\n    uploadId: uploadResp.id || uploadResp.data?.id || null,\n    fileSizeBytes: uploadResp.size || uploadResp.data?.size || null\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "7dbd4170-84e1-4e77-898c-7971cf8ce1b2",
      "name": "GPT-4o Vision - MLS Analysis",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "position": [
        1392,
        960
      ],
      "parameters": {
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "ft:gpt-3.5-turbo-0125:bar-juice::91x6k9Fc",
          "cachedResultName": "FT:GPT-3.5-TURBO-0125:BAR-JUICE::91X6K9FC"
        },
        "options": {
          "maxTokens": 700,
          "temperature": 0.3
        },
        "messages": {
          "values": [
            {
              "role": "system",
              "content": "You are a licensed real estate appraiser and MLS listing specialist. Your job is to analyse property photos and produce structured, MLS-compliant descriptions. Return ONLY a valid JSON object \u2014 no markdown, no preamble."
            },
            {
              "content": "=Analyse this property photo for a real estate MLS listing.\n\nPhoto URL: {{ $json.cdnUrl }}\nProperty Type: {{ $json.propertyType }}\nAddress: {{ $json.address }}\nAgent-provided room label (may be empty): {{ $json.photoLabel || 'Not provided' }}\nIs video walkthrough: {{ $json.isVideo }}\n\nReturn ONLY this JSON:\n{\n  \"roomType\": \"Exact room name: Kitchen, Master Bedroom, Living Room, Exterior Front, Backyard, Bathroom, Garage, etc.\",\n  \"conditionScore\": 8,\n  \"conditionLabel\": \"Excellent | Good | Fair | Poor\",\n  \"mlsDescription\": \"2-3 sentence MLS-ready room description using professional real estate language\",\n  \"featureTags\": [\"hardwood floors\", \"crown molding\", \"natural light\"],\n  \"pricingSignals\": [\"premium finishes\", \"updated appliances\"],\n  \"lightingType\": \"Natural | Artificial | Mixed\",\n  \"lightingQuality\": \"Poor | Fair | Good | Excellent\",\n  \"altText\": \"SEO-optimised alt text for this photo\",\n  \"isExterior\": false,\n  \"featuredImageScore\": 7,\n  \"renovationVisible\": false,\n  \"estimatedRenovationAge\": \"5-10 years | Recent | Original | Unknown\"\n}"
            }
          ]
        }
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.5
    },
    {
      "id": "3055e0a3-9cfb-4bf3-895c-2c1426188ed5",
      "name": "Parse Vision & Build Payloads",
      "type": "n8n-nodes-base.code",
      "notes": "Parses Vision JSON and pre-builds both platform payloads in one pass: WordPress Gutenberg block content and the complete Airtable record object. Both parallel branches just reference these pre-built fields.",
      "position": [
        1664,
        960
      ],
      "parameters": {
        "jsCode": "const aiRaw = $input.first().json;\nconst meta = $('Extract CDN URL').first().json;\n\n// \u2500\u2500 Parse Vision response \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nlet vision;\ntry {\n  const raw =\n    aiRaw.message?.content ||\n    aiRaw.choices?.[0]?.message?.content ||\n    aiRaw.content ||\n    aiRaw.text;\n  vision = typeof raw === 'string' ? JSON.parse(raw) : raw;\n} catch (e) {\n  throw new Error('Failed to parse GPT-4o Vision JSON: ' + e.message);\n}\n\nif (!vision.roomType) throw new Error('Vision did not return a room type.');\n\n// \u2500\u2500 Price formatter \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst priceFormatted = meta.price\n  ? meta.price.toLocaleString('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 })\n  : 'Price on request';\n\n// \u2500\u2500 WordPress post title & content \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst wpTitle = `${meta.propertyType} \u2014 ${meta.address}${meta.price ? ' \u2014 ' + priceFormatted : ''}`;\n\nconst wpContent = [\n  `<!-- wp:heading -->\\n<h2>${vision.roomType}</h2>\\n<!-- /wp:heading -->`,\n  `<!-- wp:paragraph -->\\n<p>${vision.mlsDescription}</p>\\n<!-- /wp:paragraph -->`,\n  vision.featureTags?.length\n    ? `<!-- wp:list -->\\n<ul>${vision.featureTags.map(f => `<li>${f}</li>`).join('')}</ul>\\n<!-- /wp:list -->`\n    : '',\n  `<!-- wp:image -->\\n<figure class=\"wp-block-image\">\\n<img src=\"${meta.cdnUrl}\" alt=\"${vision.altText}\" />\\n<figcaption>${vision.roomType} \u2014 ${meta.address}</figcaption>\\n</figure>\\n<!-- /wp:image -->`,\n  meta.notes ? `<!-- wp:paragraph -->\\n<p><em>Agent notes: ${meta.notes}</em></p>\\n<!-- /wp:paragraph -->` : ''\n].filter(Boolean).join('\\n\\n');\n\n// \u2500\u2500 Airtable fields \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst airtableRecord = {\n  'Listing ID': meta.listingId,\n  'Address': meta.address,\n  'Price': meta.price,\n  'Price Formatted': priceFormatted,\n  'Property Type': meta.propertyType,\n  'Bedrooms': meta.bedrooms,\n  'Bathrooms': meta.bathrooms,\n  'Sqft': meta.sqft,\n  'Room Type': vision.roomType,\n  'Condition Score': vision.conditionScore,\n  'Condition Label': vision.conditionLabel,\n  'MLS Description': vision.mlsDescription,\n  'Feature Tags': (vision.featureTags || []).join(', '),\n  'Pricing Signals': (vision.pricingSignals || []).join(', '),\n  'Lighting Type': vision.lightingType,\n  'Lighting Quality': vision.lightingQuality,\n  'Photo URL': meta.cdnUrl,\n  'Alt Text': vision.altText,\n  'Is Exterior': vision.isExterior || false,\n  'Featured Score': vision.featuredImageScore,\n  'Renovation Visible': vision.renovationVisible || false,\n  'Renovation Age': vision.estimatedRenovationAge || 'Unknown',\n  'Agent': meta.agentName,\n  'Status': 'Draft',\n  'Created At': meta.receivedAt\n};\n\nreturn [{\n  json: {\n    ...meta,\n    // Vision output\n    roomType: vision.roomType,\n    conditionScore: vision.conditionScore,\n    conditionLabel: vision.conditionLabel,\n    mlsDescription: vision.mlsDescription,\n    featureTags: vision.featureTags || [],\n    pricingSignals: vision.pricingSignals || [],\n    lightingType: vision.lightingType,\n    lightingQuality: vision.lightingQuality,\n    altText: vision.altText,\n    isExterior: vision.isExterior || false,\n    featuredImageScore: vision.featuredImageScore,\n    renovationVisible: vision.renovationVisible || false,\n    // Computed\n    priceFormatted,\n    wpTitle,\n    wpContent,\n    airtableRecord\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "df26a763-2db1-4101-a865-106025b2aee8",
      "name": "WordPress - Create Draft Post",
      "type": "n8n-nodes-base.httpRequest",
      "notes": "Creates a WordPress draft post with Gutenberg block content. All property meta fields are written as post meta for use with a real estate theme or plugin like WP All Import.",
      "position": [
        1904,
        848
      ],
      "parameters": {
        "url": "={{ $vars.WP_BASE_URL }}/wp-json/wp/v2/posts",
        "method": "POST",
        "options": {
          "timeout": 15000,
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        },
        "jsonBody": "={\n  \"title\": {{ JSON.stringify($json.wpTitle) }},\n  \"content\": {{ JSON.stringify($json.wpContent) }},\n  \"status\": \"draft\",\n  \"meta\": {\n    \"listing_id\": {{ JSON.stringify($json.listingId) }},\n    \"property_address\": {{ JSON.stringify($json.address) }},\n    \"property_price\": {{ JSON.stringify($json.priceFormatted) }},\n    \"property_type\": {{ JSON.stringify($json.propertyType) }},\n    \"room_type\": {{ JSON.stringify($json.roomType) }},\n    \"condition_score\": {{ JSON.stringify(String($json.conditionScore || '')) }},\n    \"feature_tags\": {{ JSON.stringify($json.featureTags.join(', ')) }},\n    \"photo_cdn_url\": {{ JSON.stringify($json.cdnUrl) }},\n    \"agent_name\": {{ JSON.stringify($json.agentName) }}\n  }\n}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBasicAuth"
      },
      "typeVersion": 4.2
    },
    {
      "id": "c3888f4d-8abf-47a9-a09a-146707e151ce",
      "name": "Airtable - Create MLS Record",
      "type": "n8n-nodes-base.airtable",
      "notes": "Creates the MLS record in Airtable simultaneously with the WordPress post. Uses autoMapInputData from the pre-built airtableRecord object in Parse Vision node.",
      "position": [
        1920,
        1056
      ],
      "parameters": {
        "base": {
          "__rl": true,
          "mode": "list",
          "value": ""
        },
        "table": {
          "__rl": true,
          "mode": "list",
          "value": ""
        },
        "columns": {
          "value": "={{ $json.airtableRecord }}",
          "mappingMode": "autoMapInputData"
        },
        "options": {},
        "operation": "create"
      },
      "typeVersion": 2.1
    },
    {
      "id": "c1335f24-0b2b-4d7e-8f0c-9a7f47f9ac31",
      "name": "Merge Platform Results",
      "type": "n8n-nodes-base.merge",
      "notes": "Waits for both WordPress and Airtable to complete before proceeding. Set to 'combine' mode so it only fires once both inputs have data.",
      "position": [
        2224,
        960
      ],
      "parameters": {
        "mode": "combine",
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "6961f6ce-7802-4d71-9f6c-2c323fa93e97",
      "name": "Build Unified Response",
      "type": "n8n-nodes-base.code",
      "position": [
        2400,
        1008
      ],
      "parameters": {
        "jsCode": "// Both inputs arrive as a combined item from Merge\nconst merged = $input.first().json;\n\n// WordPress result is in input[0], Airtable in input[1]\n// With multiplex merge both are accessible\nconst wpResult = $('WordPress - Create Draft Post').first().json;\nconst atResult = $('Airtable - Create MLS Record').first().json;\nconst vision = $('Parse Vision & Build Payloads').first().json;\n\n// \u2500\u2500 WordPress \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst wpPostId = wpResult.id || null;\nconst wpEditUrl = wpPostId\n  ? `${$vars.WP_BASE_URL}/wp-admin/post.php?post=${wpPostId}&action=edit`\n  : null;\nconst wpPreviewUrl = wpResult.link || null;\n\n// \u2500\u2500 Airtable \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst airtableRecordId = atResult.id || null;\nconst airtableUrl = airtableRecordId\n  ? `https://airtable.com/${$vars.AIRTABLE_BASE_ID}/${$vars.AIRTABLE_TABLE_NAME}/${airtableRecordId}`\n  : null;\n\nreturn [{\n  json: {\n    success: true,\n    listingId: vision.listingId,\n    address: vision.address,\n    priceFormatted: vision.priceFormatted,\n    propertyType: vision.propertyType,\n    agentName: vision.agentName,\n    agentTelegram: vision.agentTelegram,\n    // Photo\n    cdnUrl: vision.cdnUrl,\n    roomType: vision.roomType,\n    conditionScore: vision.conditionScore,\n    conditionLabel: vision.conditionLabel,\n    featureTags: vision.featureTags,\n    mlsDescription: vision.mlsDescription,\n    lightingType: vision.lightingType,\n    lightingQuality: vision.lightingQuality,\n    isExterior: vision.isExterior,\n    // WordPress\n    wpPostId,\n    wpEditUrl,\n    wpPreviewUrl,\n    // Airtable\n    airtableRecordId,\n    airtableUrl,\n    // Meta\n    publishedAt: new Date().toISOString()\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "e5114a0a-febf-49a4-9f52-1838f4f1888d",
      "name": "Telegram - Agent Confirmation",
      "type": "n8n-nodes-base.telegram",
      "position": [
        2560,
        960
      ],
      "parameters": {
        "text": "=\ud83c\udfe0 *Property Photo Published*\n\n\ud83d\udccd *{{ $json.address }}*\n\ud83c\udff7 Listing ID: `{{ $json.listingId }}`\n\ud83d\udcb0 {{ $json.priceFormatted }} \u00b7 {{ $json.propertyType }}\n\n\ud83d\udcf8 *Room Analysis*\n\ud83d\udecb Room: {{ $json.roomType }}{{ $json.isExterior ? ' (Exterior)' : '' }}\n\u2b50 Condition: {{ $json.conditionScore }}/10 \u2014 {{ $json.conditionLabel }}\n\ud83d\udca1 Lighting: {{ $json.lightingType }} / {{ $json.lightingQuality }}\n\ud83c\udff7 Tags: {{ $json.featureTags.slice(0, 4).join(', ') }}\n\n\ud83d\udcdd _{{ $json.mlsDescription }}_\n\n\ud83d\udd17 *Published to:*\n\u2022 [WordPress Draft]({{ $json.wpEditUrl }})\n\u2022 [Airtable MLS Record]({{ $json.airtableUrl }})\n\u2022 [Photo CDN]({{ $json.cdnUrl }})\n\n\ud83d\udc64 {{ $json.agentName }}{{ $json.agentTelegram ? ' (' + $json.agentTelegram + ')' : '' }}",
        "chatId": "={{ $vars.TELEGRAM_CHAT_ID }}",
        "additionalFields": {
          "parse_mode": "Markdown",
          "disable_web_page_preview": false
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "e3b2b041-172c-4128-aa89-70325128209b",
      "name": "Respond to Webhook",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        2736,
        960
      ],
      "parameters": {
        "options": {
          "responseCode": 201,
          "responseHeaders": {
            "entries": [
              {
                "name": "Content-Type",
                "value": "application/json"
              }
            ]
          }
        },
        "respondWith": "json",
        "responseBody": "={{ $('Build Unified Response').first().json }}"
      },
      "typeVersion": 1.1
    }
  ],
  "connections": {
    "Extract CDN URL": {
      "main": [
        [
          {
            "node": "GPT-4o Vision - MLS Analysis",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has Remote URL?": {
      "main": [
        [
          {
            "node": "Upload to URL - Remote",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Upload to URL - Binary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate Payload": {
      "main": [
        [
          {
            "node": "Has Remote URL?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Unified Response": {
      "main": [
        [
          {
            "node": "Telegram - Agent Confirmation",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge Platform Results": {
      "main": [
        [
          {
            "node": "Build Unified Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upload to URL - Binary": {
      "main": [
        [
          {
            "node": "Extract CDN URL",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upload to URL - Remote": {
      "main": [
        [
          {
            "node": "Extract CDN URL",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Airtable - Create MLS Record": {
      "main": [
        [
          {
            "node": "Merge Platform Results",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "GPT-4o Vision - MLS Analysis": {
      "main": [
        [
          {
            "node": "Parse Vision & Build Payloads",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Vision & Build Payloads": {
      "main": [
        [
          {
            "node": "WordPress - Create Draft Post",
            "type": "main",
            "index": 0
          },
          {
            "node": "Airtable - Create MLS Record",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Telegram - Agent Confirmation": {
      "main": [
        [
          {
            "node": "Respond to Webhook",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "WordPress - Create Draft Post": {
      "main": [
        [
          {
            "node": "Merge Platform Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Webhook - Receive Property Photo": {
      "main": [
        [
          {
            "node": "Validate Payload",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}