This workflow corresponds to n8n.io template #13635 — we link there as the canonical source.
This workflow follows the Airtable → HTTP Request 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 →
{
"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
}
]
]
}
}
}
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.
openAiApiuploadToUrlApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Accelerate your real estate marketing by moving from "photo capture" to "published listing" in seconds. This workflow automates the entire listing process by hosting property photos via UploadToURL, using GPT-4o Vision to write professional MLS descriptions, and…
Source: https://n8n.io/workflows/13635/ — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
This powerful n8n automation workflow is designed to execute advanced B2B lead enrichment and hyper-personalization for cold email outreach. By orchestrating a complex chain of data scraping, AI analy
This workflow contains community nodes that are only compatible with the self-hosted version of n8n.
Voice Note -> Veo 3 AD. Uses telegramTrigger, telegram, openAi, httpRequest. Event-driven trigger; 49 nodes.
This template is perfect for e-commerce entrepreneurs, marketers, agencies, and creative teams who want to turn simple product photos and short descriptions into professional flyers or product videos—
Listens for completed Fireflies transcripts, qualifies whether a proposal is needed using OpenAI, drafts structured proposal content, populates a Google Doc template, converts to PDF, and sends it to