This workflow corresponds to n8n.io template #7652 — we link there as the canonical source.
This workflow follows the Agent → Anthropic Chat 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": "Z1UKztds8E5oQSMe",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "n8n_6_Construction_Price_Estimation_with_LLM_for_Revt_and_IFC",
"tags": [],
"nodes": [
{
"id": "44ac22bc-92ff-450d-ac53-b2b587cc0318",
"name": "When clicking \u2018Execute workflow\u2019",
"type": "n8n-nodes-base.manualTrigger",
"position": [
944,
-64
],
"parameters": {},
"typeVersion": 1
},
{
"id": "2b81e046-ebc1-4687-8fd2-b9d2a3347a12",
"name": "Find Category Fields1",
"type": "n8n-nodes-base.code",
"position": [
1360,
768
],
"parameters": {
"jsCode": "\nconst items = $input.all();\nif (items.length === 0) {\n return [{json: {error: 'No grouped data found'}}];\n}\n\n\nconst headers = Object.keys(items[0].json);\n\n\nconst categoryPatterns = [\n { pattern: /^category$/i, type: 'Category' },\n { pattern: /^ifc[\\s_-]?type$/i, type: 'IFC' },\n { pattern: /^host[\\s_-]?category$/i, type: 'Host' },\n { pattern: /^ifc[\\s_-]?export[\\s_-]?as$/i, type: 'Export' },\n { pattern: /^layer$/i, type: 'Layer' }\n];\n\nlet categoryField = null;\nlet categoryFieldType = 'None';\n\n\nfor (const header of headers) {\n for (const {pattern, type} of categoryPatterns) {\n if (pattern.test(header)) {\n categoryField = header;\n categoryFieldType = type;\n break;\n }\n }\n if (categoryField) break;\n}\n\n\nconst volumetricPatterns = /volume|area|length|count|quantity|thickness|perimeter|depth|size|dimension|weight|mass/i;\nconst volumetricFields = headers.filter(header => volumetricPatterns.test(header));\n\n\nconst categoryValues = new Set();\nif (categoryField) {\n items.forEach(item => {\n const value = item.json[categoryField];\n if (value && value !== '' && value !== null) {\n categoryValues.add(value);\n }\n });\n}\n\nconsole.log('Category field analysis:');\nconsole.log('- Field found:', categoryField || 'None');\nconsole.log('- Field type:', categoryFieldType);\nconsole.log('- Unique values:', categoryValues.size);\nconsole.log('- Volumetric fields:', volumetricFields.length);\n\n\nreturn [{\n json: {\n categoryField: categoryField,\n categoryFieldType: categoryFieldType,\n categoryValues: Array.from(categoryValues),\n volumetricFields: volumetricFields,\n groupedData: items.map(item => item.json),\n totalGroups: items.length\n }\n}];"
},
"typeVersion": 2
},
{
"id": "65f07357-2e47-41ac-87ee-43d692b7511d",
"name": "Apply Classification to Groups1",
"type": "n8n-nodes-base.code",
"position": [
1872,
832
],
"parameters": {
"jsCode": "\nconst categoryInfo = $node['Find Category Fields1'].json;\nconst groupedData = categoryInfo.groupedData;\nconst categoryField = categoryInfo.categoryField;\nconst volumetricFields = categoryInfo.volumetricFields || [];\n\n\nlet classifications = {};\nlet buildingCategories = [];\nlet drawingCategories = [];\n\ntry {\n const aiResponse = $input.first().json;\n const content = aiResponse.content || aiResponse.message || aiResponse.response || '';\n \n const jsonMatch = content.match(/\\{[\\s\\S]*\\}/);\n if (jsonMatch) {\n const parsed = JSON.parse(jsonMatch[0]);\n classifications = parsed.classifications || {};\n buildingCategories = parsed.building_categories || [];\n drawingCategories = parsed.drawing_categories || [];\n console.log(`AI classified ${Object.keys(classifications).length} categories`);\n console.log(`Building categories: ${buildingCategories.length}`);\n console.log(`Drawing categories: ${drawingCategories.length}`);\n }\n} catch (error) {\n console.error('Error parsing AI classification:', error.message);\n}\n\n\nreturn groupedData.map(group => {\n let isBuildingElement = false;\n let reason = '';\n let confidence = 0;\n \n if (categoryField && group[categoryField]) {\n \n const categoryValue = group[categoryField];\n \n if (classifications[categoryValue] !== undefined) {\n isBuildingElement = classifications[categoryValue];\n confidence = 95;\n reason = `Category '${categoryValue}' classified by AI as ${isBuildingElement ? 'building element' : 'drawing/annotation'}`;\n } else {\n \n const lowerCategory = categoryValue.toLowerCase();\n const drawingKeywords = /annotation|drawing|text|dimension|tag|view|sheet|grid|section|elevation|callout|revision|legend|symbol|mark|note|detail items|filled region|detail line/i;\n const buildingKeywords = /wall|floor|roof|column|beam|door|window|stair|pipe|duct|equipment|fixture|furniture/i;\n \n if (drawingKeywords.test(lowerCategory)) {\n isBuildingElement = false;\n confidence = 85;\n reason = `Category '${categoryValue}' matched drawing keywords`;\n } else if (buildingKeywords.test(lowerCategory)) {\n isBuildingElement = true;\n confidence = 85;\n reason = `Category '${categoryValue}' matched building keywords`;\n } else {\n \n isBuildingElement = true;\n confidence = 70;\n reason = `Category '${categoryValue}' assumed as building element (no clear match)`;\n }\n }\n } else {\n \n let hasSignificantVolumetricData = false;\n let volumetricCount = 0;\n \n for (const field of volumetricFields) {\n const value = parseFloat(group[field]);\n if (!isNaN(value) && value > 0) {\n hasSignificantVolumetricData = true;\n volumetricCount++;\n }\n }\n \n isBuildingElement = hasSignificantVolumetricData;\n confidence = hasSignificantVolumetricData ? 80 : 60;\n reason = hasSignificantVolumetricData ? \n `Has ${volumetricCount} volumetric parameters with values` : \n 'No category field and no significant volumetric data';\n }\n \n return {\n json: {\n ...group,\n is_building_element: isBuildingElement,\n element_confidence: confidence,\n element_reason: reason\n }\n };\n});"
},
"typeVersion": 2
},
{
"id": "50462cce-7978-4147-b215-25749d3bbf79",
"name": "Non-Building Elements Output1",
"type": "n8n-nodes-base.set",
"position": [
2272,
848
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "message",
"name": "message",
"type": "string",
"value": "Non-building elements (drawings, annotations, etc.)"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "93e37bf0-a126-4174-9bc6-a8a5378885a1",
"name": "AI Classify Categories1",
"type": "@n8n/n8n-nodes-langchain.openAi",
"position": [
1536,
832
],
"parameters": {
"modelId": {
"__rl": true,
"mode": "list",
"value": "chatgpt-4o-latest",
"cachedResultName": "CHATGPT-4O-LATEST"
},
"options": {
"maxTokens": 4000,
"temperature": 0.1
},
"messages": {
"values": [
{
"role": "system",
"content": "You are an expert in Revit, BIM (Building Information Modeling) and construction classification. Your task is to classify category values as either building elements or non-building elements (drawings, annotations, etc.).\n\nBuilding elements include:\n- Structural elements (walls, floors, roofs, columns, beams, foundations, slabs)\n- MEP elements (pipes, ducts, equipment, fixtures, mechanical equipment)\n- Architectural elements (doors, windows, stairs, railings, curtain walls)\n- Site elements (parking, roads, landscaping)\n- Furniture and fixtures\n- Any physical construction element that has volume, area, or physical properties\n\nNon-building elements include:\n- Drawings and sheets\n- Annotations, dimensions, text notes\n- Views, sections, elevations, plans\n- Tags, symbols, legends, schedules\n- Grids, levels, reference planes\n- Revision clouds, callouts, detail items\n- Lines, filled regions, detail lines\n- Any 2D documentation or annotation element\n\nIMPORTANT: Analyze the actual category name, not just keywords. For example:\n- \"Detail Items\" = non-building (annotation)\n- \"Plumbing Fixtures\" = building element\n- \"Room Tags\" = non-building (annotation)\n- \"Structural Columns\" = building element\n\nReturn ONLY valid JSON in this exact format:\n{\n \"classifications\": {\n \"category_value_1\": true,\n \"category_value_2\": false\n },\n \"building_categories\": [\"list\", \"of\", \"building\", \"categories\"],\n \"drawing_categories\": [\"list\", \"of\", \"drawing\", \"categories\"]\n}"
},
{
"content": "{{ $json.categoryField ? `Classify these ${$json.categoryValues.length} category values from field '${$json.categoryField}' as building elements (true) or drawings/annotations (false):\n\nCategory values:\n${JSON.stringify($json.categoryValues, null, 2)}\n\nCategory field type: ${$json.categoryFieldType}` : 'No category field found. Please classify based on volumetric data presence.'}}"
}
]
}
},
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.3
},
{
"id": "2adc7c4e-1133-43bd-a3b8-8648a95861be",
"name": "Is Building Element1",
"type": "n8n-nodes-base.if",
"position": [
2064,
832
],
"parameters": {
"conditions": {
"boolean": [
{
"value1": "={{ $json.is_building_element }}",
"value2": true
}
]
}
},
"typeVersion": 1
},
{
"id": "a6b94ddd-8674-4072-8bb0-f2a813b4c042",
"name": "Check If All Batches Done",
"type": "n8n-nodes-base.if",
"position": [
1024,
1280
],
"parameters": {
"options": {},
"conditions": {
"options": {
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "9af8b6d4-3a3a-4c4a-8d4d-d5a8c2958f5f",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"leftValue": "={{ $node['Process in Batches1'].context['noItemsLeft'] }}",
"rightValue": true
}
]
}
},
"typeVersion": 2
},
{
"id": "6a62ad33-7c47-495d-b81b-3be78ae0cd81",
"name": "Collect All Results",
"type": "n8n-nodes-base.code",
"position": [
1200,
1568
],
"parameters": {
"jsCode": "// Get all accumulated data and prepare for reporting\nconst storedData = $getWorkflowStaticData('global');\nconst allProcessedItems = storedData.accumulatedData || [];\n\nconsole.log(`\\n=== BATCH PROCESSING COMPLETE ===`);\nconsole.log(`Total items processed: ${allProcessedItems.length}`);\n\n// Clear accumulated data for next run\nstoredData.accumulatedData = [];\n\n// Return all items for report generation\nreturn allProcessedItems;"
},
"typeVersion": 2
},
{
"id": "e3ab164d-d34c-4415-9ecc-d1e2dcd35ebe",
"name": "Process in Batches1",
"type": "n8n-nodes-base.splitInBatches",
"position": [
1184,
1152
],
"parameters": {
"options": {},
"batchSize": 1
},
"typeVersion": 1
},
{
"id": "b1504e4e-6a04-4014-aeed-b38dbaa9b12d",
"name": "Clean Empty Values1",
"type": "n8n-nodes-base.code",
"position": [
1360,
1152
],
"parameters": {
"jsCode": "// Clean empty values from the item\nreturn $input.all().map(item => {\n const cleanedJson = {};\n Object.entries(item.json).forEach(([key, value]) => {\n if (value !== null) {\n if (typeof value === 'number') {\n if (value !== 0 || key === 'Element Count') {\n cleanedJson[key] = value;\n }\n } else if (value !== '') {\n cleanedJson[key] = value;\n }\n }\n });\n return { json: cleanedJson };\n});"
},
"typeVersion": 2
},
{
"id": "d152a0a9-71c9-4532-9743-15225429bcc8",
"name": "AI Agent Enhanced",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
1712,
1152
],
"parameters": {
"text": "={{ $json.userPrompt }}",
"options": {
"systemMessage": "={{ $json.systemPrompt }}"
},
"promptType": "define"
},
"typeVersion": 1.7
},
{
"id": "72e3dd39-013c-43b4-a0e1-9ce4167d37f0",
"name": "Accumulate Results",
"type": "n8n-nodes-base.code",
"position": [
2288,
1264
],
"parameters": {
"jsCode": "// Accumulate all processed items\nconst currentItem = $input.first();\nconst storedData = $getWorkflowStaticData('global');\n\n// Initialize accumulator if needed\nif (!storedData.accumulatedData) {\n storedData.accumulatedData = [];\n}\n\n// Add current item to accumulated data\nstoredData.accumulatedData.push(currentItem);\n\nconsole.log(`Item processed. Total accumulated: ${storedData.accumulatedData.length} items`);\n\n// Return the current item to continue the flow\nreturn [currentItem];"
},
"typeVersion": 2
},
{
"id": "7f62341e-e758-4664-aa1d-62cdba49cd84",
"name": "Anthropic Chat Model1",
"type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
"position": [
1472,
1312
],
"parameters": {
"model": {
"__rl": true,
"mode": "list",
"value": "claude-opus-4-20250514",
"cachedResultName": "Claude Opus 4"
},
"options": {}
},
"credentials": {
"anthropicApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.3
},
{
"id": "dfb09d43-49c8-47be-af77-e3bef30a5053",
"name": "Calculate Project Totals1",
"type": "n8n-nodes-base.code",
"position": [
1376,
1568
],
"parameters": {
"jsCode": "// Aggregate all results and calculate project totals\nconst items = $input.all();\n\n// Initialize aggregators\nconst projectTotals = {\n totalElements: 0,\n totalCost: 0,\n totalVolume: 0,\n totalArea: 0,\n byMaterial: {},\n byCategory: {},\n byImpact: {}\n};\n\n// Process each item\nitems.forEach(item => {\n const data = item.json;\n const elementCount = parseFloat(data['Element Count']) || 1;\n const cost = parseFloat(data['Total Cost (EUR)']) || 0;\n const volume = parseFloat(data['Volume (m\u00b3)']) || 0;\n const area = parseFloat(data['Area (m\u00b2)']) || 0;\n const material = data['Material (EU Standard)'] || 'Unknown';\n const category = data['Element Category'] || 'Unknown';\n const impact = data['Impact Category'] || 'Unknown';\n \n // Update totals\n projectTotals.totalElements += elementCount;\n projectTotals.totalCost += cost;\n projectTotals.totalVolume += volume;\n projectTotals.totalArea += area;\n \n // Aggregate by material\n if (!projectTotals.byMaterial[material]) {\n projectTotals.byMaterial[material] = {\n elements: 0,\n cost: 0,\n volume: 0,\n types: new Set()\n };\n }\n projectTotals.byMaterial[material].elements += elementCount;\n projectTotals.byMaterial[material].cost += cost;\n projectTotals.byMaterial[material].volume += volume;\n projectTotals.byMaterial[material].types.add(data['Element Name']);\n \n // Aggregate by category\n if (!projectTotals.byCategory[category]) {\n projectTotals.byCategory[category] = {\n elements: 0,\n cost: 0\n };\n }\n projectTotals.byCategory[category].elements += elementCount;\n projectTotals.byCategory[category].cost += cost;\n \n // Aggregate by impact\n if (!projectTotals.byImpact[impact]) {\n projectTotals.byImpact[impact] = {\n elements: 0,\n cost: 0\n };\n }\n projectTotals.byImpact[impact].elements += elementCount;\n projectTotals.byImpact[impact].cost += cost;\n});\n\n// Add percentages and rankings to each item\nconst enrichedItems = items.map((item, index) => {\n const data = item.json;\n const cost = parseFloat(data['Total Cost (EUR)']) || 0;\n const elementCount = parseFloat(data['Element Count']) || 1;\n \n return {\n json: {\n ...data,\n // Add project percentages\n 'Cost % of Total': projectTotals.totalCost > 0 ? \n ((cost / projectTotals.totalCost) * 100).toFixed(2) : '0.00',\n 'Elements % of Total': projectTotals.totalElements > 0 ? \n ((elementCount / projectTotals.totalElements) * 100).toFixed(2) : '0.00',\n // Add ranking\n 'Cost Rank': index + 1,\n // Project totals (same for all rows)\n 'Project Total Elements': projectTotals.totalElements,\n 'Project Total Cost (EUR)': projectTotals.totalCost.toFixed(3),\n 'Project Total Volume (m\u00b3)': projectTotals.totalVolume.toFixed(2),\n 'Project Total Area (m\u00b2)': projectTotals.totalArea.toFixed(2)\n }\n };\n});\n\n// Sort by Cost (highest first)\nenrichedItems.sort((a, b) => \n parseFloat(b.json['Total Cost (EUR)']) - parseFloat(a.json['Total Cost (EUR)'])\n);\n\n// Store aggregated data for summary\n$getWorkflowStaticData('global').projectTotals = projectTotals;\n\nreturn enrichedItems;"
},
"typeVersion": 2
},
{
"id": "4d061758-4051-4bcc-85db-d7ed7fa06ec7",
"name": "Enhance Excel Output",
"type": "n8n-nodes-base.code",
"position": [
1936,
1744
],
"parameters": {
"jsCode": "// Enhanced Excel styling configuration\nconst excelBuffer = $input.first().binary.data;\nconst fileName = `Price_Estimation_Report_${new Date().toISOString().slice(0,10)}.xlsx`;\n\n// Add metadata to the file\nconst metadata = {\n title: 'Price Estimation Analysis Report',\n author: 'DataDrivenConstruction.io',\n company: 'Automated Price Estimation System',\n created: new Date().toISOString(),\n description: 'Comprehensive project cost assessment with multi-standard material classification',\n keywords: 'Price, Cost Estimation, Building Materials'\n};\n\n// Return the enhanced Excel file\nreturn [{\n json: {\n fileName: fileName,\n fileSize: excelBuffer.data.length,\n sheets: 8,\n metadata: metadata,\n timestamp: new Date().toISOString()\n },\n binary: {\n data: {\n ...excelBuffer,\n fileName: fileName,\n fileExtension: 'xlsx',\n mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'\n }\n }\n}];"
},
"typeVersion": 2
},
{
"id": "d006c84f-3df6-493e-8965-2552f06cc862",
"name": "Prepare Excel Data",
"type": "n8n-nodes-base.code",
"position": [
1600,
1744
],
"parameters": {
"jsCode": "// Prepare comprehensive data for multi-sheet Excel export\nconst items = $input.all();\nconst projectTotals = $getWorkflowStaticData('global').projectTotals;\n\n// Helper function to format numbers\nconst formatNumber = (num, decimals = 2) => {\n return typeof num === 'number' ? parseFloat(num.toFixed(decimals)) : 0;\n};\n\n// Sheet 1: Executive Summary with project metrics\nconst executiveSummary = [\n {\n 'Category': 'PROJECT OVERVIEW',\n 'Metric': 'Total Elements Analyzed',\n 'Value': projectTotals.totalElements,\n 'Unit': 'elements'\n },\n {\n 'Category': 'PROJECT OVERVIEW',\n 'Metric': 'Element Groups',\n 'Value': items.length,\n 'Unit': 'groups'\n },\n {\n 'Category': 'COST METRICS',\n 'Metric': 'Total Estimated Cost',\n 'Value': formatNumber(projectTotals.totalCost, 2),\n 'Unit': 'EUR'\n },\n {\n 'Category': 'COST METRICS',\n 'Metric': 'Average Cost per Element',\n 'Value': formatNumber(projectTotals.totalCost / projectTotals.totalElements, 3),\n 'Unit': 'EUR/element'\n },\n {\n 'Category': 'MATERIAL METRICS',\n 'Metric': 'Unique Material Types',\n 'Value': Object.keys(projectTotals.byMaterial).length,\n 'Unit': 'materials'\n },\n {\n 'Category': 'VOLUMETRIC DATA',\n 'Metric': 'Total Volume',\n 'Value': formatNumber(projectTotals.totalVolume, 2),\n 'Unit': 'm\u00b3'\n },\n {\n 'Category': 'VOLUMETRIC DATA',\n 'Metric': 'Total Area',\n 'Value': formatNumber(projectTotals.totalArea, 2),\n 'Unit': 'm\u00b2'\n }\n];\n\n// Sheet 2: Detailed Elements Analysis - ALL fields from processing\nconst detailedElements = items.map((item, index) => {\n const data = item.json;\n return {\n 'Cost_Rank': index + 1,\n 'Element_Group': data['Element Name'] || data['Type Name'] || 'Unknown',\n 'Element_Category': data['Element Category'] || 'Unknown',\n 'Element_Type': data['Element Type'] || 'Unknown',\n 'Element_Function': data['Element Function'] || 'Unknown',\n 'Element_Count': parseInt(data['Element Count']) || 0,\n 'Material_EU_Standard': data['Material (EU Standard)'] || 'Unknown',\n 'Material_DE_Standard': data['Material (DE Standard)'] || 'Unknown',\n 'Material_US_Standard': data['Material (US Standard)'] || 'Unknown',\n 'Primary_Material': data['Primary Material'] || 'Unknown',\n 'Secondary_Materials': data['Secondary Materials'] || 'None',\n 'Quantity_Value': formatNumber(data['Quantity'] || 0, 3),\n 'Quantity_Unit': data['Quantity Unit'] || 'piece',\n 'Volume_m3': formatNumber(data['Volume (m\u00b3)'] || 0, 3),\n 'Area_m2': formatNumber(data['Area (m\u00b2)'] || 0, 3),\n 'Price_per_Unit_EUR': formatNumber(data['Price per Unit (EUR)'] || 0, 2),\n 'Total_Cost_EUR': formatNumber(data['Total Cost (EUR)'] || 0, 2),\n 'Cost_per_Element_EUR': formatNumber(data['Cost per Element (EUR)'] || 0, 2),\n 'Cost_Percent_of_Total': formatNumber(data['Cost % of Total'] || 0, 2),\n 'Price_Source': data['Price Source'] || 'Unknown',\n 'Overall_Confidence_%': parseInt(data['Overall Confidence (%)']) || 0,\n 'Assumptions': data['Assumptions'] || 'None',\n 'Warnings': data['Warnings'] || 'None'\n };\n});\n\n// Sheet 3: Material Summary with detailed breakdown\nconst materialSummary = Object.entries(projectTotals.byMaterial)\n .sort((a, b) => b[1].cost - a[1].cost)\n .map(([material, data], index) => {\n const costPercent = (data.cost / projectTotals.totalCost) * 100;\n return {\n 'Rank': index + 1,\n 'Material_Type': material,\n 'Element_Count': data.elements,\n 'Cost_EUR': formatNumber(data.cost, 2),\n 'Cost_%': formatNumber(costPercent, 1),\n 'Volume_m3': formatNumber(data.volume, 2)\n };\n });\n\n// Sheet 4: Top 10 Expensive Groups\nconst top10Groups = items\n .slice(0, 10)\n .map((item, index) => {\n const data = item.json;\n const cost = parseFloat(data['Total Cost (EUR)']) || 0;\n const costPercent = parseFloat(data['Cost % of Total']) || 0;\n const elementCount = parseInt(data['Element Count']) || 1;\n \n return {\n 'Rank': index + 1,\n 'Element_Group': data['Element Name'] || 'Unknown',\n 'Category': data['Element Category'] || 'Unknown',\n 'Material': data['Material (EU Standard)'] || 'Unknown',\n 'Element_Count': elementCount,\n 'Cost_EUR': formatNumber(cost, 2),\n 'Cost_%': formatNumber(costPercent, 1),\n 'Cost_per_Element': formatNumber(cost / elementCount, 1)\n };\n });\n\n// Create worksheet structure with proper sheet names\nconst worksheets = [\n { name: 'Summary', data: executiveSummary },\n { name: 'Detailed Elements', data: detailedElements },\n { name: 'Material Summary', data: materialSummary },\n { name: 'Top 10 Groups', data: top10Groups }\n];\n\n// Flatten all data with sheet markers\nconst allData = [];\nworksheets.forEach(sheet => {\n sheet.data.forEach(row => {\n allData.push({\n json: {\n ...row,\n _sheetName: sheet.name\n }\n });\n });\n});\n\nreturn allData;"
},
"typeVersion": 2
},
{
"id": "b55dc4f5-1309-4a06-9924-d796733a21f5",
"name": "Create Excel File",
"type": "n8n-nodes-base.spreadsheetFile",
"position": [
1760,
1744
],
"parameters": {
"options": {
"fileName": "=Price_Estimation_Report_{{ $now.format('yyyy-MM-dd') }}",
"headerRow": true,
"sheetName": "={{ $json._sheetName }}"
},
"operation": "toFile",
"fileFormat": "xlsx"
},
"typeVersion": 2
},
{
"id": "750047a0-76ea-41a6-8e6e-83447eaef026",
"name": "Prepare Enhanced Prompts",
"type": "n8n-nodes-base.code",
"position": [
1536,
1152
],
"parameters": {
"jsCode": "// Enhanced prompts for comprehensive price estimation analysis\nconst inputData = $input.first().json;\nconst originalGroupedData = { ...inputData };\nconst country = $node['Process AI Response1'].json.country;\n\nconst systemPrompt = `You are an expert in construction cost estimation, material pricing, and building element classification. Analyze the provided building element data and return a comprehensive price estimation assessment.\n\nUse tools like web_search or browse_page to find current prices from reliable sources (e.g., manufacturer websites, construction databases like RSMeans, \u00d6KOBAUDAT for ${country}, etc.). Prioritize sources from ${country} or relevant region.\n\nIMPORTANT NOTE ON DATA:\n- All quantitative fields like Volume, Area, Length, etc. in the input data represent ALREADY AGGREGATED TOTALS for the entire group of elements.\n- 'Element Count' indicates the number of individual elements in this group.\n- DO NOT multiply volumes/areas by Element Count - they are already total sums.\n- Infer the best unit for pricing (e.g., per m\u00b3 for volumes, per m\u00b2 for areas, per piece for counts) based on parameters.\n- Search for prices using element parameters (material, dimensions, type).\n- If exact price not found, use average or rough estimate and note it.\n- Consider secondary materials if possible, otherwise focus on primary.\n- For additional factors (e.g., labor, transport), note percentages but do not include in calculations.\n- If no price found, state 'Price not found' and suggest average if possible.\n\n## Your Analysis Must Include:\n\n### 1. Element Identification\n- Element type and category\n- Primary material composition\n- Secondary materials if applicable\n- Functional classification\n\n### 2. Material Classification (All 3 Standards, prioritize ${country}'s if applicable)\n- **European (EN 15978/15804)**: YOUR_AWS_SECRET_KEY_HERE.\n- **German (\u00d6KOBAUDAT)**: Mineralische/Metalle/Holz/D\u00e4mmstoffe/etc.\n- **US (MasterFormat)**: Division classifications\n\n### 3. Quantity Analysis\n- Primary quantity with unit (m\u00b3, m\u00b2, m, kg, pieces) - use aggregated total from input\n- Calculation method used (e.g., 'Direct from provided total volume')\n- Confidence level (0-100%)\n- Data quality assessment\n\n### 4. Price Estimation\n- Price per unit (EUR/unit)\n- Total price (for the entire group)\n- Price source (URL or database name)\n- Additional factors (percentages only, e.g., labor: 20%)\n\n### 5. Data Quality & Confidence\n- Overall confidence score\n- Data completeness assessment\n- Key assumptions made\n- Warnings or limitations\n\n## Output Format\nReturn ONLY valid JSON with this exact structure:\n{\n \"element_identification\": {\n \"name\": \"string\",\n \"category\": \"string\",\n \"type\": \"string\",\n \"function\": \"string\"\n },\n \"material_classification\": {\n \"european\": \"string\",\n \"german\": \"string\",\n \"us\": \"string\",\n \"primary_material\": \"string\",\n \"secondary_materials\": [\"string\"]\n },\n \"quantities\": {\n \"value\": number,\n \"unit\": \"string\",\n \"calculation_method\": \"string\",\n \"raw_dimensions\": {\n \"length\": number,\n \"width\": number,\n \"height\": number,\n \"thickness\": number,\n \"area\": number,\n \"volume\": number\n }\n },\n \"price_analysis\": {\n \"price_per_unit_eur\": number,\n \"total_price_eur\": number,\n \"price_source\": \"string\",\n \"additional_factors\": {\"labor\": number, \"transport\": number},\n \"data_source\": \"string\"\n },\n \"confidence\": {\n \"overall_score\": number,\n \"material_confidence\": number,\n \"quantity_confidence\": number,\n \"price_confidence\": number,\n \"data_quality\": \"high/medium/low\"\n },\n \"metadata\": {\n \"assumptions\": [\"string\"],\n \"warnings\": [\"string\"],\n \"notes\": \"string\"\n }\n}`;\n\nconst userPrompt = `Analyze this building element group for price estimation in ${country}. Remember: Volumes and areas are already total for the group, not per element. Use tools to search for prices.\n\n${JSON.stringify(inputData, null, 2)}\n\nProvide comprehensive price analysis following the specified format. Focus on accuracy and use conservative estimates where data is uncertain.`;\n\nreturn [{\n json: {\n ...originalGroupedData,\n systemPrompt,\n userPrompt,\n _originalGroupedData: originalGroupedData\n }\n}];"
},
"typeVersion": 2
},
{
"id": "f5d3ae3d-68fe-43e3-a6df-4bed5a82aefa",
"name": "Parse Enhanced Response",
"type": "n8n-nodes-base.code",
"position": [
2064,
1152
],
"parameters": {
"jsCode": "// Parse and enrich AI response with all necessary data\nconst aiResponse = $input.first().json.output || $input.first().json.response || $input.first().json.text || $input.first().json;\nconst originalData = $node[\"Prepare Enhanced Prompts\"].json._originalGroupedData || $node[\"Prepare Enhanced Prompts\"].json;\n\ntry {\n // Extract JSON from response\n let jsonStr = aiResponse;\n if (typeof jsonStr === 'string') {\n const jsonMatch = jsonStr.match(/```json\\n?([\\s\\S]*?)\\n?```/) || jsonStr.match(/\\{[\\s\\S]*\\}/);\n if (jsonMatch) {\n jsonStr = jsonMatch[1] || jsonMatch[0];\n }\n }\n \n const analysis = typeof jsonStr === 'string' ? JSON.parse(jsonStr) : jsonStr;\n \n // Calculate additional metrics - use total price for group\n const total_price_eur = analysis.price_analysis?.total_price_eur || 0;\n const elementCount = parseFloat(originalData['Element Count']) || 1;\n const price_per_element = total_price_eur / elementCount;\n \n // Determine impact category\n let impactCategory = 'Unknown';\n const pricePerUnit = analysis.price_analysis?.price_per_unit_eur || 0;\n if (pricePerUnit <= 10) {\n impactCategory = 'Very Low Cost';\n } else if (pricePerUnit <= 50) {\n impactCategory = 'Low Cost';\n } else if (pricePerUnit <= 200) {\n impactCategory = 'Medium Cost';\n } else if (pricePerUnit <= 500) {\n impactCategory = 'High Cost';\n } else {\n impactCategory = 'Very High Cost';\n }\n \n // Create comprehensive output record\n return [{\n json: {\n // Original data\n ...originalData,\n \n // Element identification\n 'Element Name': analysis.element_identification?.name || originalData['Type Name'] || 'Unknown',\n 'Element Category': analysis.element_identification?.category || originalData['Category'] || 'Unknown',\n 'Element Type': analysis.element_identification?.type || 'Unknown',\n 'Element Function': analysis.element_identification?.function || 'Unknown',\n \n // Material classification\n 'Material (EU Standard)': analysis.material_classification?.european || 'Unknown',\n 'Material (DE Standard)': analysis.material_classification?.german || 'Unknown',\n 'Material (US Standard)': analysis.material_classification?.us || 'Unknown',\n 'Primary Material': analysis.material_classification?.primary_material || 'Unknown',\n 'Secondary Materials': (analysis.material_classification?.secondary_materials || []).join(', ') || 'None',\n \n // Quantities\n 'Quantity': analysis.quantities?.value || 0,\n 'Quantity Unit': analysis.quantities?.unit || 'piece',\n 'Calculation Method': analysis.quantities?.calculation_method || 'Not specified',\n \n // Dimensions (from raw data) - these are totals\n 'Length (mm)': analysis.quantities?.raw_dimensions?.length || originalData['Length'] || 0,\n 'Width (mm)': analysis.quantities?.raw_dimensions?.width || originalData['Width'] || 0,\n 'Height (mm)': analysis.quantities?.raw_dimensions?.height || originalData['Height'] || 0,\n 'Thickness (mm)': analysis.quantities?.raw_dimensions?.thickness || originalData['Thickness'] || 0,\n 'Area (m\u00b2)': analysis.quantities?.raw_dimensions?.area || originalData['Area'] || 0,\n 'Volume (m\u00b3)': analysis.quantities?.raw_dimensions?.volume || originalData['Volume'] || 0,\n \n // Price Analysis\n 'Price per Unit (EUR)': analysis.price_analysis?.price_per_unit_eur || 0,\n 'Total Cost (EUR)': total_price_eur,\n 'Cost per Element (EUR)': price_per_element,\n 'Price Source': analysis.price_analysis?.price_source || 'Unknown',\n 'Additional Factors': JSON.stringify(analysis.price_analysis?.additional_factors || {}),\n 'Data Source': analysis.price_analysis?.data_source || 'Industry average',\n 'Impact Category': impactCategory,\n \n // Confidence scores\n 'Overall Confidence (%)': analysis.confidence?.overall_score || 0,\n 'Material Confidence (%)': analysis.confidence?.material_confidence || 0,\n 'Quantity Confidence (%)': analysis.confidence?.quantity_confidence || 0,\n 'Price Confidence (%)': analysis.confidence?.price_confidence || 0,\n 'Data Quality': analysis.confidence?.data_quality || 'unknown',\n \n // Metadata\n 'Assumptions': (analysis.metadata?.assumptions || []).join('; ') || 'None',\n 'Warnings': (analysis.metadata?.warnings || []).join('; ') || 'None',\n 'Analysis Notes': analysis.metadata?.notes || '',\n 'Processing Timestamp': new Date().toISOString(),\n 'Analysis Status': 'Complete'\n }\n }];\n \n} catch (error) {\n // Return error record with original data preserved\n return [{\n json: {\n ...originalData,\n 'Analysis Status': 'Failed',\n 'Error': error.message,\n 'Processing Timestamp': new Date().toISOString()\n }\n }];\n}"
},
"typeVersion": 2
},
{
"id": "428bb655-a1e9-45df-a80c-57c00810e5e9",
"name": "Generate HTML Report",
"type": "n8n-nodes-base.code",
"position": [
1600,
1568
],
"parameters": {
"jsCode": "// Generate professional McKinsey/Accenture style HTML report with visualizations\nconst items = $input.all();\nconst projectTotals = $getWorkflowStaticData('global').projectTotals;\n\n// Get project name from file path\nconst projectFile = $node['Setup - Define file paths'].json.project_file || '';\nconst projectName = projectFile.split('\\\\').pop().replace('.rvt', '').replace(/_/g, ' ').toUpperCase();\n\n// Sort items by total cost descending\nconst sortedItems = [...items].sort((a, b) => \n parseFloat(b.json['Total Cost (EUR)'] || 0) - parseFloat(a.json['Total Cost (EUR)'] || 0)\n);\n\n// Calculate additional metrics\nconst avgConfidence = sortedItems.reduce((sum, item) => sum + (parseInt(item.json['Overall Confidence (%)']) || 0), 0) / sortedItems.length;\nconst completeItems = sortedItems.filter(item => item.json['Analysis Status'] === 'Complete').length;\nconst coveragePercent = (completeItems / sortedItems.length) * 100;\n\n// Get top material\nconst topMaterial = Object.entries(projectTotals.byMaterial)\n .sort((a, b) => b[1].cost - a[1].cost)[0];\n\n// Calculate high impact items\nconst highImpactItems = sortedItems.filter(item => \n parseFloat(item.json['Total Cost (EUR)']) >= projectTotals.totalCost * 0.05\n).length;\n\n// Calculate top 3 materials percentage\nconst top3Materials = Object.entries(projectTotals.byMaterial)\n .sort((a, b) => b[1].cost - a[1].cost)\n .slice(0, 3);\nconst top3Percentage = (top3Materials.reduce((sum, [, data]) => sum + data.cost, 0) / projectTotals.totalCost) * 100;\n\n// Prepare data for charts\nconst materialChartData = Object.entries(projectTotals.byMaterial)\n .sort((a, b) => b[1].cost - a[1].cost)\n .slice(0, 6)\n .map(([material, data]) => ({\n label: material,\n value: data.cost\n }));\n\nconst barChartData = sortedItems.slice(0, 10).map(item => ({\n label: item.json['Element Name'] || 'Unknown',\n value: parseFloat(item.json['Total Cost (EUR)'] || 0)\n}));\n\n// Generate HTML\nconst html = `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Project Cost Intelligence | Executive Report</title>\n <script src=\"https://cdn.jsdelivr.net/npm/chart.js\"></script>\n <link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Poppins:wght@600;700;800&display=swap\" rel=\"stylesheet\">\n <style>\n * {\n margin: 0;\n padding: 0;\n box-sizing: border-box;\n }\n \n :root {\n --primary-gradient: linear-gradient(135deg, #0F4C75 0%, #3282B8 50%, #1B262C 100%);\n --secondary-gradient: linear-gradient(135deg, #3282B8 0%, #0F4C75 100%);\n --light-gradient: linear-gradient(180deg, #f8fafc 0%, #f1f5f9 100%);\n --accent-gradient: linear-gradient(135deg, #BBE1FA 0%, #3282B8 100%);\n }\n \n body {\n font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;\n background: linear-gradient(180deg, #fafbfc 0%, #ffffff 100%);\n min-height: 100vh;\n color: #0f172a;\n line-height: 1.6;\n }\n \n .container {\n max-width: 1600px;\n margin: 0 auto;\n background: white;\n box-shadow: 0 0 40px rgba(0, 0, 0, 0.05);\n position: relative;\n }\n \n /* Professional Header */\n .header {\n background: var(--primary-gradient);\n color: white;\n padding: 45px 80px;\n position: relative;\n overflow: hidden;\n }\n \n .header::before {\n content: '';\n position: absolute;\n top: -50%;\n right: -10%;\n width: 800px;\n height: 800px;\n background: radial-gradient(circle, rgba(50, 130, 184, 0.15) 0%, transparent 70%);\n animation: float 6s ease-in-out infinite;\n }\n \n .header::after {\n content: '';\n position: absolute;\n bottom: -30%;\n left: -10%;\n width: 600px;\n height: 600px;\n background: radial-gradient(circle, rgba(187, 225, 250, 0.1) 0%, transparent 70%);\n animation: float 6s ease-in-out infinite 3s;\n }\n \n @keyframes float {\n 0%, 100% { transform: translateY(0) scale(1); }\n 50% { transform: translateY(-20px) scale(1.05); }\n }\n \n .header-content {\n position: relative;\n z-index: 1;\n }\n \n .header-top {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-bottom: 50px;\n }\n \n .company-brand {\n display: flex;\n align-items: center;\n gap: 20px;\n }\n \n .company-logo {\n font-size: 13px;\n font-weight: 800;\n letter-spacing: 3px;\n text-transform: uppercase;\n padding-left: 20px;\n border-left: 4px solid #BBE1FA;\n opacity: 0.95;\n }\n \n .report-badge {\n background: linear-gradient(135deg, rgba(255,255,255,0.2) 0%, rgba(255,255,255,0.05) 100%);\n backdrop-filter: blur(20px);\n padding: 12px 24px;\n border-radius: 50px;\n font-size: 11px;\n font-weight: 700;\n letter-spacing: 2px;\n text-transform: uppercase;\n border: 1px solid rgba(255,255,255,0.2);\n box-shadow: 0 4px 20px rgba(0,0,0,0.1);\n }\n \n h1 {\n font-family: 'Poppins', sans-serif;\n font-size: 48px;\n font-weight: 800;\n margin-bottom: 16px;\n letter-spacing: -2px;\n line-height: 1.1;\n color: white;\n }\n \n .subtitle {\n font-size: 18px;\n font-weight: 300;\n opacity: 0.9;\n margin-bottom: 30px;\n color: #cbd5e1;\n letter-spacing: 0.5px;\n }\n \n .meta-grid {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));\n gap: 30px;\n padding-top: 30px;\n border-top: 1px solid rgba(255,255,255,0.15);\n }\n \n .meta-card {\n display: flex;\n align-items: flex-start;\n gap: 16px;\n flex-direction: column;\n }\n \n .meta-info {\n flex: 1;\n }\n \n .meta-label {\n font-size: 12px;\n opacity: 0.8;\n text-transform: uppercase;\n letter-spacing: 1px;\n margin-bottom: 4px;\n font-weight: 500;\n }\n \n .meta-value {\n font-size: 20px;\n font-weight: 700;\n }\n \n /* KPI Cards Section */\n .kpi-section {\n padding: 80px;\n background: linear-gradient(180deg, #f8fafc 0%, #ffffff 100%);\n position: relative;\n }\n \n .kpi-section::before {\n content: '';\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n height: 2px;\n background: var(--accent-gradient);\n }\n \n .kpi-grid {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));\n gap: 32px;\n }\n \n .kpi-card {\n background: white;\n padding: 40px;\n border-radius: 20px;\n border: 1px solid #e5e7eb;\n box-shadow: 0 4px 20px rgba(0, 0, 0, 0.04);\n transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);\n position: relative;\n overflow: hidden;\n }\n \n .kpi-card::before {\n content: '';\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 3px;\n background: linear-gradient(90deg, #3282B8 0%, #0F4C75 100%);\n }\n \n .kpi-card:hover {\n transform: translateY(-8px);\n box-shadow: 0 12px 40px rgba(0, 0, 0, 0.08);\n border-color: #cbd5e1;\n }\n \n .kpi-icon {\n width: 56px;\n height: 56px;\n background: linear-gradient(180deg, #f8fafc 0%, #f1f5f9 100%);\n border-radius: 16px;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 24px;\n margin-bottom: 20px;\n border: 1px solid #e5e7eb;\n color: #94a3b8;\n }\n \n .kpi-label {\n font-size: 11px;\n color: #64748b;\n text-transform: uppercase;\n letter-spacing: 1.5px;\n margin-bottom: 12px;\n font-weight: 700;\n }\n \n .kpi-value {\n font-size: 42px;\n font-weight: 800;\n color: #1e293b;\n margin-bottom: 8px;\n font-family: 'Poppins', sans-serif;\n }\n \n .kpi-unit {\n font-size: 14px;\n color: #94a3b8;\n font-weight: 500;\n }\n \n .kpi-trend {\n position: absolute;\n top: 24px;\n right: 24px;\n padding: 8px 16px;\n background: linear-gradient(135deg, #dbeafe 0%, #e0f2fe 100%);\n border-radius: 20px;\n font-size: 12px;\n font-weight: 600;\n color: #3282B8;\n }\n \n /* Executive Summary */\n .executive-section {\n padding: 80px;\n background: white;\n }\n \n .section-header {\n display: flex;\n align-items: center;\n gap: 20px;\n margin-bottom: 50px;\n padding-bottom: 30px;\n border-bottom: 2px solid #e0f2fe;\n }\n \n .section-number {\n width: 60px;\n height: 60px;\n background: linear-gradient(135deg, #3282B8 0%, #0F4C75 100%);\n color: white;\n border-radius: 16px;\n display: flex;\n align-items: center;\n justify-content: center;\n font-weight: 800;\n font-size: 24px;\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);\n }\n \n h2 {\n font-size: 36px;\n font-weight: 800;\n color: #1e293b;\n font-family: 'Poppins', sans-serif;\n letter-spacing: -1px;\n }\n \n .summary-grid {\n display: grid;\n grid-template-columns: 1.5fr 1fr;\n gap: 60px;\n margin-top: 50px;\n }\n \n .summary-content {\n font-size: 17px;\n line-height: 1.9;\n color: #475569;\n }\n \n .summary-content p {\n margin-bottom: 20px;\n }\n \n .summary-content strong {\n color: #3282B8;\n font-weight: 700;\n }\n \n .insight-panel {\n background: linear-gradient(180deg, #f8fafc 0%, #f1f5f9 100%);\n padding: 40px;\n border-radius: 20px;\n border: 1px solid #e5e7eb;\n position: relative;\n box-shadow: 0 4px 20px rgba(0, 0, 0, 0.04);\n }\n \n .insight-panel h3 {\n font-size: 20px;\n margin-bottom: 30px;\n color: #1e293b;\n font-weight: 800;\n font-family: 'Poppins', sans-serif;\n }\n \n .insight-item {\n display: flex;\n align-items: center;\n gap: 20px;\n margin-bottom: 24px;\n padding: 16px 20px;\n background: white;\n border-radius: 12px;\n transition: all 0.3s;\n border: 1px solid transparent;\n }\n \n .insight-item:hover {\n transform: translateX(8px);\n border-color: #cbd5e1;\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);\n }\n \n .insight-icon {\n width: 40px;\n height: 40px;\n background: linear-gradient(135deg, #3282B8 0%, #0F4C75 100%);\n border-radius: 10px;\n display: flex;\n align-items: center;\n justify-content: center;\n color: white;\n font-size: 18px;\n font-weight: bold;\n flex-shrink: 0;\n }\n \n .insight-icon::before {\n content: '\u29bf';\n opacity: 0.7;\n }\n \n .insight-text {\n flex: 1;\n font-size: 15px;\n color: #334155;\n font-weight: 500;\n }\n \n /* Charts Section */\n .charts-section {\n padding: 80px;\n background: linear-gradient(180deg, #f8fafc 0%, #ffffff 100%);\n }\n \n .charts-grid {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));\n gap: 50px;\n margin-top: 50px;\n }\n \n .chart-container {\n background: white;\n padding: 50px;\n border-radius: 20px;\n border: 1px solid #e5e7eb;\n box-shadow: 0 4px 20px rgba(0, 0, 0, 0.04);\n transition: all 0.3s;\n }\n \n .chart-container:hover {\n box-shadow: 0 8px 30px rgba(0, 0, 0, 0.08);\n }\n \n .chart-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-bottom: 40px;\n padding-bottom: 20px;\n border-bottom: 2px solid #e0f2fe;\n }\n \n .chart-title {\n font-size: 22px;\n font-weight: 700;\n color: #1e293b;\n }\n \n .chart-badge {\n padding: 6px 12px;\n background: linear-gradient(180deg, #f8fafc 0%, #f1f5f9 100%);\n border-radius: 20px;\n font-size: 11px;\n font-weight: 600;\n color: #64748b;\n text-transform: uppercase;\n letter-spacing: 1px;\n }\n \n /* Data Tables */\n .data-section {\n padding: 80px;\n background: white;\n }\n \n .table-container {\n margin-top: 50px;\n border-radius: 16px;\n overflow: hidden;\n box-shadow: 0 0 0 1px #e0f2fe;\n }\n \n .data-table {\n width: 100%;\n border-collapse: separate;\n border-spacing: 0;\n background: white;\n }\n \n .data-table thead {\n background: var(--primary-gradient);\n }\n \n .data-table th {\n padding: 20px;\n text-align: left;\n font-size: 11px;\n font-weight: 800;\n color: white;\n text-transform: uppercase;\n letter-spacing: 1.5px;\n }\n \n .data-table td {\n padding: 20px;\n font-size: 14px;\n color: #1e293b;\n border-bottom: 1px solid #f1f5f9;\n transition: all 0.2s;\n }\n \n .data-table tbody tr {\n transition: all 0.3s;\n }\n \n .data-table tbody tr:hover {\n background: linear-gradient(90deg, #f0f9ff 0%, #e0f2fe 100%);\n }\n \n .rank-badge {\n display: inline-flex;\n width: 36px;\n height: 36px;\n background: linear-gradient(135deg, #3282B8 0%, #0F4C75 100%);\n color: white;\n border-radius: 10px;\n align-items: center;\n justify-content: center;\n font-size: 14px;\n font-weight: 800;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);\n }\n \n .cost-bar-container {\n display: flex;\n align-items: center;\n gap: 12px;\n }\n \n .cost-bar {\n height: 10px;\n background: var(--accent-gradient);\n border-radius: 5px;\n box-shadow: 0 2px 8px rgba(50, 130, 184, 0.2);\n transition: all 0.3s;\n }\n \n .data-table tbody tr:hover .cost-bar {\n transform: scaleY(1.4);\n }\n \n .impact-badge {\n padding: 6px 12px;\n background: linear-gradient(135deg, #dbeafe 0%, #bae6fd 100%);\n border-radius: 20px;\n font-size: 12px;\n font-weight: 700;\n color: #0F4C75;\n }\n \n /* Methodology Section */\n .methodology-section {\n padding: 80px;\n background: linear-gradient(180deg, #f8fafc 0%, #f1f5f9 100%);\n }\n \n .methodology-grid {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));\n gap: 40px;\n margin-top: 50px;\n }\n \n .step-card {\n text-align: center;\n padding: 40px 30px;\n background: white;\n border-radius: 20px;\n border: 1px solid #e5e7eb;\n transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);\n position: relative;\n overflow: hidden;\n }\n \n .step-card::before {\n content: '';\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n height: 3px;\n background: linear-gradient(90deg, #1B262C 0%, #3282B8 100%);\n transform: scaleX(0);\n transition: transform 0.4s;\n }\n \n .step-card:hover {\n transform: translateY(-12px);\n box-shadow: 0 12px 30px rgba(0, 0, 0, 0.1);\n }\n \n .step-card:hover::before {\n transform: scaleX(1);\n }\n \n .step-number {\n width: 80px;\n height: 80px;\n background: linear-gradient(135deg, #ffffff 0%, #f0f9ff 100%);\n border: 4px solid;\n border-image: var(--secondary-gradient) 1;\n border-radius: 24px;\n display: flex;\n align-items: center;\n justify-content: center;\n margin: 0 auto 24px;\n font-size: 32px;\n font-weight: 800;\n color: #3282B8;\n font-family: 'Poppins', sans-serif;\n }\n \n .step-title {\n font-size: 18px;\n font-weight: 700;\n color: #0c4a6e;\n margin-bottom: 12px;\n font-family: 'Poppins', sans-serif;\n }\n \n .step-description {\n font-size: 14px;\n color: #64748b;\n line-height: 1.6;\n }\n \n /* Footer */\n .footer {\n background: linear-gradient(135deg, #0F4C75 0%, #3282B8 50%, #1B262C 100%);\n color: white;\n padding: 60px 80px;\n position: relative;\n overflow: hidden;\n }\n \n .footer::before {\n content: '';\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n height: 2px;\n background: linear-gradient(90deg, transparent 0%, #BBE1FA 50%, transparent 100%);\n }\n \n .footer-grid {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));\n gap: 60px;\n margin-bottom: 50px;\n }\n \n .footer-section h3 {\n font-size: 20px;\n font-weight: 700;\n margin-bottom: 24px;\n color: #bae6fd;\n font-family: 'Poppins', sans-serif;\n }\n \n .footer-text {\n color: #cbd5e1;\n font-size: 14px;\n line-height: 1.8;\n margin-bottom: 20px;\n }\n \n .footer-links {\n display: flex;\n flex-direction: column;\n gap: 12px;\n }\n \n .footer-link {\n color: #cbd5e1;\n text-decoration: none;\n font-size: 14px;\n transition: all 0.3s;\n display: inline-flex;\n align-items: center;\n gap: 8px;\n }\n \n .footer-link:hover {\n color: #BBE1FA;\n transform: translateX(4px);\n }\n \n .footer-link::before {\n content: '\u2192';\n color: #BBE1FA;\n }\n \n .footer-bottom {\n padding-top: 40px;\n border-top: 1px solid rgba(255,255,255,0.1);\n text-align: center;\n font-size: 13px;\n color: #94a3b8;\n }\n \n /* Responsive Design */\n @media (max-width: 768px) {\n .header {\n padding: 60px 40px;\n }\n \n h1 {\n font-size: 48px;\n }\n \n .kpi-section,\n .executive-section,\n .charts-section,\n .data-section,\n .methodology-section {\n padding: 50px 30px;\n }\n \n .summary-grid {\n grid-template-columns: 1fr;\n }\n \n .charts-grid {\n grid-template-columns: 1fr;\n }\n \n .data-table {\n font-size: 12px;\n }\n \n .data-table th,\n .data-table td {\n padding: 12px;\n }\n }\n \n /* Print Styles */\n @media print {\n body {\n background: white;\n }\n \n .container {\n box-shadow: none;\n }\n \n .header {\n background: #3282B8;\n print-color-adjust: exact;\n -webkit-print-color-adjust: exact;\n }\n }\n </style>\n</head>\n<body>\n <div class=\"container\">\n <!-- Professional Header -->\n <div class=\"header\">\n <div class=\"header-content\">\n <div class=\"header-top\">\n <div class=\"company-brand\">\n <div class=\"company-logo\">${projectName}</div>\n </div>\n <div class=\"report-badge\">Cost Analysis</div>\n </div>\n \n <h1>Project Cost Intelligence</h1>\n <div class=\"subtitle\">Building Element Cost Analysis & Strategic Insights</div>\n \n <div class=\"meta-grid\">\n <div class=\"meta-card\">\n <div class=\"meta-info\">\n <div class=\"meta-label\">Report Date</div>\n <div class=\"meta-value\">${new Date().toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}</div>\n </div>\n </div>\n <div class=\"meta-card\">\n <div class=\"meta-info\">\n <div class=\"meta-label\">Total Elements</div>\n <div class=\"meta-value\">${projectTotals.totalElements.toLocaleString()}</div>\n </div>\n </div>\n <div class=\"meta-card\">\n <div class=\"meta-info\">\n <div class=\"meta-label\">Element Groups</div>\n <div class=\"meta-value\">${items.length}</div>\n </div>\n </div>\n <div class=\"meta-card\">\n <div class=\"meta-info\">\n <div cl
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.
anthropicApiopenAiApixAiApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Estimate material price and total cost for grouped BIM/CAD elements using an LLM-driven analysis. The workflow accepts an existing XLSX (from your model) or, if missing, can trigger a local RvtExporter.exe to generate one. It enriches each element group with quantities, pricing,…
Source: https://n8n.io/workflows/7652/ — 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.
Estimate embodied carbon (CO2e) for grouped BIM/CAD elements. The workflow accepts an existing XLSX (grouped element data) or, if missing, can trigger a local RvtExporter.exe to generate one. It detec
This workflow is for beauty salons who want consistent, high‑quality social media content without writing every post manually. It also suits agencies and automation builders who manage multiple beauty
This workflow is designed for marketers, content creators, agencies, and solo founders who want to publish long‑form posts with visuals on autopilot using n8n and AI agents.
Who Is This For?
Episode 15: Startup ideas for YC RFS. Uses lmChatOpenAi, googleSheets, agent, informationExtractor. Event-driven trigger; 33 nodes.