This workflow corresponds to n8n.io template #12175 — we link there as the canonical source.
This workflow follows the Chainllm → OpenAI Embeddings 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 →
{
"id": "OvYK6agG8RmwF6Gq",
"name": "Photo Cost Estimate Pro v2.0",
"tags": [],
"nodes": [
{
"id": "0f4cbe14-7ba4-46c5-81d2-6b1d4482efcc",
"name": "Photo Upload Form",
"type": "n8n-nodes-base.formTrigger",
"position": [
29104,
400
],
"parameters": {
"options": {},
"formTitle": "\ud83d\udcf8 Photo Cost Estimate Pro v2",
"formFields": {
"values": [
{
"fieldType": "file",
"fieldLabel": "\ud83d\udcf7 Upload Photo",
"multipleFiles": false,
"requiredField": true,
"acceptFileTypes": ".jpg,.jpeg,.png,.webp"
},
{
"fieldType": "dropdown",
"fieldLabel": "\ud83c\udf0d Region & Language",
"fieldOptions": {
"values": [
{
"option": "\ud83c\udde9\ud83c\uddea German - Berlin (EUR \u20ac)"
},
{
"option": "\ud83c\uddec\ud83c\udde7 English - Toronto (CAD $)"
},
{
"option": "\ud83c\uddf7\ud83c\uddfa Russian - St. Petersburg (RUB \u20bd)"
},
{
"option": "\ud83c\uddea\ud83c\uddf8 Spanish - Barcelona (EUR \u20ac)"
},
{
"option": "\ud83c\uddeb\ud83c\uddf7 French - Paris (EUR \u20ac)"
},
{
"option": "\ud83c\udde7\ud83c\uddf7 Portuguese - S\u00e3o Paulo (BRL R$)"
},
{
"option": "\ud83c\udde8\ud83c\uddf3 Chinese - Shanghai (CNY \u00a5)"
},
{
"option": "\ud83c\udde6\ud83c\uddea Arabic - Dubai (AED \u062f.\u0625)"
},
{
"option": "\ud83c\uddee\ud83c\uddf3 Hindi - Mumbai (INR \u20b9)"
}
]
},
"requiredField": true
},
{
"fieldType": "dropdown",
"fieldLabel": "\ud83c\udfd7\ufe0f Work Type",
"fieldOptions": {
"values": [
{
"option": "\ud83d\udd28 New Construction"
},
{
"option": "\ud83d\udd04 Renovation / Remodel"
},
{
"option": "\ud83d\udd27 Repair"
},
{
"option": "\u2753 Auto-detect"
}
]
},
"requiredField": true
},
{
"fieldType": "textarea",
"fieldLabel": "\ud83d\udcdd Description (optional)",
"placeholder": "Describe what's in the photo, specify dimensions, or add context..."
}
]
},
"responseMode": "lastNode",
"formDescription": "Upload a construction photo for automatic cost estimation.\nIMPROVED: Multi-stage AI decomposition for accurate work identification.\nPrices based on DDC CWICR regional databases (9 languages)."
},
"typeVersion": 2.2
},
{
"id": "fae54f1a-9225-48c1-a755-83dca9f15033",
"name": "Extract Input",
"type": "n8n-nodes-base.code",
"position": [
29328,
400
],
"parameters": {
"jsCode": "// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// EXTRACT INPUT FROM FORM - 9 LANGUAGES + WORK TYPE\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nconst input = $input.first();\nconst formData = input.json || {};\n\nconst regionField = formData['\ud83c\udf0d Region & Language'] || formData['Region & Language'] || '';\nconst workTypeField = formData['\ud83c\udfd7\ufe0f Work Type'] || formData['Work Type'] || 'Auto-detect';\n\n// 9 languages mapping\nconst langMap = {\n 'German': 'DE',\n 'English': 'EN',\n 'Russian': 'RU',\n 'Spanish': 'ES',\n 'French': 'FR',\n 'Portuguese': 'PT',\n 'Chinese': 'ZH',\n 'Arabic': 'AR',\n 'Hindi': 'HI'\n};\n\nlet languageCode = 'EN';\nfor (const [lang, code] of Object.entries(langMap)) {\n if (regionField.includes(lang)) {\n languageCode = code;\n break;\n }\n}\n\n// Work type detection\nlet workType = 'auto';\nif (workTypeField.includes('New')) workType = 'new_construction';\nelse if (workTypeField.includes('Renovation')) workType = 'renovation';\nelse if (workTypeField.includes('Repair')) workType = 'repair';\n\nconst userDescription = formData['\ud83d\udcdd Description (optional)'] || formData['Description (optional)'] || '';\n\nlet photoBase64 = '';\nlet photoMimeType = 'image/jpeg';\n\nif (input.binary) {\n for (const key of Object.keys(input.binary)) {\n const bin = input.binary[key];\n if (bin.mimeType && bin.mimeType.startsWith('image/')) {\n photoBase64 = bin.data;\n photoMimeType = bin.mimeType;\n break;\n }\n }\n}\n\nreturn {\n json: {\n language_code: languageCode,\n photo_base64: photoBase64,\n photo_mime_type: photoMimeType,\n has_photo: photoBase64.length > 100,\n user_description: userDescription,\n selected_region: regionField,\n work_type: workType\n }\n};"
},
"typeVersion": 2
},
{
"id": "d5b99499-be6d-4da7-a381-94cdf5e9f7d4",
"name": "Configure Language",
"type": "n8n-nodes-base.code",
"position": [
29552,
400
],
"parameters": {
"jsCode": "// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// LANGUAGE & VECTOR DB CONFIGURATION - 9 LANGUAGES\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nconst input = $input.first().json;\nconst languageCode = (input.language_code || 'EN').toUpperCase();\n\nconst languageConfig = {\n 'DE': {\n city: 'Berlin',\n vectorDb: 'DE_BERLIN_workitems_costs_resources_EMBEDDINGS_3072_DDC_CWICR',\n language: 'German',\n languageNative: 'Deutsch',\n currency: 'EUR',\n currencySymbol: '\u20ac',\n locale: 'de-DE',\n systemPromptLang: 'Antworte auf Deutsch.',\n searchLang: 'German'\n },\n 'EN': {\n city: 'Toronto',\n vectorDb: 'ENG_TORONTO_workitems_costs_resources_EMBEDDINGS_3072_DDC_CWICR',\n language: 'English',\n languageNative: 'English',\n currency: 'CAD',\n currencySymbol: '$',\n locale: 'en-CA',\n systemPromptLang: 'Respond in English.',\n searchLang: 'English'\n },\n 'RU': {\n city: 'St. Petersburg',\n vectorDb: 'RU_STPETERSBURG_workitems_costs_resources_EMBEDDINGS_3072_DDC_CWICR',\n language: 'Russian',\n languageNative: '\u0420\u0443\u0441\u0441\u043a\u0438\u0439',\n currency: 'RUB',\n currencySymbol: '\u20bd',\n locale: 'ru-RU',\n systemPromptLang: '\u041e\u0442\u0432\u0435\u0447\u0430\u0439 \u043d\u0430 \u0440\u0443\u0441\u0441\u043a\u043e\u043c \u044f\u0437\u044b\u043a\u0435.',\n searchLang: 'Russian'\n },\n 'FR': {\n city: 'Paris',\n vectorDb: 'FR_PARIS_workitems_costs_resources_EMBEDDINGS_3072_DDC_CWICR',\n language: 'French',\n languageNative: 'Fran\u00e7ais',\n currency: 'EUR',\n currencySymbol: '\u20ac',\n locale: 'fr-FR',\n systemPromptLang: 'R\u00e9pondez en fran\u00e7ais.',\n searchLang: 'French'\n },\n 'ES': {\n city: 'Barcelona',\n vectorDb: 'ES_BARCELONA_workitems_costs_resources_EMBEDDINGS_3072_DDC_CWICR',\n language: 'Spanish',\n languageNative: 'Espa\u00f1ol',\n currency: 'EUR',\n currencySymbol: '\u20ac',\n locale: 'es-ES',\n systemPromptLang: 'Responde en espa\u00f1ol.',\n searchLang: 'Spanish'\n },\n 'PT': {\n city: 'S\u00e3o Paulo',\n vectorDb: 'PT_SAOPAULO_workitems_costs_resources_EMBEDDINGS_3072_DDC_CWICR',\n language: 'Portuguese',\n languageNative: 'Portugu\u00eas',\n currency: 'BRL',\n currencySymbol: 'R$',\n locale: 'pt-BR',\n systemPromptLang: 'Responda em portugu\u00eas.',\n searchLang: 'Portuguese'\n },\n 'ZH': {\n city: 'Shanghai',\n vectorDb: 'ZH_SHANGHAI_workitems_costs_resources_EMBEDDINGS_3072_DDC_CWICR',\n language: 'Chinese',\n languageNative: '\u4e2d\u6587',\n currency: 'CNY',\n currencySymbol: '\u00a5',\n locale: 'zh-CN',\n systemPromptLang: '\u8bf7\u7528\u4e2d\u6587\u56de\u7b54\u3002',\n searchLang: 'Chinese'\n },\n 'AR': {\n city: 'Dubai',\n vectorDb: 'AR_DUBAI_workitems_costs_resources_EMBEDDINGS_3072_DDC_CWICR',\n language: 'Arabic',\n languageNative: '\u0627\u0644\u0639\u0631\u0628\u064a\u0629',\n currency: 'AED',\n currencySymbol: '\u062f.\u0625',\n locale: 'ar-AE',\n systemPromptLang: '\u0623\u062c\u0628 \u0628\u0627\u0644\u0644\u063a\u0629 \u0627\u0644\u0639\u0631\u0628\u064a\u0629.',\n searchLang: 'Arabic'\n },\n 'HI': {\n city: 'Mumbai',\n vectorDb: 'HI_MUMBAI_workitems_costs_resources_EMBEDDINGS_3072_DDC_CWICR',\n language: 'Hindi',\n languageNative: '\u0939\u093f\u0928\u094d\u0926\u0940',\n currency: 'INR',\n currencySymbol: '\u20b9',\n locale: 'hi-IN',\n systemPromptLang: '\u0939\u093f\u0902\u0926\u0940 \u092e\u0947\u0902 \u091c\u0935\u093e\u092c \u0926\u0947\u0902\u0964',\n searchLang: 'Hindi'\n }\n};\n\nconst config = languageConfig[languageCode] || languageConfig['EN'];\n\nreturn {\n json: {\n ...input,\n language_code: languageCode,\n language_config: config,\n qdrant_collection: config.vectorDb,\n city: config.city,\n language: config.language,\n language_native: config.languageNative,\n currency: config.currency,\n currency_symbol: config.currencySymbol,\n locale: config.locale,\n system_prompt_lang: config.systemPromptLang,\n search_lang: config.searchLang,\n pricing_level: config.city\n }\n};"
},
"typeVersion": 2
},
{
"id": "2ad831a9-2893-4a0c-992e-337eb4a4bc36",
"name": "Has Photo?",
"type": "n8n-nodes-base.if",
"position": [
29776,
400
],
"parameters": {
"options": {},
"conditions": {
"options": {
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"operator": {
"type": "boolean",
"operation": "equals"
},
"leftValue": "={{ $json.has_photo }}",
"rightValue": true
}
]
}
},
"typeVersion": 2
},
{
"id": "97d8463c-6e3b-4b16-b44f-6147d7b17f91",
"name": "Error No Photo",
"type": "n8n-nodes-base.code",
"position": [
29984,
560
],
"parameters": {
"jsCode": "return {\n json: {\n success: false,\n error: true,\n message: '\u274c No photo provided',\n html_content: '<!DOCTYPE html><html><head><meta charset=\"UTF-8\"></head><body style=\"font-family:system-ui;padding:40px;text-align:center;\"><h1>\u274c Error</h1><p>No photo was uploaded. Please go back and upload a construction photo.</p></body></html>'\n }\n};"
},
"typeVersion": 2
},
{
"id": "b9545c52-79e0-447f-bb2c-fe81fc38a6bf",
"name": "STAGE 1 Vision Prompt",
"type": "n8n-nodes-base.set",
"position": [
29984,
384
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "prompt",
"name": "chatInput",
"type": "string",
"value": "=You are an expert construction surveyor. {{ $json.system_prompt_lang }}\n\n## STAGE 1: IDENTIFY ELEMENTS FROM PHOTO\n\nAnalyze this construction photo and identify ALL visible elements.\n\n### IDENTIFICATION RULES:\n1. **Identify ROOM TYPE** first (bathroom, kitchen, bedroom, living room, hallway, exterior, facade, roof, basement, utility)\n2. **List ALL visible construction elements** with their materials\n3. **Estimate dimensions** using reference objects:\n - Standard door: 2.0m height, 0.9m width\n - Standard window: 1.2m \u00d7 1.4m\n - Ceiling height: typically 2.5-2.7m\n - Brick: 25cm \u00d7 12cm \u00d7 6.5cm\n - Tile: 30cm \u00d7 30cm or 60cm \u00d7 60cm\n - Socket/switch: 8cm \u00d7 8cm\n\n{{ $json.user_description ? 'User note: ' + $json.user_description : '' }}\n\nReturn ONLY valid JSON (no markdown, no code blocks):\n{\n \"room_type\": \"bathroom|kitchen|bedroom|living_room|hallway|exterior|facade|roof|basement|utility|other\",\n \"room_description\": \"Brief description of what you see\",\n \"estimated_dimensions\": {\n \"floor_area_m2\": 0,\n \"wall_area_m2\": 0,\n \"ceiling_height_m\": 2.5,\n \"perimeter_m\": 0\n },\n \"elements\": [\n {\n \"element_type\": \"wall|floor|ceiling|window|door|fixture|furniture|mep\",\n \"element_name\": \"Descriptive name\",\n \"material\": \"concrete|brick|drywall|tile|wood|glass|metal|plastic\",\n \"surface_finish\": \"painted|tiled|plastered|wallpaper|raw|laminate\",\n \"quantity\": 1,\n \"unit\": \"m\u00b2|m|pcs\",\n \"estimated_size\": \"e.g. 3.5 m\u00b2 or 2.1 m\",\n \"condition\": \"new|good|worn|damaged\",\n \"notes\": \"Any additional observations\"\n }\n ],\n \"fixtures\": [\n {\n \"fixture_type\": \"toilet|sink|bathtub|shower|faucet|radiator|socket|switch|light|vent\",\n \"quantity\": 1,\n \"notes\": \"Description\"\n }\n ],\n \"work_type_detected\": \"new_construction|renovation|repair\",\n \"confidence\": \"high|medium|low\"\n}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "0fb1af6a-fa06-44e6-9181-2f0c3073f1f6",
"name": "STAGE 1 Analyze Photo",
"type": "@n8n/n8n-nodes-langchain.chainLlm",
"position": [
30208,
384
],
"parameters": {
"messages": {
"messageValues": [
{
"message": "={{ $json.chatInput }}"
}
]
}
},
"typeVersion": 1.4
},
{
"id": "7a62ea97-a203-4ce0-b693-90d385aa542c",
"name": "GPT-4 Vision",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
"position": [
30208,
592
],
"parameters": {
"model": {
"__rl": true,
"mode": "list",
"value": "chatgpt-4o-latest",
"cachedResultName": "chatgpt-4o-latest"
},
"options": {
"maxTokens": 4000,
"temperature": 0.2
}
},
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.2
},
{
"id": "f4168e15-aa61-4878-a583-79df493e8104",
"name": "Parse STAGE 1",
"type": "n8n-nodes-base.code",
"position": [
30480,
384
],
"parameters": {
"jsCode": "// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// PARSE STAGE 1 VISION RESPONSE\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nconst aiResponse = $input.first().json;\nconst configData = $('Configure Language').first().json;\n\nlet parsed = {\n room_type: 'unknown',\n room_description: 'Analysis failed',\n elements: [],\n fixtures: [],\n estimated_dimensions: { floor_area_m2: 10, wall_area_m2: 30, ceiling_height_m: 2.5, perimeter_m: 12 }\n};\n\ntry {\n const content = aiResponse.text || aiResponse.content || aiResponse.response || '';\n let jsonStr = content;\n \n const jsonMatch = content.match(/```json\\s*([\\s\\S]*?)\\s*```/);\n if (jsonMatch) {\n jsonStr = jsonMatch[1];\n } else {\n const codeMatch = content.match(/```\\s*([\\s\\S]*?)\\s*```/);\n if (codeMatch) {\n jsonStr = codeMatch[1];\n } else {\n const objMatch = content.match(/\\{[\\s\\S]*\\}/);\n if (objMatch) {\n jsonStr = objMatch[0];\n }\n }\n }\n \n parsed = JSON.parse(jsonStr);\n} catch (error) {\n console.error('Parse error:', error.message);\n}\n\nreturn {\n json: {\n ...configData,\n stage1_result: parsed,\n room_type: parsed.room_type || 'unknown',\n room_description: parsed.room_description || 'Photo analysis',\n elements: parsed.elements || [],\n fixtures: parsed.fixtures || [],\n dimensions: parsed.estimated_dimensions || {},\n work_type_detected: parsed.work_type_detected || configData.work_type || 'renovation',\n confidence: parsed.confidence || 'medium'\n }\n};"
},
"typeVersion": 2
},
{
"id": "c733ceb2-3357-42b1-b585-0cf2f8d3f2d1",
"name": "STAGE 4 Decompose Prompt",
"type": "n8n-nodes-base.set",
"position": [
30656,
384
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "prompt2",
"name": "chatInput",
"type": "string",
"value": "={{ $json.system_prompt_lang }}\n\n## STAGE 4: DECOMPOSE ELEMENTS TO CONSTRUCTION WORKS\n\nYou are a construction cost estimator. Based on the photo analysis, decompose each element into specific construction work items.\n\n### PHOTO ANALYSIS RESULTS:\n- Room Type: {{ $json.room_type }}\n- Description: {{ $json.room_description }}\n- Work Type: {{ $json.work_type_detected }}\n- Dimensions: Floor {{ $json.dimensions.floor_area_m2 || 10 }} m\u00b2, Walls {{ $json.dimensions.wall_area_m2 || 30 }} m\u00b2\n\n### ELEMENTS DETECTED:\n{{ JSON.stringify($json.elements, null, 2) }}\n\n### FIXTURES DETECTED:\n{{ JSON.stringify($json.fixtures, null, 2) }}\n\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n## CATEGORY \u2192 WORK ITEMS MAPPING (USE THIS!)\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n### BATHROOM:\n1. Demolition of old finishes (m\u00b2) - if renovation\n2. Waterproofing floor (m\u00b2)\n3. Waterproofing walls wet zone (m\u00b2)\n4. Floor screed (m\u00b2)\n5. Wall tiling (m\u00b2)\n6. Floor tiling (m\u00b2)\n7. Ceiling finishing (m\u00b2)\n8. Toilet installation (pcs)\n9. Sink/washbasin installation (pcs)\n10. Bathtub installation (pcs) - if present\n11. Shower cabin installation (pcs) - if present\n12. Faucet/mixer installation (pcs)\n13. Towel rail installation (pcs)\n14. Mirror installation (pcs)\n15. Ventilation installation (pcs)\n16. Electrical: sockets, switches, lighting (pcs)\n17. Plumbing pipes (m)\n18. Door installation (pcs)\n\n### KITCHEN:\n1. Demolition of old finishes (m\u00b2) - if renovation\n2. Wall preparation/plastering (m\u00b2)\n3. Wall tiling (backsplash area) (m\u00b2)\n4. Floor finishing (m\u00b2)\n5. Ceiling finishing (m\u00b2)\n6. Kitchen cabinets installation (m)\n7. Countertop installation (m)\n8. Sink installation (pcs)\n9. Faucet installation (pcs)\n10. Appliance connections (pcs)\n11. Electrical: sockets, switches (pcs)\n12. Lighting installation (pcs)\n13. Ventilation hood installation (pcs)\n14. Plumbing pipes (m)\n\n### LIVING ROOM / BEDROOM:\n1. Demolition of old finishes (m\u00b2) - if renovation\n2. Wall preparation (m\u00b2)\n3. Wall painting OR wallpaper (m\u00b2)\n4. Floor preparation/screed (m\u00b2)\n5. Floor covering - laminate/parquet/carpet (m\u00b2)\n6. Baseboard/skirting installation (m)\n7. Ceiling finishing - paint/stretch/drywall (m\u00b2)\n8. Electrical: sockets, switches (pcs)\n9. Lighting installation (pcs)\n10. Window sill finishing (m)\n11. Door installation (pcs)\n12. Radiator installation (pcs) - if visible\n\n### FLOOR WORKS:\n1. Old floor demolition (m\u00b2) - if renovation\n2. Floor leveling/screed (m\u00b2)\n3. Insulation (m\u00b2) - if ground floor\n4. Underfloor heating (m\u00b2) - if applicable\n5. Primer/preparation (m\u00b2)\n6. Floor covering (m\u00b2)\n7. Baseboard installation (m)\n\n### WALL WORKS:\n- Drywall: Metal framing (m\u00b2) \u2192 Insulation (m\u00b2) \u2192 Boarding (m\u00b2) \u2192 Jointing (m\u00b2) \u2192 Painting (m\u00b2)\n- Masonry: Brickwork (m\u00b2) \u2192 Plastering (m\u00b2) \u2192 Painting (m\u00b2)\n- Tiling: Wall preparation (m\u00b2) \u2192 Tiling (m\u00b2) \u2192 Grouting (m\u00b2)\n\n### WINDOW WORKS:\n1. Old window demolition (pcs) - if renovation\n2. Window frame installation (pcs)\n3. Glazing (m\u00b2)\n4. Internal sill (m)\n5. External sill (m)\n6. Sealing/foam (m)\n7. Trim/architrave (m)\n8. Painting/finishing (m)\n\n### DOOR WORKS:\n1. Old door demolition (pcs) - if renovation\n2. Door frame installation (pcs)\n3. Door leaf hanging (pcs)\n4. Hardware installation (pcs)\n5. Trim/architrave (m)\n6. Painting/finishing (m\u00b2)\n\n### ELECTRICAL WORKS:\n1. Cable routing (m)\n2. Socket installation (pcs)\n3. Switch installation (pcs)\n4. Junction box (pcs)\n5. Lighting point (pcs)\n6. Panel/breaker installation (pcs)\n\n### PLUMBING WORKS:\n1. Pipe installation - supply (m)\n2. Pipe installation - drain (m)\n3. Valve installation (pcs)\n4. Connection to fixture (pcs)\n5. Pressure testing (pcs)\n\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n## CRITICAL RULES:\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n1. **NEVER return empty work_items** - minimum 3 works per element\n2. **Include PREPARATION works** - demolition, priming, leveling\n3. **Include FINISHING works** - painting, sealing, cleaning\n4. **Match units correctly**: areas\u2192m\u00b2, lengths\u2192m, items\u2192pcs\n5. **For RENOVATION**: always include demolition/removal works first\n6. **Scale quantities** from dimensions provided\n7. **search_query must be in {{ $json.search_lang }}** for vector database\n\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nReturn ONLY valid JSON:\n{\n \"work_items\": [\n {\n \"work_sequence\": 1,\n \"work_category\": \"PREPARATION|MAIN|FINISHING|MEP\",\n \"work_name\": \"Work name in {{ $json.language }}\",\n \"search_query\": \"Search terms in {{ $json.search_lang }} for DDC CWICR database - be specific!\",\n \"quantity\": 12.5,\n \"unit\": \"m\u00b2|m|pcs\",\n \"calculation_basis\": \"floor_area \u00d7 1.0 = 12.5 m\u00b2\",\n \"source_element\": \"Which element this work is for\",\n \"is_demolition\": false\n }\n ],\n \"total_works_count\": 15,\n \"phases\": [\"PREPARATION\", \"MAIN\", \"FINISHING\", \"MEP\"]\n}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "b7550960-96b9-400a-82cd-57fcae2813ec",
"name": "STAGE 4 Decompose LLM",
"type": "@n8n/n8n-nodes-langchain.chainLlm",
"position": [
30880,
384
],
"parameters": {
"messages": {
"messageValues": [
{
"message": "={{ $json.chatInput }}"
}
]
}
},
"typeVersion": 1.4
},
{
"id": "b973f4c7-7800-48ac-bd3f-f6a7f1e4d278",
"name": "GPT-4 Decompose",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
"position": [
30880,
592
],
"parameters": {
"model": {
"__rl": true,
"mode": "list",
"value": "chatgpt-4o-latest",
"cachedResultName": "chatgpt-4o-latest"
},
"options": {
"maxTokens": 8000,
"temperature": 0.3
}
},
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.2
},
{
"id": "ae0ff40d-08ce-4c80-8cdc-69d7a343b1a7",
"name": "Parse STAGE 4",
"type": "n8n-nodes-base.code",
"position": [
31168,
384
],
"parameters": {
"jsCode": "// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// PARSE STAGE 4 DECOMPOSITION RESPONSE\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nconst aiResponse = $input.first().json;\nconst configData = $('Parse STAGE 1').first().json;\n\nlet parsed = { work_items: [] };\n\ntry {\n const content = aiResponse.text || aiResponse.content || aiResponse.response || '';\n let jsonStr = content;\n \n const jsonMatch = content.match(/```json\\s*([\\s\\S]*?)\\s*```/);\n if (jsonMatch) {\n jsonStr = jsonMatch[1];\n } else {\n const objMatch = content.match(/\\{[\\s\\S]*\\}/);\n if (objMatch) {\n jsonStr = objMatch[0];\n }\n }\n \n parsed = JSON.parse(jsonStr);\n} catch (error) {\n console.error('STAGE 4 Parse error:', error.message);\n}\n\n// Validate and enrich work items\nconst workItems = (parsed.work_items || []).map((work, idx) => {\n // Ensure search_query exists and is meaningful\n let searchQuery = work.search_query || work.work_name || '';\n \n // Add language-specific terms if needed\n if (searchQuery.length < 5) {\n searchQuery = work.work_name + ' ' + (work.source_element || '');\n }\n \n return {\n work_id: `W${String(idx + 1).padStart(3, '0')}`,\n work_sequence: work.work_sequence || idx + 1,\n work_category: work.work_category || 'MAIN',\n work_name: work.work_name || 'Unnamed work',\n search_query: searchQuery.trim(),\n project_quantity: parseFloat(work.quantity) || 1,\n unit: work.unit || 'm\u00b2',\n calculation_basis: work.calculation_basis || '',\n source_element: work.source_element || '',\n is_demolition: work.is_demolition || false\n };\n});\n\n// Sort by sequence\nworkItems.sort((a, b) => a.work_sequence - b.work_sequence);\n\nreturn {\n json: {\n ...configData,\n work_items: workItems,\n works_count: workItems.length,\n phases: parsed.phases || ['PREPARATION', 'MAIN', 'FINISHING', 'MEP'],\n stage4_success: workItems.length >= 3\n }\n};"
},
"typeVersion": 2
},
{
"id": "70e62b94-292e-4f2e-b3bc-ca607618d772",
"name": "Prepare Works",
"type": "n8n-nodes-base.code",
"position": [
31344,
384
],
"parameters": {
"jsCode": "// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// PREPARE WORKS FOR LOOP\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nconst data = $input.first().json;\nconst works = data.work_items || [];\n\nconst staticData = $getWorkflowStaticData('global');\nstaticData.photo_config = {\n language_code: data.language_code,\n language: data.language,\n currency: data.currency,\n currency_symbol: data.currency_symbol,\n locale: data.locale,\n city: data.city,\n qdrant_collection: data.qdrant_collection,\n room_type: data.room_type,\n room_description: data.room_description,\n dimensions: data.dimensions,\n work_type_detected: data.work_type_detected,\n phases: data.phases,\n elements: data.elements,\n fixtures: data.fixtures\n};\nstaticData.work_results = [];\n\nif (works.length === 0) {\n return [{ json: { _no_works: true, message: 'No work items generated' } }];\n}\n\nreturn works.map((work) => ({\n json: {\n ...work,\n expected_unit: work.unit,\n type_name: work.work_name,\n category: work.work_category,\n assigned_phase: work.work_category,\n qdrant_collection: data.qdrant_collection,\n language: data.language,\n currency: data.currency,\n currency_symbol: data.currency_symbol,\n locale: data.locale\n }\n}));"
},
"typeVersion": 2
},
{
"id": "10b9b98b-f782-499f-9c12-e254af13bd0a",
"name": "Loop Works",
"type": "n8n-nodes-base.splitInBatches",
"position": [
31536,
384
],
"parameters": {
"options": {
"reset": false
}
},
"typeVersion": 3
},
{
"id": "79f735d7-6de7-4a05-88e7-12d08b2fd6df",
"name": "Store Work Data",
"type": "n8n-nodes-base.code",
"position": [
32016,
592
],
"parameters": {
"jsCode": "// Store current work item for Vector Search\nconst work = $input.first().json;\nconst staticData = $getWorkflowStaticData('global');\nstaticData.currentWork = work;\nreturn { json: work };"
},
"typeVersion": 2
},
{
"id": "60873726-0ece-4e69-91dc-73c77ad796b2",
"name": "Wait",
"type": "n8n-nodes-base.wait",
"position": [
32160,
592
],
"parameters": {
"amount": 0.3
},
"typeVersion": 1.1
},
{
"id": "c5e35231-575a-4d53-95c4-ef2875363906",
"name": "Restore Work Data",
"type": "n8n-nodes-base.code",
"position": [
32304,
592
],
"parameters": {
"jsCode": "// Restore work data from staticData after Wait\nconst staticData = $getWorkflowStaticData('global');\nconst work = staticData.currentWork || {};\nreturn { json: work };"
},
"typeVersion": 2
},
{
"id": "595e380e-4b57-4063-a594-fb89e010f81f",
"name": "Vector Search",
"type": "@n8n/n8n-nodes-langchain.vectorStoreQdrant",
"position": [
32464,
592
],
"parameters": {
"mode": "load",
"topK": 5,
"prompt": "={{ $json.search_query || $json.work_name }}",
"options": {
"contentPayloadKey": "content"
},
"qdrantCollection": {
"__rl": true,
"mode": "id",
"value": "={{ $json.qdrant_collection }}"
}
},
"credentials": {
"qdrantApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.1
},
{
"id": "782d04ee-61c6-40a0-85bf-c5554533f3b5",
"name": "Embeddings",
"type": "@n8n/n8n-nodes-langchain.embeddingsOpenAi",
"position": [
32448,
752
],
"parameters": {
"model": "text-embedding-3-large",
"options": {
"dimensions": 3072
}
},
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.2
},
{
"id": "e527cbd6-8841-4425-902a-ee4a28706c2f",
"name": "STAGE 5 Parse & Score",
"type": "n8n-nodes-base.code",
"position": [
32720,
592
],
"parameters": {
"jsCode": "// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// STAGE 5: PARSE & VALIDATE VECTOR SEARCH RESULTS\n// FIXED: Correct document/pageContent extraction\n// Quality scoring v2.0 + Resource extraction\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nconst searchResults = $input.all();\nconst staticData = $getWorkflowStaticData('global');\nconst workData = staticData.currentWork || {};\n\n// MACHINE/LABOR PATTERNS (9 languages)\nconst MACHINE_PATTERNS = ['masch', 'maschinenstunde', 'ger\u00e4t', 'machine', 'mach-h', 'equipment', 'heure machine', 'engin', 'm\u00e1quina', 'equipo', 'equipamento', '\u043c\u0430\u0448\u0438\u043d', '\u043c\u0430\u0448-\u0447', '\u043c\u0430\u0448.\u0447', '\u043c\u0430\u0448.-\u0447', '\u043c\u0435\u0445\u0430\u043d\u0438\u0437\u043c', '\u673a\u5668', '\u673a\u68b0', '\u8bbe\u5907', '\u53f0\u73ed', '\u0622\u0644\u0629', '\u0645\u0639\u062f\u0629', '\u092e\u0936\u0940\u0928', '\u092f\u0902\u0924\u094d\u0930'];\nconst LABOR_PATTERNS = ['std', 'stunde', 'arbeiter', 'hour', 'man-hour', 'labor', 'worker', 'heure', 'ouvrier', 'hora', 'obrero', 'oper\u00e1rio', '\u0447-\u0447', '\u0447\u0435\u043b-\u0447', '\u0447\u0435\u043b\u043e\u0432\u0435\u043a\u043e-\u0447\u0430\u0441', '\u0442\u0440\u0443\u0434', '\u0440\u0430\u0431\u043e\u0447\u0438\u0439', '\u0440\u0430\u0437\u0440\u044f\u0434', ' \u0447', '\u5de5\u65f6', '\u4eba\u5de5', '\u5de5\u4eba', '\u0633\u0627\u0639\u0629', '\u0639\u0627\u0645\u0644', '\u0918\u0902\u091f\u093e', '\u0936\u094d\u0930\u092e', '\u092e\u091c\u0926\u0942\u0930'];\n\nfunction detectResourceType(unit, name, code) {\n const combined = ((unit || '') + ' ' + (name || '') + ' ' + (code || '')).toLowerCase();\n if (MACHINE_PATTERNS.some(p => combined.includes(p.toLowerCase()))) return 'machine';\n if (LABOR_PATTERNS.some(p => combined.includes(p.toLowerCase()))) return 'labor';\n return 'material';\n}\n\nfunction normalizeUnit(unit) {\n if (!unit) return '';\n const u = unit.toLowerCase().trim();\n const unitGroups = {\n 'm2': ['m\u00b2', 'm2', 'qm', '\u043a\u0432\u043c', '\u043a\u0432.\u043c', '\u043a\u0432. \u043c', 'sq.m', 'sqm', '\u5e73\u65b9\u7c73'],\n 'm3': ['m\u00b3', 'm3', 'cbm', '\u043a\u0443\u0431.\u043c', '\u043a\u0443\u0431. \u043c', 'cu.m', '\u7acb\u65b9\u7c73'],\n 'm': ['m', '\u043c', 'lm', 'lfm', '\u043f.\u043c', '\u043f. \u043c', 'lin.m', '\u7c73'],\n 'stk': ['stk', 'st\u00fcck', 'st', '\u0448\u0442', '\u0448\u0442.', 'pcs', 'ea', 'each', 'unit', '\u4e2a', '\u4ef6'],\n '100m2': ['100 m\u00b2', '100m\u00b2', '100 m2', '100m2', '100 qm', '100 \u043a\u0432.\u043c']\n };\n for (const [normalized, variants] of Object.entries(unitGroups)) {\n if (variants.some(v => u === v || u.includes(v))) return normalized;\n }\n return u;\n}\n\n// MATERIAL KEYWORDS for matching\nconst MATERIAL_KEYWORDS = [\n 'aluminum', 'aluminium', 'alu', '\u0430\u043b\u044e\u043c\u0438\u043d',\n 'wood', 'holz', '\u0434\u0435\u0440\u0435\u0432', '\u0434\u0440\u0435\u0432\u0435\u0441', '\u6728',\n 'plastic', 'pvc', 'kunststoff', '\u043f\u043b\u0430\u0441\u0442\u0438\u043a', '\u043f\u0432\u0445',\n 'steel', 'stahl', '\u0441\u0442\u0430\u043b', '\u043c\u0435\u0442\u0430\u043b\u043b', '\u94a2',\n 'concrete', 'beton', '\u0431\u0435\u0442\u043e\u043d', '\u6df7\u51dd\u571f',\n 'glass', 'glas', '\u0441\u0442\u0435\u043a\u043b', '\u73bb\u7483',\n 'brick', 'ziegel', '\u043a\u0438\u0440\u043f\u0438\u0447', '\u7816',\n 'tile', 'fliese', '\u043f\u043b\u0438\u0442\u043a', '\u043a\u0430\u0444\u0435\u043b\u044c', '\u74f7\u7816',\n 'ceramic', 'keramik', '\u043a\u0435\u0440\u0430\u043c\u0438\u043a', '\u9676\u74f7',\n 'gypsum', 'gips', '\u0433\u0438\u043f\u0441', '\u77f3\u818f',\n 'drywall', '\u0433\u0438\u043f\u0441\u043e\u043a\u0430\u0440\u0442\u043e\u043d', '\u0433\u043a\u043b',\n 'paint', 'farbe', '\u043a\u0440\u0430\u0441\u043a', '\u6cb9\u6f06',\n 'wallpaper', 'tapete', '\u043e\u0431\u043e', '\u58c1\u7eb8',\n 'laminate', 'laminat', '\u043b\u0430\u043c\u0438\u043d\u0430\u0442',\n 'parquet', 'parkett', '\u043f\u0430\u0440\u043a\u0435\u0442'\n];\n\n// WORK TYPE KEYWORDS for matching\nconst WORK_TYPE_KEYWORDS = [\n 'installation', 'montage', '\u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a', '\u043c\u043e\u043d\u0442\u0430\u0436', '\u5b89\u88c5',\n 'demolition', 'abbruch', '\u0434\u0435\u043c\u043e\u043d\u0442\u0430\u0436', '\u0441\u043d\u043e\u0441', '\u0440\u0430\u0437\u0431\u043e\u0440', '\u62c6\u9664',\n 'preparation', 'vorbereitung', '\u043f\u043e\u0434\u0433\u043e\u0442\u043e\u0432', '\u51c6\u5907',\n 'sealing', 'abdichtung', '\u0433\u0435\u0440\u043c\u0435\u0442\u0438\u0437', '\u0433\u0438\u0434\u0440\u043e\u0438\u0437\u043e\u043b', '\u5bc6\u5c01',\n 'insulation', 'd\u00e4mmung', '\u0438\u0437\u043e\u043b\u044f\u0446', '\u0443\u0442\u0435\u043f\u043b', '\u4fdd\u6e29',\n 'finishing', '\u043e\u0442\u0434\u0435\u043b\u043a', 'finish', '\u88c5\u4fee',\n 'excavation', 'aushub', '\u0432\u044b\u0435\u043c\u043a', '\u043a\u043e\u043f\u0430\u043d', '\u5f00\u6316',\n 'concrete', 'beton', '\u0431\u0435\u0442\u043e\u043d\u0438\u0440', '\u6df7\u51dd\u571f',\n 'reinforcement', 'bewehrung', '\u0430\u0440\u043c\u0438\u0440', '\u94a2\u7b4b',\n 'plastering', 'putz', '\u0448\u0442\u0443\u043a\u0430\u0442\u0443\u0440', '\u62b9\u7070',\n 'painting', 'malen', 'anstrich', '\u043e\u043a\u0440\u0430\u0441\u043a', '\u043f\u043e\u043a\u0440\u0430\u0441\u043a', '\u6cb9\u6f06',\n 'tiling', 'fliesen', '\u043e\u0431\u043b\u0438\u0446\u043e\u0432', '\u0443\u043a\u043b\u0430\u0434\u043a \u043f\u043b\u0438\u0442\u043a', '\u8d34\u7816',\n 'flooring', 'boden', '\u043d\u0430\u043f\u043e\u043b\u044c\u043d', '\u043f\u043e\u043b', '\u5730\u677f',\n 'roofing', 'dach', '\u043a\u0440\u043e\u0432\u043b', '\u5c4b\u9762',\n 'plumbing', 'sanit\u00e4r', '\u0441\u0430\u043d\u0442\u0435\u0445\u043d', '\u7ba1\u9053',\n 'electrical', 'elektro', '\u044d\u043b\u0435\u043a\u0442\u0440', '\u7535\u6c14'\n];\n\n// NOT FOUND FALLBACK\nif (!searchResults || searchResults.length === 0) {\n return [{\n json: {\n ...workData,\n rate_code: 'NOT_FOUND',\n rate_name: '[Not Found] ' + (workData.work_name || 'Unknown'),\n rate_unit: workData.expected_unit || 'm\u00b2',\n unit_cost: 0,\n total_cost: 0,\n estimated_labor_hours: 0,\n quality_level: 'not_found',\n quality_score: 0,\n quality_reason: 'No search results',\n resources_all: [],\n cost_breakdown: { workers: 0, machines: 0, materials: 0 }\n }\n }];\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// FIX: CORRECT EXTRACTION OF document.pageContent\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nconst parsedResults = searchResults.map((item) => {\n const data = item.json || {};\n \n // FIX: Handle nested document structure from Vector Search\n const doc = data.document || {};\n const content = String(doc.pageContent || doc.content || data.pageContent || data.content || '');\n const metadata = doc.metadata || data.metadata || {};\n const score = data.score || 0;\n \n // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n // EXTRACT TOTAL COST from \"Total cost: 3127.16 EUR\"\n // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n let totalCost = 0;\n const costPatterns = [\n /Total\\s*cost:\\s*([\\d.,]+)\\s*(?:EUR|USD|RUB|CAD|CNY|AED|INR|BRL)/i,\n /Resources?\\s*cost:\\s*([\\d.,]+)/i,\n /(?:Gesamt|\u0418\u0422\u041e\u0413\u041e|\u0412\u0441\u0435\u0433\u043e|\u0421\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u044c)[^\\d]*([\\d.,]+)/i,\n /(?:EUR|USD|RUB|CAD|\\$|\u20ac|\u20bd)\\s*([\\d.,]+)/i\n ];\n for (const pattern of costPatterns) {\n const match = content.match(pattern);\n if (match) {\n totalCost = parseFloat(match[1].replace(/,/g, '.').replace(/\\s/g, '')) || 0;\n if (totalCost > 0) break;\n }\n }\n \n // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n // EXTRACT CODE, NAME, UNIT from content if not in metadata\n // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n let rateCode = metadata.rsts || metadata.code || '';\n let rateName = metadata.names || metadata.name || '';\n let rateUnit = metadata.unit || '';\n \n // Fallback: extract from content\n if (!rateCode) {\n const codeMatch = content.match(/CODE:\\s*([A-Z\u0410-\u042fa-z\u0430-\u044f0-9_-]+)/i);\n if (codeMatch) rateCode = codeMatch[1];\n }\n if (!rateName) {\n const nameMatch = content.match(/NAME:\\s*(.+?)(?:\\n|UNIT:|CATEGORY:)/i);\n if (nameMatch) rateName = nameMatch[1].trim();\n }\n if (!rateUnit) {\n const unitMatch = content.match(/UNIT:\\s*(.+?)(?:\\n|CATEGORY:)/i);\n if (unitMatch) rateUnit = unitMatch[1].trim();\n }\n \n // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n // EXTRACT LABOR HOURS\n // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n let laborHours = 0;\n const laborMatch = content.match(/(?:\u0440\u0430\u0437\u0440\u044f\u0434|worker|arbeiter|labor)[^\u2014\\n]*\u2014\\s*([\\d.,]+)\\s*(?:\u0447|\u0447\u0430\u0441|std|hour|h)/i);\n if (laborMatch) {\n laborHours = parseFloat(laborMatch[1].replace(',', '.')) || 0;\n }\n \n // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n // EXTRACT RESOURCES from RESOURCES: section\n // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n const resources = [];\n const resourcesSection = content.match(/RESOURCES:\\s*([\\s\\S]*?)(?:$|MACHINES:|SCOPE|CLASSIFICATION:)/i);\n if (resourcesSection) {\n const resourcesText = resourcesSection[1];\n const resourceLines = resourcesText.split('\\n').filter(line => line.trim().startsWith('-'));\n \n for (const line of resourceLines) {\n const resMatch = line.match(/^-\\s*([A-Z\u0410-\u042fa-z\u0430-\u044f0-9_-]+)\\s*[\u2014\u2013-]\\s*(.+?)\\s*[\u2014\u2013-]\\s*([\\d.,]+)\\s*(.+?)$/i);\n if (resMatch) {\n const resCode = resMatch[1].trim();\n const resName = resMatch[2].trim();\n const resQty = parseFloat(resMatch[3].replace(',', '.')) || 0;\n const resUnit = resMatch[4].trim();\n const resType = detectResourceType(resUnit, resName, resCode);\n \n resources.push({\n resource_code: resCode,\n resource_name: resName,\n resource_quantity: resQty,\n resource_unit: resUnit,\n resource_cost: 0,\n resource_type: resType\n });\n }\n }\n }\n \n return {\n rate_code: rateCode,\n rate_name: rateName,\n rate_unit: rateUnit,\n total_cost_position: totalCost,\n worker_labor_hours: laborHours,\n hierarchy: metadata.hierarchy || '',\n resources: resources,\n resources_count: resources.length,\n vector_score: score,\n content_preview: content.substring(0, 300)\n };\n});\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// QUALITY SCORING v2.0\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nconst expectedUnit = normalizeUnit(workData.expected_unit || workData.unit || '');\nconst searchQuery = (workData.search_query || workData.work_name || '').toLowerCase();\nconst workName = (workData.work_name || '').toLowerCase();\n\nconst scoredResults = parsedResults.map(result => {\n let score = 0;\n const factors = [];\n const resultUnit = normalizeUnit(result.rate_unit);\n const rateName = (result.rate_name || '').toLowerCase();\n const rateContent = (result.content_preview || '').toLowerCase();\n const combined = rateName + ' ' + rateContent;\n \n if (result.total_cost_position > 0) { \n score += 30; \n factors.push('has_price:' + result.total_cost_position.toFixed(0)); \n }\n \n if (result.resources_count > 0) { \n score += Math.min(25, result.resources_count * 5); \n factors.push('resources:' + result.resources_count); \n }\n \n if (resultUnit && expectedUnit) {\n if (resultUnit === expectedUnit) { \n score += 20; \n factors.push('unit_exact'); \n } else if (resultUnit.includes(expectedUnit) || expectedUnit.includes(resultUnit)) { \n score += 10; \n factors.push('unit_partial'); \n }\n }\n \n const materialMatches = MATERIAL_KEYWORDS.filter(kw => \n (workName.includes(kw) || searchQuery.includes(kw)) && combined.includes(kw)\n );\n if (materialMatches.length > 0) {\n score += 15;\n factors.push('material:' + materialMatches[0]);\n }\n \n const workTypeMatches = WORK_TYPE_KEYWORDS.filter(kw => \n (workName.includes(kw) || searchQuery.includes(kw)) && combined.includes(kw)\n );\n if (workTypeMatches.length > 0) {\n score += 10;\n factors.push('work_type:' + workTypeMatches[0]);\n }\n \n const queryWords = searchQuery.split(/\\s+/).filter(w => w.length > 3);\n const matchedWords = queryWords.filter(w => rateName.includes(w) || rateContent.includes(w));\n if (matchedWords.length > 0) { \n score += Math.min(15, matchedWords.length * 5); \n factors.push('words:' + matchedWords.length); \n }\n \n if (result.vector_score > 0.5) {\n score += 10;\n factors.push('vscore:' + result.vector_score.toFixed(2));\n } else if (result.vector_score > 0.4) {\n score += 5;\n factors.push('vscore:' + result.vector_score.toFixed(2));\n }\n \n const hasLabor = result.resources.some(r => r.resource_type === 'labor');\n const hasMaterial = result.resources.some(r => r.resource_type === 'material');\n if (hasLabor && hasMaterial) {\n score += 5;\n factors.push('complete_rate');\n }\n \n return { ...result, quality_score: score, quality_factors: factors };\n});\n\nconst sorted = scoredResults.sort((a, b) => b.quality_score - a.quality_score);\nconst best = sorted[0] || { quality_score: 0, quality_factors: [], resources: [] };\n\nlet qualityLevel = 'not_found';\nconst s = best.quality_score;\nif (s >= 60) { qualityLevel = 'high'; }\nelse if (s >= 40) { qualityLevel = 'medium'; }\nelse if (s >= 20) { qualityLevel = 'low'; }\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// CALCULATE COSTS\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nconst projectQuantity = workData.project_quantity || 0;\nconst unitCost = best.total_cost_position || 0;\n\nlet unitDivisor = 1;\nconst rateUnit = (best.rate_unit || '').toLowerCase();\nif (rateUnit.includes('100')) unitDivisor = 100;\nelse if (rateUnit.includes('10 ')) unitDivisor = 10;\n\nconst quantityInRateUnits = projectQuantity / unitDivisor;\nconst totalCost = quantityInRateUnits * unitCost;\nconst estimatedLaborHours = (best.worker_labor_hours || 0) * quantityInRateUnits;\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// SCALE & CATEGORIZE RESOURCES\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nlet laborCost = 0, machineCost = 0, materialCost = 0;\nconst scaledResources = (best.resources || []).map(r => {\n const scaledQty = (r.resource_quantity || 0) * quantityInRateUnits;\n const scaledCost = (r.resource_cost || 0) * quantityInRateUnits;\n \n if (r.resource_type === 'labor') laborCost += scaledCost;\n else if (r.resource_type === 'machine') machineCost += scaledCost;\n else materialCost += scaledCost;\n \n return { ...r, scaled_quantity: scaledQty, scaled_cost: scaledCost };\n});\n\nif (laborCost === 0 && materialCost === 0 && totalCost > 0) {\n laborCost = totalCost * 0.35;\n materialCost = totalCost * 0.55;\n machineCost = totalCost * 0.10;\n}\n\nreturn [{\n json: {\n work_id: workData.work_id,\n work_sequence: workData.work_sequence,\n work_category: workData.work_category,\n work_name: workData.work_name,\n source_element: workData.source_element,\n calculation_basis: workData.calculation_basis,\n is_demolition: workData.is_demolition,\n rate_code: best.rate_code || 'NOT_FOUND',\n rate_name: best.rate_name || workData.work_name || 'Not found',\n rate_unit: best.rate_unit || workData.expected_unit,\n project_quantity: projectQuantity,\n project_unit: workData.unit || workData.expected_unit,\n quantity_in_rate_units: quantityInRateUnits,\n calculated_quantity: quantityInRateUnits,\n unit_divisor: unitDivisor,\n unit_cost: unitCost,\n total_cost: totalCost,\n estimated_labor_hours: estimatedLaborHours,\n quality_level: qualityLevel,\n quality_score: s,\n quality_reason: 'Score ' + s + '/100: ' + best.quality_factors.join(', '),\n currency: workData.currency,\n currency_symbol: workData.currency_symbol,\n locale: workData.locale,\n type_name: workData.type_name,\n category: workData.category,\n assigned_phase: workData.assigned_phase,\n resources_all: scaledResources,\n cost_breakdown: {\n workers: laborCost,\n machines: machineCost,\n materials: materialCost\n },\n calculation_details: {\n method: 'photo_analysis',\n calculation_basis: workData.calculation_basis,\n raw_value: projectQuantity,\n unit_divisor: unitDivisor,\n formula_display: (workData.unit || 'Qty') + ' = ' + projectQuantity\n },\n search_debug: {\n query_used: workData.search_query,\n results_count: searchResults.length,\n best_match_score: s,\n best_vector_score: best.vector_score || 0,\n best_rate_found: best.rate_name || 'none'\n }\n }\n}];"
},
"typeVersion": 2
},
{
"id": "577e8350-ff19-4b73-a53f-80605484a493",
"name": "Accumulate",
"type": "n8n-nodes-base.code",
"position": [
32864,
592
],
"parameters": {
"jsCode": "// ACCUMULATE RESULTS\nconst staticData = $getWorkflowStaticData('global');\nconst work = $input.first().json;\nif (!staticData.work_results) staticData.work_results = [];\nstaticData.work_results.push(work);\nreturn { json: work };"
},
"typeVersion": 2
},
{
"id": "9b8dd6f9-e36b-4fc0-bac8-5a0857890e32",
"name": "STAGE 7.5 Aggregate & Validate",
"type": "n8n-nodes-base.code",
"position": [
31824,
352
],
"parameters": {
"jsCode": "// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// STAGE 7.5: AGGREGATE & VALIDATE RESULTS\n// Build by_phase structure with validation\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nconst staticData = $getWorkflowStaticData('global');\nconst config = staticData.photo_config || {};\nconst works = staticData.work_results || [];\n\n// Calculate totals\nlet grandTotal = 0, grandHours = 0;\nlet grandLaborCost = 0, grandMaterialCost = 0, grandMachineCost = 0;\nlet qHigh = 0, qMedium = 0, qLow = 0, qNotFound = 0;\n\nworks.forEach(w => {\n grandTotal += w.total_cost || 0;\n grandHours += w.estimated_labor_hours || 0;\n \n const cb = w.cost_breakdown || {};\n grandLaborCost += cb.workers || 0;\n grandMaterialCost += cb.materials || 0;\n grandMachineCost += cb.machines || 0;\n \n if (w.quality_level === 'high') qHigh++;\n else if (w.quality_level === 'medium') qMedium++;\n else if (w.quality_level === 'low') qLow++;\n else qNotFound++;\n});\n\n// Sort by sequence\nworks.sort((a, b) => (a.work_sequence || 0) - (b.work_sequence || 0));\n\n// Group by category (PREPA
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.
openAiApiqdrantApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Upload a construction photo via web form → get a detailed cost estimate with work breakdown, resource costs, and professional HTML report. Powered by GPT-4 Vision and the open-source DDC CWICR database (55,000+ work items). Site managers who need quick estimates from mobile…
Source: https://n8n.io/workflows/12175/ — 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.
I originally started to template to ask questions on the "n8n @ scale office-hours" livestream videos but then extended it to include the latest videos on the official channel.
Code Extractfromfile. Uses manualTrigger, sort, httpRequest, compression. Event-driven trigger; 50 nodes.
2464. Uses httpRequest, compression, editImage, documentDefaultDataLoader. Event-driven trigger; 50 nodes.
Workflow 2464. Uses httpRequest, compression, editImage, documentDefaultDataLoader. Event-driven trigger; 50 nodes.
Are you a popular tech startup accelerator (named after a particular higher order function) overwhelmed with 1000s of pitch decks on a daily basis? Wish you could filter through them quickly using AI