n8n_7_Revit와 IFC 탄소 배출 CO2 추산기
고급
이것은AI Summarization, Multimodal AI분야의자동화 워크플로우로, 55개의 노드를 포함합니다.주로 If, Set, Code, Merge, ManualTrigger 등의 노드를 사용하며. AI 분류 계산을 통해 Revit/IFC 모델의 숨겨진 탄소(CO2)을 계산합니다.
사전 요구사항
- •OpenAI API Key
- •Anthropic API Key
사용된 노드 (55)
워크플로우 미리보기
노드 연결 관계를 시각적으로 표시하며, 확대/축소 및 이동을 지원합니다
워크플로우 내보내기
다음 JSON 구성을 복사하여 n8n에 가져오면 이 워크플로우를 사용할 수 있습니다
{
"id": "UgJXCTdg8rea9Skt",
"meta": {
"instanceId": "faa70e11b7175129a74fd834d3451fdc1862589b16d68ded03f91ca7b1ecca12"
},
"name": "n8n_7_Carbon_Footprint_CO2_Estimator_for_Revit and_IFC",
"tags": [],
"nodes": [
{
"id": "86512d4b-52b7-46ac-ac59-61ed748b3045",
"name": "Find Category Fields1",
"type": "n8n-nodes-base.code",
"position": [
-80,
720
],
"parameters": {
"jsCode": "const items = $input.all();\nif (items.length === 0) {\n return [{json: {error: 'No grouped data found'}}];\n}\n\nconst headers = Object.keys(items[0].json);\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\nconst volumetricPatterns = /volume|area|length|count|quantity|thickness|perimeter|depth|size|dimension|weight|mass/i;\nconst volumetricFields = headers.filter(header => volumetricPatterns.test(header));\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\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": "ceeaa6f3-781e-421f-bae1-c6ccb784ecbd",
"name": "Apply Classification to Groups1",
"type": "n8n-nodes-base.code",
"position": [
448,
768
],
"parameters": {
"jsCode": "const categoryInfo = $node['Find Category Fields1'].json;\nconst groupedData = categoryInfo.groupedData;\nconst categoryField = categoryInfo.categoryField;\nconst volumetricFields = categoryInfo.volumetricFields || [];\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\nreturn groupedData.map(group => {\n let isBuildingElement = false;\n let reason = '';\n let confidence = 0;\n \n if (categoryField && group[categoryField]) {\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": "ffc937c8-4ca6-4963-a24f-114f45b5345b",
"name": "Non-Building Elements Output1",
"type": "n8n-nodes-base.set",
"position": [
848,
784
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "message",
"name": "message",
"type": "string",
"value": "Non-building elements (drawings, annotations, etc.)"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "731a2f76-60b4-4b4c-8e56-465c0ea6ad5c",
"name": "Extract Headers and Data1",
"type": "n8n-nodes-base.code",
"position": [
80,
496
],
"parameters": {
"jsCode": " const items = $input.all();\n if (items.length === 0) {\n throw new Error('No data found in Excel file');\n }\n\n const allHeaders = new Set();\n items.forEach(item => {\n Object.keys(item.json).forEach(key => allHeaders.add(key));\n });\n\n const headers = Array.from(allHeaders);\n const cleanedHeaders = headers.map(header => {\n return header.replace(/:\\s*(string|double|int|float|boolean|number)\\s*$/i, '').trim();\n });\n\n const headerMapping = {};\n headers.forEach((oldHeader, index) => {\n headerMapping[oldHeader] = cleanedHeaders[index];\n });\n\n const sampleValues = {};\n cleanedHeaders.forEach((header, index) => {\n const originalHeader = headers[index];\n for (const item of items) {\n const value = item.json[originalHeader];\n if (value !== null && value !== undefined && value !== '') {\n sampleValues[header] = value;\n break;\n }\n }\n if (!sampleValues[header]) {\n sampleValues[header] = null;\n }\n });\n\n console.log(`Found ${headers.length} unique headers across ${items.length} items`);\n\n return [{\n json: {\n headers: cleanedHeaders,\n originalHeaders: headers,\n headerMapping: headerMapping,\n sampleValues: sampleValues,\n totalRows: items.length,\n totalHeaders: headers.length,\n rawData: items.map(item => item.json)\n }\n }];"
},
"typeVersion": 2
},
{
"id": "7bbf3d9d-4406-4fa3-9ca6-49eeaea551ff",
"name": "AI Analyze All Headers1",
"type": "@n8n/n8n-nodes-langchain.openAi",
"position": [
240,
496
],
"parameters": {
"modelId": {
"__rl": true,
"mode": "list",
"value": "chatgpt-4o-latest",
"cachedResultName": "CHATGPT-4O-LATEST"
},
"options": {
"temperature": 0.1
},
"messages": {
"values": [
{
"role": "system",
"content": "You are an expert in construction classification systems. Analyze building element groups and assign aggregation methods for grouping data.\n\nRules:\n1. 'sum' - for quantities that should be totaled:\n - Volume, Area, Length, Width, Height, Depth, Size\n - Count, Quantity, Number, Amount, Total\n - Thickness, Perimeter, Dimension\n - Weight, Mass, Load\n - Any measurable physical property that accumulates\n\n2. 'mean' (average) - for rates and unit values:\n - Price, Cost, Rate (per unit)\n - Coefficient, Factor, Ratio\n - Percentage, Percent\n - Efficiency, Performance metrics\n - Any per-unit or normalized values\n\n3. 'first' - for descriptive/categorical data:\n - ID, Code, Number (when used as identifier)\n - Name, Title, Description\n - Type, Category, Class, Group\n - Material, Component, Element\n - Project, Building, Location\n - Status, Phase, Stage\n - Any text or categorical field\n\nIMPORTANT: \n- Analyze each header carefully\n- Consider both the header name AND sample value\n- Return aggregation rule for EVERY header provided\n- Use exact header names from input\n\nReturn ONLY valid JSON in this exact format:\n{\n \"aggregation_rules\": {\n \"Header1\": \"sum\",\n \"Header2\": \"first\",\n \"Header3\": \"mean\"\n }\n}"
},
{
"content": "Analyze these {{ $json.totalHeaders }} headers and determine aggregation method for each:\n\nHeaders with sample values:\n{{ JSON.stringify($json.sampleValues, null, 2) }}\n\nProvide aggregation rule for EACH header listed above."
}
]
}
},
"credentials": {
"openAiApi": {
"id": "5SwKOx6OOukR6C0w",
"name": "OpenAi account n8n"
}
},
"typeVersion": 1.3
},
{
"id": "10caf6f7-ad6b-4de2-9e63-8b06b5609414",
"name": "Process AI Response1",
"type": "n8n-nodes-base.code",
"position": [
592,
496
],
"parameters": {
"jsCode": "// Обрабатываем ответ AI и применяем правила\n const aiResponse = $input.first().json;\n const headerData = $node['Extract Headers and Data1'].json;\n\n// Извлекаем правила из AI ответа\n let aiRules = {};\n try {\n const content = aiResponse.content || aiResponse.message || aiResponse.response || '';\n console.log('AI Response received, length:', content.length);\n \n // Ищем JSON в ответе\n const jsonMatch = content.match(/\\{[\\s\\S]*\\}/);\n if (jsonMatch) {\n const parsed = JSON.parse(jsonMatch[0]);\n aiRules = parsed.aggregation_rules || parsed.parameter_aggregation || {};\n console.log(`AI provided ${Object.keys(aiRules).length} rules`);\n } else {\n console.warn('No JSON found in AI response');\n }\n } catch (error) {\n console.error('Error parsing AI response:', error.message);\n }\n\n// Создаем финальные правила с дефолтами\n const finalRules = {};\n headerData.headers.forEach(header => {\n if (aiRules[header]) {\n finalRules[header] = aiRules[header];\n } else {\n // Применяем правила по умолчанию\n const lowerHeader = header.toLowerCase();\n \n if (lowerHeader.match(/volume|area|length|width|height|count|quantity|thickness|perimeter|depth|size|dimension|weight|mass|total|amount|number/)) {\n finalRules[header] = 'sum';\n } else if (lowerHeader.match(/price|rate|cost|coefficient|factor|percent|ratio|efficiency|avg|average|mean/)) {\n finalRules[header] = 'mean';\n } else {\n finalRules[header] = 'first';\n }\n }\n });\n\n const groupByParam = $node['Setup - Define file paths'].json.group_by;\n\n console.log(`\\nAggregation rules summary:`);\n console.log(`- Total headers: ${headerData.headers.length}`);\n console.log(`- AI rules: ${Object.keys(aiRules).length}`);\n console.log(`- Default rules: ${headerData.headers.length - Object.keys(aiRules).length}`);\n console.log(`- Group by: ${groupByParam}`);\n\n// Возвращаем все данные для группировки\n return [{\n json: {\n aggregationRules: finalRules,\n headerMapping: headerData.headerMapping,\n headers: headerData.headers,\n originalHeaders: headerData.originalHeaders,\n rawData: headerData.rawData,\n groupByParam: groupByParam,\n totalRows: headerData.totalRows\n }\n }];"
},
"typeVersion": 2
},
{
"id": "435f2cb9-9ccb-4f32-ba95-6bb0126a73d2",
"name": "Group Data with AI Rules1",
"type": "n8n-nodes-base.code",
"position": [
784,
496
],
"parameters": {
"jsCode": " const input = $input.first().json;\n const aggregationRules = input.aggregationRules;\n const headerMapping = input.headerMapping;\n const rawData = input.rawData;\n const groupByParamOriginal = input.groupByParam;\n\n const groupByParam = headerMapping[groupByParamOriginal] || groupByParamOriginal;\n\n console.log(`Grouping ${rawData.length} items by: ${groupByParam}`);\n\n const cleanedData = rawData.map(item => {\n const cleaned = {};\n Object.entries(item).forEach(([key, value]) => {\n const newKey = headerMapping[key] || key;\n cleaned[newKey] = value;\n });\n return cleaned;\n });\n\n const grouped = {};\n\n cleanedData.forEach(item => {\n const groupKey = item[groupByParam];\n \n if (!groupKey || groupKey === '' || groupKey === null) return;\n \n if (!grouped[groupKey]) {\n grouped[groupKey] = {\n _count: 0,\n _values: {}\n };\n \n Object.keys(aggregationRules).forEach(param => {\n if (param !== groupByParam) {\n grouped[groupKey]._values[param] = [];\n }\n });\n }\n \n grouped[groupKey]._count++;\n \n Object.entries(item).forEach(([key, value]) => {\n if (key === groupByParam) return;\n \n if (value !== null && value !== undefined && value !== '' && grouped[groupKey]._values[key]) {\n grouped[groupKey]._values[key].push(value);\n }\n });\n });\n\n const result = [];\n\n Object.entries(grouped).forEach(([groupKey, groupData]) => {\n const aggregated = {\n [groupByParam]: groupKey,\n 'Element Count': groupData._count\n };\n \n Object.entries(groupData._values).forEach(([param, values]) => {\n const rule = aggregationRules[param] || 'first';\n \n if (values.length === 0) {\n aggregated[param] = null;\n return;\n }\n \n switch(rule) {\n case 'sum':\n const numericValues = values.map(v => {\n const num = parseFloat(String(v).replace(',', '.'));\n return isNaN(num) ? 0 : num;\n });\n aggregated[param] = numericValues.reduce((a, b) => a + b, 0);\n // Округляем до 2 знаков после запятой если нужно\n if (aggregated[param] % 1 !== 0) {\n aggregated[param] = Math.round(aggregated[param] * 100) / 100;\n }\n break;\n \n case 'mean':\n case 'average':\n const avgValues = values.map(v => {\n const num = parseFloat(String(v).replace(',', '.'));\n return isNaN(num) ? null : num;\n }).filter(v => v !== null);\n \n if (avgValues.length > 0) {\n const avg = avgValues.reduce((a, b) => a + b, 0) / avgValues.length;\n aggregated[param] = Math.round(avg * 100) / 100;\n } else {\n aggregated[param] = values[0];\n }\n break;\n \n case 'first':\n default:\n aggregated[param] = values[0];\n break;\n }\n });\n \n result.push({ json: aggregated });\n });\n\n// Сортируем результат\n result.sort((a, b) => {\n const aVal = a.json[groupByParam];\n const bVal = b.json[groupByParam];\n if (aVal < bVal) return -1;\n if (aVal > bVal) return 1;\n return 0;\n });\n\n console.log(`\\nGrouping complete:`);\n console.log(`- Input items: ${cleanedData.length}`);\n console.log(`- Output groups: ${result.length}`);\n console.log(`- Parameters processed: ${Object.keys(aggregationRules).length}`);\n\n const rulesSummary = { sum: [], mean: [], first: [] };\n Object.entries(aggregationRules).forEach(([param, rule]) => {\n if (rulesSummary[rule]) rulesSummary[rule].push(param);\n });\n\n console.log('\\nAggregation summary:');\n if (rulesSummary.sum.length > 0) {\n console.log(`- SUM (${rulesSummary.sum.length}): ${rulesSummary.sum.slice(0, 5).join(', ')}${rulesSummary.sum.length > 5 ? '...' : ''}`);\n }\n if (rulesSummary.mean.length > 0) {\n console.log(`- MEAN (${rulesSummary.mean.length}): ${rulesSummary.mean.slice(0, 5).join(', ')}${rulesSummary.mean.length > 5 ? '...' : ''}`);\n }\n if (rulesSummary.first.length > 0) {\n console.log(`- FIRST (${rulesSummary.first.length}): ${rulesSummary.first.slice(0, 5).join(', ')}${rulesSummary.first.length > 5 ? '...' : ''}`);\n }\n\n return result;"
},
"typeVersion": 2
},
{
"id": "ff967309-0ef4-4b49-abf1-b4c3ff6d48ac",
"name": "AI Classify Categories1",
"type": "@n8n/n8n-nodes-langchain.openAi",
"position": [
112,
768
],
"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": {
"id": "5SwKOx6OOukR6C0w",
"name": "OpenAi account n8n"
}
},
"typeVersion": 1.3
},
{
"id": "3e9cd8b6-3117-493b-97f7-c9d6a889e55d",
"name": "Is Building Element1",
"type": "n8n-nodes-base.if",
"position": [
640,
768
],
"parameters": {
"conditions": {
"boolean": [
{
"value1": "={{ $json.is_building_element }}",
"value2": true
}
]
}
},
"typeVersion": 1
},
{
"id": "1f3e2b55-6c9d-4466-801b-d8b31792a907",
"name": "Check If All Batches Done",
"type": "n8n-nodes-base.if",
"position": [
-416,
1200
],
"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": "15da077e-8560-4bf9-b610-0199f66292f8",
"name": "Collect All Results",
"type": "n8n-nodes-base.code",
"position": [
-192,
1552
],
"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": "7ae11a97-c8f9-4554-a1ae-0fc22c5ef6f9",
"name": "Process in Batches1",
"type": "n8n-nodes-base.splitInBatches",
"position": [
-256,
1072
],
"parameters": {
"options": {},
"batchSize": 1
},
"typeVersion": 1
},
{
"id": "b2423ed4-6d42-4b31-a1c1-db9affbf720b",
"name": "Clean Empty Values1",
"type": "n8n-nodes-base.code",
"position": [
-80,
1072
],
"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": "b9e77fc6-907b-47d2-8625-fdc0f6febb56",
"name": "AI 에이전트 Enhanced",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
272,
1072
],
"parameters": {
"text": "={{ $json.userPrompt }}",
"options": {
"systemMessage": "={{ $json.systemPrompt }}"
},
"promptType": "define"
},
"typeVersion": 1.7
},
{
"id": "2c3a57c2-66c2-414a-82e0-8f0df26cbfc8",
"name": "Accumulate Results",
"type": "n8n-nodes-base.code",
"position": [
848,
1216
],
"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": "fbf5d0a1-ed19-49e0-9a6e-27bf506bedec",
"name": "Anthropic 채팅 모델1",
"type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
"position": [
160,
1264
],
"parameters": {
"model": {
"__rl": true,
"mode": "list",
"value": "claude-opus-4-20250514",
"cachedResultName": "Claude Opus 4"
},
"options": {}
},
"credentials": {
"anthropicApi": {
"id": "af7za1FRO2jVYY9L",
"name": "Anthropic account"
}
},
"typeVersion": 1.3
},
{
"id": "e7df68eb-4420-48b5-8465-b80c49f0122f",
"name": "Calculate Project Totals4",
"type": "n8n-nodes-base.code",
"position": [
-16,
1552
],
"parameters": {
"jsCode": "// Aggregate all results and calculate project totals\nconst items = $input.all();\n\n// Initialize aggregators\nconst projectTotals = {\n totalElements: 0,\n totalMass: 0,\n totalCO2: 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 mass = parseFloat(data['Element Mass (tonnes)']) || 0;\n const co2 = parseFloat(data['Total CO2 (tonnes CO2e)']) || 0;\n const volume = parseFloat(data['Volume (m³)']) || 0;\n const area = parseFloat(data['Area (m²)']) || 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.totalMass += mass;\n projectTotals.totalCO2 += co2;\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 mass: 0,\n co2: 0,\n volume: 0,\n types: new Set()\n };\n }\n projectTotals.byMaterial[material].elements += elementCount;\n projectTotals.byMaterial[material].mass += mass;\n projectTotals.byMaterial[material].co2 += co2;\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 co2: 0\n };\n }\n projectTotals.byCategory[category].elements += elementCount;\n projectTotals.byCategory[category].co2 += co2;\n \n // Aggregate by impact\n if (!projectTotals.byImpact[impact]) {\n projectTotals.byImpact[impact] = {\n elements: 0,\n co2: 0\n };\n }\n projectTotals.byImpact[impact].elements += elementCount;\n projectTotals.byImpact[impact].co2 += co2;\n});\n\n// Add percentages and rankings to each item\nconst enrichedItems = items.map((item, index) => {\n const data = item.json;\n const co2 = parseFloat(data['Total CO2 (tonnes CO2e)']) || 0;\n const mass = parseFloat(data['Element Mass (tonnes)']) || 0;\n const elementCount = parseFloat(data['Element Count']) || 1;\n \n return {\n json: {\n ...data,\n // Add project percentages\n 'CO2 % of Total': projectTotals.totalCO2 > 0 ? \n ((co2 / projectTotals.totalCO2) * 100).toFixed(2) : '0.00',\n 'Mass % of Total': projectTotals.totalMass > 0 ? \n ((mass / projectTotals.totalMass) * 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 'CO2 Rank': index + 1,\n // Project totals (same for all rows)\n 'Project Total Elements': projectTotals.totalElements,\n 'Project Total Mass (tonnes)': projectTotals.totalMass.toFixed(3),\n 'Project Total CO2 (tonnes)': projectTotals.totalCO2.toFixed(3),\n 'Project Total Volume (m³)': projectTotals.totalVolume.toFixed(2),\n 'Project Total Area (m²)': projectTotals.totalArea.toFixed(2)\n }\n };\n});\n\n// Sort by CO2 emissions (highest first)\nenrichedItems.sort((a, b) => \n parseFloat(b.json['Total CO2 (tonnes CO2e)']) - parseFloat(a.json['Total CO2 (tonnes CO2e)'])\n);\n\n// Store aggregated data for summary\n$getWorkflowStaticData('global').projectTotals = projectTotals;\n\nreturn enrichedItems;"
},
"typeVersion": 2
},
{
"id": "82b3f0eb-07d8-4a97-82a0-bbc38c7d040a",
"name": "Enhance Excel Output",
"type": "n8n-nodes-base.code",
"position": [
528,
1712
],
"parameters": {
"jsCode": "// Enhanced Excel styling configuration\nconst excelBuffer = $input.first().binary.data;\nconst fileName = `CO2_Analysis_Professional_Report_${new Date().toISOString().slice(0,10)}.xlsx`;\n\n// Add metadata to the file\nconst metadata = {\n title: 'Carbon Footprint Analysis Report',\n author: 'DataDrivenConstruction.io',\n company: 'Automated CO2 Analysis System',\n created: new Date().toISOString(),\n description: 'Comprehensive embodied carbon assessment with multi-standard material classification',\n keywords: 'CO2, Carbon Footprint, Embodied Carbon, LCA, 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": "4d379a3c-ab28-450b-a7a1-0eb28b706376",
"name": "Prepare Excel Data",
"type": "n8n-nodes-base.code",
"position": [
192,
1712
],
"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 'Benchmark': 'N/A',\n 'Status': '✓',\n 'Notes': 'All building elements included in analysis'\n },\n {\n 'Category': 'PROJECT OVERVIEW',\n 'Metric': 'Element Groups',\n 'Value': items.length,\n 'Unit': 'groups',\n 'Benchmark': 'N/A',\n 'Status': '✓',\n 'Notes': 'Grouped by element type'\n },\n {\n 'Category': 'CARBON METRICS',\n 'Metric': 'Total Embodied Carbon',\n 'Value': formatNumber(projectTotals.totalCO2, 2),\n 'Unit': 'tonnes CO2e',\n 'Benchmark': 'Industry avg: ' + formatNumber(projectTotals.totalCO2 * 1.2, 0),\n 'Status': projectTotals.totalCO2 < projectTotals.totalCO2 * 1.2 ? '✓' : '⚠',\n 'Notes': 'A1-A3 lifecycle stages only'\n },\n {\n 'Category': 'CARBON METRICS',\n 'Metric': 'Average Carbon Intensity',\n 'Value': formatNumber(projectTotals.totalCO2 / projectTotals.totalMass, 3),\n 'Unit': 'kg CO2e/kg',\n 'Benchmark': '1.5-2.5',\n 'Status': (projectTotals.totalCO2 / projectTotals.totalMass) < 2.5 ? '✓' : '⚠',\n 'Notes': 'Average across all materials'\n },\n {\n 'Category': 'MATERIAL METRICS',\n 'Metric': 'Total Material Mass',\n 'Value': formatNumber(projectTotals.totalMass, 2),\n 'Unit': 'tonnes',\n 'Benchmark': 'N/A',\n 'Status': '✓',\n 'Notes': 'Combined mass of all materials'\n },\n {\n 'Category': 'MATERIAL METRICS',\n 'Metric': 'Unique Material Types',\n 'Value': Object.keys(projectTotals.byMaterial).length,\n 'Unit': 'materials',\n 'Benchmark': '10-20',\n 'Status': '✓',\n 'Notes': 'Distinct material classifications'\n },\n {\n 'Category': 'VOLUMETRIC DATA',\n 'Metric': 'Total Volume',\n 'Value': formatNumber(projectTotals.totalVolume, 2),\n 'Unit': 'm³',\n 'Benchmark': 'N/A',\n 'Status': '✓',\n 'Notes': 'Where volume data available'\n },\n {\n 'Category': 'VOLUMETRIC DATA',\n 'Metric': 'Total Area',\n 'Value': formatNumber(projectTotals.totalArea, 2),\n 'Unit': 'm²',\n 'Benchmark': 'N/A',\n 'Status': '✓',\n 'Notes': 'Where area data available'\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 // Ranking and identification\n 'CO2_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 \n // Material classification (all 3 standards)\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 \n // Quantities and dimensions\n 'Quantity_Value': formatNumber(data['Quantity'] || 0, 3),\n 'Quantity_Unit': data['Quantity Unit'] || 'piece',\n 'Volume_m3': formatNumber(data['Volume (m³)'] || 0, 3),\n 'Area_m2': formatNumber(data['Area (m²)'] || 0, 3),\n 'Length_mm': formatNumber(data['Length (mm)'] || 0, 0),\n 'Width_mm': formatNumber(data['Width (mm)'] || 0, 0),\n 'Height_mm': formatNumber(data['Height (mm)'] || 0, 0),\n 'Thickness_mm': formatNumber(data['Thickness (mm)'] || 0, 0),\n \n // Mass and density\n 'Material_Density_kg_m3': formatNumber(data['Material Density (kg/m³)'] || 0, 0),\n 'Element_Mass_kg': formatNumber(data['Element Mass (kg)'] || 0, 2),\n 'Element_Mass_tonnes': formatNumber(data['Element Mass (tonnes)'] || 0, 3),\n \n // CO2 emissions data\n 'CO2_Factor_kg_CO2_per_kg': formatNumber(data['CO2 Factor (kg CO2e/kg)'] || 0, 3),\n 'Total_CO2_kg': formatNumber(data['Total CO2 (kg CO2e)'] || 0, 2),\n 'Total_CO2_tonnes': formatNumber(data['Total CO2 (tonnes CO2e)'] || 0, 3),\n 'CO2_per_Element_kg': formatNumber(data['CO2 per Element (kg CO2e)'] || 0, 2),\n 'CO2_Intensity': formatNumber(data['CO2 Intensity'] || 0, 3),\n 'CO2_Percent_of_Total': formatNumber(data['CO2 % of Total'] || 0, 2),\n \n // Impact and quality metrics\n 'Impact_Category': data['Impact Category'] || 'Unknown',\n 'Lifecycle_Stage': data['Lifecycle Stage'] || 'A1-A3',\n 'Data_Source': data['Data Source'] || 'Industry average',\n \n // Confidence scores\n 'Overall_Confidence_%': parseInt(data['Overall Confidence (%)']) || 0,\n 'Material_Confidence_%': parseInt(data['Material Confidence (%)']) || 0,\n 'Quantity_Confidence_%': parseInt(data['Quantity Confidence (%)']) || 0,\n 'CO2_Confidence_%': parseInt(data['CO2 Confidence (%)']) || 0,\n 'Data_Quality': data['Data Quality'] || 'unknown',\n \n // Analysis metadata\n 'Calculation_Method': data['Calculation Method'] || 'Not specified',\n 'Assumptions': data['Assumptions'] || 'None',\n 'Warnings': data['Warnings'] || 'None',\n 'Analysis_Notes': data['Analysis Notes'] || '',\n 'Processing_Timestamp': data['Processing Timestamp'] || new Date().toISOString(),\n 'Analysis_Status': data['Analysis Status'] || 'Unknown'\n };\n});\n\n// Sheet 3: Material Summary with detailed breakdown\nconst materialSummary = Object.entries(projectTotals.byMaterial)\n .sort((a, b) => b[1].co2 - a[1].co2)\n .map(([material, data], index) => {\n const co2Percent = (data.co2 / projectTotals.totalCO2) * 100;\n const massPercent = (data.mass / projectTotals.totalMass) * 100;\n const avgCO2Factor = data.mass > 0 ? data.co2 / data.mass : 0;\n \n return {\n 'Rank': index + 1,\n 'Material_Type': material,\n 'Element_Count': data.elements,\n 'Unique_Types': data.types ? data.types.size : 0,\n 'Mass_tonnes': formatNumber(data.mass, 2),\n 'Mass_%': formatNumber(massPercent, 1),\n 'Volume_m3': formatNumber(data.volume, 2),\n 'CO2_tonnes': formatNumber(data.co2, 2),\n 'CO2_%': formatNumber(co2Percent, 1),\n 'Avg_CO2_Factor': formatNumber(avgCO2Factor, 3),\n 'CO2_per_Element_kg': formatNumber(data.co2 / data.elements * 1000, 1),\n 'Impact_Level': co2Percent >= 20 ? 'CRITICAL' : co2Percent >= 10 ? 'HIGH' : co2Percent >= 5 ? 'MEDIUM' : 'LOW',\n 'Reduction_Potential_20%': formatNumber(data.co2 * 0.2, 2),\n 'Benchmark_Factor': formatNumber(avgCO2Factor * 0.8, 3)\n };\n });\n\n// Sheet 4: Category Analysis\nconst categoryAnalysis = Object.entries(projectTotals.byCategory)\n .sort((a, b) => b[1].co2 - a[1].co2)\n .map(([category, data], index) => {\n const co2Percent = (data.co2 / projectTotals.totalCO2) * 100;\n const elementsPercent = (data.elements / projectTotals.totalElements) * 100;\n \n return {\n 'Rank': index + 1,\n 'Category': category,\n 'Element_Count': data.elements,\n 'Elements_%': formatNumber(elementsPercent, 1),\n 'CO2_tonnes': formatNumber(data.co2, 2),\n 'CO2_%': formatNumber(co2Percent, 1),\n 'Avg_CO2_per_Element': formatNumber(data.co2 / data.elements * 1000, 1),\n 'CO2_Intensity_Ratio': formatNumber((data.co2 / data.elements) / (projectTotals.totalCO2 / projectTotals.totalElements), 2),\n 'Priority': co2Percent >= 15 ? 'HIGH' : co2Percent >= 5 ? 'MEDIUM' : 'LOW'\n };\n });\n\n// Sheet 5: Impact Analysis by Category\nconst impactAnalysis = Object.entries(projectTotals.byImpact || {})\n .map(([impact, data]) => ({\n 'Impact_Category': impact,\n 'Element_Count': data.elements,\n 'CO2_tonnes': formatNumber(data.co2, 2),\n 'CO2_%': formatNumber((data.co2 / projectTotals.totalCO2) * 100, 1),\n 'Avg_CO2_per_Element': formatNumber(data.co2 / data.elements * 1000, 1)\n }));\n\n// Sheet 6: Top 20 Hotspots with action items\nconst top20Hotspots = items\n .slice(0, 20)\n .map((item, index) => {\n const data = item.json;\n const co2Tonnes = parseFloat(data['Total CO2 (tonnes CO2e)']) || 0;\n const co2Percent = parseFloat(data['CO2 % of Total']) || 0;\n const elementCount = parseInt(data['Element Count']) || 1;\n \n // Generate specific recommendations based on material and impact\n let recommendation = '';\n if (co2Percent >= 10) {\n recommendation = 'CRITICAL: Prioritize immediate material substitution or design optimization';\n } else if (co2Percent >= 5) {\n recommendation = 'HIGH: Evaluate low-carbon alternatives and quantity reduction opportunities';\n } else if (co2Percent >= 2) {\n recommendation = 'MEDIUM: Consider optimization during value engineering phase';\n } else {\n recommendation = 'LOW: Monitor and optimize if convenient';\n }\n \n return {\n 'Priority_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 'Mass_tonnes': formatNumber(data['Element Mass (tonnes)'] || 0, 2),\n 'CO2_tonnes': formatNumber(co2Tonnes, 3),\n 'CO2_%': formatNumber(co2Percent, 2),\n 'CO2_per_Element': formatNumber(co2Tonnes / elementCount * 1000, 1),\n 'Impact_Level': co2Percent >= 10 ? 'CRITICAL' : co2Percent >= 5 ? 'HIGH' : co2Percent >= 2 ? 'MEDIUM' : 'LOW',\n 'Confidence_%': parseInt(data['Overall Confidence (%)']) || 0,\n 'Reduction_Target_20%': formatNumber(co2Tonnes * 0.2, 2),\n 'Recommendation': recommendation\n };\n });\n\n// Sheet 7: Data Quality Report\nconst dataQuality = items.map((item, index) => {\n const data = item.json;\n return {\n 'Element_Rank': index + 1,\n 'Element_Group': data['Element Name'] || 'Unknown',\n 'Overall_Confidence_%': parseInt(data['Overall Confidence (%)']) || 0,\n 'Material_Confidence_%': parseInt(data['Material Confidence (%)']) || 0,\n 'Quantity_Confidence_%': parseInt(data['Quantity Confidence (%)']) || 0,\n 'CO2_Confidence_%': parseInt(data['CO2 Confidence (%)']) || 0,\n 'Data_Quality': data['Data Quality'] || 'unknown',\n 'Data_Source': data['Data Source'] || 'Unknown',\n 'Assumptions': data['Assumptions'] || 'None',\n 'Warnings': data['Warnings'] || 'None',\n 'Analysis_Status': data['Analysis Status'] || 'Unknown'\n };\n}).filter(item => \n item['Overall_Confidence_%'] < 90 || \n item['Data_Quality'] !== 'high' || \n item['Warnings'] !== 'None'\n);\n\n// Sheet 8: Recommendations Summary\nconst recommendations = [\n {\n 'Priority': 1,\n 'Category': 'IMMEDIATE ACTIONS',\n 'Recommendation': `Focus on ${Object.entries(projectTotals.byMaterial).sort((a,b) => b[1].co2 - a[1].co2)[0][0]} optimization`,\n 'Potential_Savings': formatNumber(Object.entries(projectTotals.byMaterial).sort((a,b) => b[1].co2 - a[1].co2)[0][1].co2 * 0.2, 1) + ' tonnes CO2e',\n 'Implementation': 'Material substitution or design optimization',\n 'Timeline': '0-3 months'\n },\n {\n 'Priority': 2,\n 'Category': 'IMMEDIATE ACTIONS',\n 'Recommendation': `Review top ${items.filter(item => parseFloat(item.json['CO2 % of Total']) >= 5).length} high-impact element groups`,\n 'Potential_Savings': formatNumber(projectTotals.totalCO2 * 0.15, 1) + ' tonnes CO2e',\n 'Implementation': 'Design review and value engineering',\n 'Timeline': '0-3 months'\n },\n {\n 'Priority': 3,\n 'Category': 'SHORT TERM',\n 'Recommendation': 'Implement low-carbon concrete mixes where applicable',\n 'Potential_Savings': '10-15% reduction possible',\n 'Implementation': 'Specification updates',\n 'Timeline': '3-6 months'\n },\n {\n 'Priority': 4,\n 'Category': 'SHORT TERM',\n 'Recommendation': 'Increase recycled content in steel elements',\n 'Potential_Savings': '20-30% reduction possible',\n 'Implementation': 'Supplier engagement',\n 'Timeline': '3-6 months'\n },\n {\n 'Priority': 5,\n 'Category': 'MEDIUM TERM',\n 'Recommendation': 'Explore timber alternatives for suitable applications',\n 'Potential_Savings': 'Carbon negative potential',\n 'Implementation': 'Structural analysis required',\n 'Timeline': '6-12 months'\n }\n];\n\n// Create worksheet structure with proper sheet names\nconst worksheets = [\n { name: 'Executive Summary', data: executiveSummary },\n { name: 'All Elements', data: detailedElements },\n { name: 'Material Summary', data: materialSummary },\n { name: 'Category Analysis', data: categoryAnalysis },\n { name: 'Impact Analysis', data: impactAnalysis },\n { name: 'Top 20 Hotspots', data: top20Hotspots },\n { name: 'Data Quality', data: dataQuality },\n { name: 'Recommendations', data: recommendations }\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": "a4d43d4c-ef26-4d96-b8df-88cdff94a9b3",
"name": "Create Excel File",
"type": "n8n-nodes-base.spreadsheetFile",
"position": [
352,
1712
],
"parameters": {
"options": {
"fileName": "=CO2_Analysis_Report_{{ $now.format('yyyy-MM-dd') }}",
"headerRow": true,
"sheetName": "={{ $json._sheetName }}"
},
"operation": "toFile",
"fileFormat": "xlsx"
},
"typeVersion": 2
},
{
"id": "36bb02f7-baba-48c7-aad7-4fe2b057e6be",
"name": "Prepare Enhanced Prompts",
"type": "n8n-nodes-base.code",
"position": [
96,
1072
],
"parameters": {
"jsCode": "// Enhanced prompts for comprehensive CO2 analysis\nconst inputData = $input.first().json;\nconst originalGroupedData = { ...inputData };\n\nconst systemPrompt = `You are an expert in construction materials, carbon footprint analysis, and building element classification. Analyze the provided building element data and return a comprehensive CO2 emissions assessment.\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- Use the provided totals directly for mass and CO2 calculations.\n- If no volumetric data is available, estimate based on typical values, but prioritize provided data.\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)\n- **European (EN 15978/15804)**: Concrete/Steel/Wood/Glass/Insulation/etc.\n- **German (ÖKOBAUDAT)**: Mineralische/Metalle/Holz/Dämmstoffe/etc.\n- **US (MasterFormat)**: Division classifications\n\n### 3. Quantity Analysis\n- Primary quantity with unit (m³, m², 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. CO2 Emissions Calculation\n- Material density (kg/m³)\n- Mass calculation: Use total volume/area * density (do not multiply by count)\n- CO2 emission factor (kg CO2e/kg)\n- Total CO2 emissions (for the entire group)\n- Emission intensity metrics\n\n### 5. Data Quality & Confidence\n- Overall confidence score\n- Data completeness assessment\n- Key assumptions made\n- Warnings or limitations\n\n## Important Guidelines:\n1. Use industry-standard emission factors\n2. Apply conservative estimates when uncertain\n3. Consider full lifecycle emissions (A1-A3 minimum)\n4. Account for regional variations where relevant\n5. Include embodied carbon only (not operational)\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 \"co2_analysis\": {\n \"density_kg_m3\": number,\n \"mass_kg\": number,\n \"co2_factor_kg_co2_per_kg\": number,\n \"total_co2_kg\": number,\n \"co2_intensity_kg_per_unit\": number,\n \"lifecycle_stage\": \"A1-A3\",\n \"data_source\": \"string\"\n },\n \"confidence\": {\n \"overall_score\": number,\n \"material_confidence\": number,\n \"quantity_confidence\": number,\n \"co2_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 CO2 emissions. Remember: Volumes and areas are already total for the group, not per element.\n\n${JSON.stringify(inputData, null, 2)}\n\nProvide comprehensive CO2 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": "32640ecd-b337-4745-bbe2-072e60163dcf",
"name": "Parse Enhanced Response",
"type": "n8n-nodes-base.code",
"position": [
624,
1072
],
"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 CO2 for group\n const co2_kg = analysis.co2_analysis?.total_co2_kg || 0;\n const co2_tonnes = co2_kg / 1000;\n const elementCount = parseFloat(originalData['Element Count']) || 1;\n const co2_per_element = co2_kg / elementCount;\n \n // Determine impact category\n let impactCategory = 'Unknown';\n const co2Factor = analysis.co2_analysis?.co2_factor_kg_co2_per_kg || 0;\n if (co2Factor < 0) {\n impactCategory = 'Carbon Negative (Storage)';\n } else if (co2Factor <= 0.5) {\n impactCategory = 'Very Low Impact';\n } else if (co2Factor <= 1.0) {\n impactCategory = 'Low Impact';\n } else if (co2Factor <= 2.0) {\n impactCategory = 'Medium Impact';\n } else if (co2Factor <= 5.0) {\n impactCategory = 'High Impact';\n } else {\n impactCategory = 'Very High Impact';\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²)': analysis.quantities?.raw_dimensions?.area || originalData['Area'] || 0,\n 'Volume (m³)': analysis.quantities?.raw_dimensions?.volume || originalData['Volume'] || 0,\n \n // CO2 Analysis\n 'Material Density (kg/m³)': analysis.co2_analysis?.density_kg_m3 || 0,\n 'Element Mass (kg)': analysis.co2_analysis?.mass_kg || 0,\n 'Element Mass (tonnes)': (analysis.co2_analysis?.mass_kg || 0) / 1000,\n 'CO2 Factor (kg CO2e/kg)': analysis.co2_analysis?.co2_factor_kg_co2_per_kg || 0,\n 'Total CO2 (kg CO2e)': co2_kg,\n 'Total CO2 (tonnes CO2e)': co2_tonnes,\n 'CO2 per Element (kg CO2e)': co2_per_element,\n 'CO2 Intensity': analysis.co2_analysis?.co2_intensity_kg_per_unit || 0,\n 'Lifecycle Stage': analysis.co2_analysis?.lifecycle_stage || 'A1-A3',\n 'Data Source': analysis.co2_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 'CO2 Confidence (%)': analysis.confidence?.co2_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": "62b7db30-b6f2-4608-bcd8-f866759bcf56",
"name": "Read Excel File",
"type": "n8n-nodes-base.readBinaryFile",
"position": [
-288,
496
],
"parameters": {
"filePath": "={{ $json.path_to_file }}"
},
"typeVersion": 1
},
{
"id": "40d06cd0-e57e-4d9d-8557-a06f64126d43",
"name": "Parse Excel",
"type": "n8n-nodes-base.spreadsheetFile",
"position": [
-96,
496
],
"parameters": {
"options": {
"headerRow": true,
"sheetName": "={{ $node['Set Parameters'].json.sheet_name }}",
"includeEmptyCells": false
},
"fileFormat": "xlsx"
},
"typeVersion": 2
},
{
"id": "5f385292-ebab-4c4f-92b9-15c7ee875e02",
"name": "설정up - Define file paths",
"type": "n8n-nodes-base.set",
"position": [
-272,
-64
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "9cbd4ec9-df24-41e8-b47a-720a4cdb733b",
"name": "path_to_converter",
"type": "string",
"value": "C:\\Users\\Artem Boiko\\Desktop\\n8n pipelines\\DDC_Converter_Revit\\datadrivenlibs\\RvtExporter.exe"
},
{
"id": "aa834467-80fb-476a-bac1-6728478834f0",
"name": "project_file",
"type": "string",
"value": "C:\\Users\\Artem Boiko\\Documents\\GitHub\\cad2data-Revit-IFC-DWG-DGN-pipeline-with-conversion-validation-qto\\Sample_Projects\\2023 racbasicsampleproject.rvt"
},
{
"id": "4e4f5e6f-7a8b-4c5d-9e0f-1a2b3c4d5e6f",
"name": "group_by",
"type": "string",
"value": "Type Name"
},
{
"id": "5f6a7b8c-9d0e-4f1a-2b3c-4d5e6f7a8b9c",
"name": "country",
"type": "string",
"value": "Germany"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "c8ae79e4-3a12-4b9e-bcc2-129adf5ecf25",
"name": "Create - Excel filename",
"type": "n8n-nodes-base.set",
"position": [
-48,
-64
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "xlsx-filename-id",
"name": "xlsx_filename",
"type": "string",
"value": "={{ $json[\"project_file\"].slice(0, -4) + \"_rvt.xlsx\" }}"
},
{
"id": "path-to-converter-pass",
"name": "path_to_converter",
"type": "string",
"value": "={{ $json[\"path_to_converter\"] }}"
},
{
"id": "project-file-pass",
"name": "project_file",
"type": "string",
"value": "={{ $json[\"project_file\"] }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "60bcb5b5-2f61-4314-94a2-7d0bf53427fe",
"name": "Check - Does Excel file exist?",
"type": "n8n-nodes-base.readBinaryFile",
"position": [
144,
-64
],
"parameters": {
"filePath": "={{ $json[\"xlsx_filename\"] }}"
},
"typeVersion": 1,
"continueOnFail": true,
"alwaysOutputData": true
},
{
"id": "4a82e1e2-4c87-4132-82f0-bfcacf07ca38",
"name": "If - File exists?",
"type": "n8n-nodes-base.if",
"position": [
304,
-64
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 1,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "e7fb1577-e753-43f5-9f5a-4d5285aeb96e",
"operator": {
"type": "boolean",
"operation": "equals"
},
"leftValue": "={{ $binary.data ? true : false }}",
"rightValue": "={{ true }}"
}
]
}
},
"typeVersion": 2
},
{
"id": "3b60c81d-ec57-4183-b6e0-03fdf6636739",
"name": "Extract - Run converter",
"type": "n8n-nodes-base.executeCommand",
"position": [
64,
144
],
"parameters": {
"command": "=\"{{$node[\"Setup - Define file paths\"].json[\"path_to_converter\"]}}\" \"{{$node[\"Setup - Define file paths\"].json[\"project_file\"]}}\""
},
"typeVersion": 1,
"continueOnFail": true
},
{
"id": "4bf02c30-f53f-4345-a564-9f6b8f7f8a14",
"name": "Info - Skip conversion",
"type": "n8n-nodes-base.set",
"position": [
496,
-80
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "status-id",
"name": "status",
"type": "string",
"value": "File already exists - skipping conversion"
},
{
"id": "xlsx-filename-id",
"name": "xlsx_filename",
"type": "string",
"value": "={{ $node[\"Create - Excel filename\"].json[\"xlsx_filename\"] }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "3b014ecd-d104-4d18-8d78-260e45b9b61a",
"name": "Check - Did extraction succeed?",
"type": "n8n-nodes-base.if",
"position": [
272,
144
],
"parameters": {
"options": {},
"conditions": {
"options": {
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "condition1",
"operator": {
"type": "object",
"operation": "exists",
"rightType": "any"
},
"leftValue": "={{ $node[\"Extract - Run converter\"].json.error }}",
"rightValue": ""
}
]
}
},
"typeVersion": 2
},
{
"id": "760e0a5b-86a1-4515-b5bb-58a88448eba5",
"name": "Error - Show what went wrong",
"type": "n8n-nodes-base.set",
"position": [
496,
80
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "error-message-id",
"name": "error_message",
"type": "string",
"value": "=Extraction failed: {{ $node[\"Extract - Run converter\"].json.error || \"Unknown error\" }}"
},
{
"id": "error-code-id",
"name": "error_code",
"type": "number",
"value": "={{ $node[\"Extract - Run converter\"].json.code || -1 }}"
},
{
"id": "xlsx-filename-error",
"name": "xlsx_filename",
"type": "string",
"value": "={{ $node[\"Create - Excel filename\"].json[\"xlsx_filename\"] }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "977dcd47-8b05-4d18-9fa8-e8ce86ca6f8a",
"name": "설정 xlsx_filename after success",
"type": "n8n-nodes-base.set",
"position": [
496,
256
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "xlsx-filename-success",
"name": "xlsx_filename",
"type": "string",
"value": "={{ $node[\"Create - Excel filename\"].json[\"xlsx_filename\"] }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "04eb1693-ca61-4ec5-ad69-7993df0cb087",
"name": "병합 - Continue workflow",
"type": "n8n-nodes-base.merge",
"position": [
688,
-16
],
"parameters": {},
"typeVersion": 3
},
{
"id": "bc8164ab-2ae9-4e1d-b180-d492b1296473",
"name": "설정 Parameters",
"type": "n8n-nodes-base.set",
"position": [
832,
256
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "path-id",
"name": "path_to_file",
"type": "string",
"value": "={{ $json.xlsx_filename }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "28c70099-5314-4475-8ee0-0435a1e5ece8",
"name": "클릭 시 ‘Execute workflow’",
"type": "n8n-nodes-base.manualTrigger",
"position": [
-480,
-64
],
"parameters": {},
"typeVersion": 1
},
{
"id": "b9899d95-37cf-4540-902b-dce17b8f90ec",
"name": "Conversion Block",
"type": "n8n-nodes-base.stickyNote",
"position": [
-864,
-240
],
"parameters": {
"color": 5,
"width": 1912,
"height": 660,
"content": "## 🔄 Conversion Block\n\nAutomatic conversion of project files (Revit) to Excel.\n\n- Checks if Excel file exists\n- If not, runs the converter\n- If exists, skips conversion\n\nSimply: Converts project to data for analysis."
},
"typeVersion": 1
},
{
"id": "8e7df7b7-d4ab-499a-aacb-3e358e34da19",
"name": "Data Loading Block",
"type": "n8n-nodes-base.stickyNote",
"position": [
-864,
448
],
"parameters": {
"color": 6,
"width": 1920,
"height": 240,
"content": "## 📊 Block 1: Data Loading and Processing\n\n- Reads Excel file from specified path\n- Extracts and cleans column headers\n- Analyzes headers using AI for aggregation rules (sum, mean, first)\n- Groups data by parameter (e.g., Type Name)\n- Creates pivot table with aggregation\n\nSimply: Prepares data for further analysis."
},
"typeVersion": 1
},
{
"id": "26af2822-394b-4746-bf11-7bd7c6a692c1",
"name": "Element Classification Block",
"type": "n8n-nodes-base.stickyNote",
"position": [
-864,
720
],
"parameters": {
"color": 6,
"width": 1920,
"height": 256,
"content": "## 🏗️ Block 2: Element Classification\n\n- Searches for category fields (Category, IFC Type, etc.)\n- Uses AI to classify: building elements or annotations/drawings\n- Applies classification to data groups\n- Splits flow: building elements proceed, annotations output separately\n\nSimply: Separates real elements from drawings."
},
"typeVersion": 1
},
{
"id": "2b8e5d57-8f51-41d4-8558-594d921e2311",
"name": "Material Analysis Block",
"type": "n8n-nodes-base.stickyNote",
"position": [
-864,
1008
],
"parameters": {
"color": 5,
"width": 1920,
"height": 448,
"content": "\n## 🧪 Block 3: Material Analysis\n\n- Processes building elements in batches for optimization\n- Classifies materials by standards (EU, DE, US)\n- Determines quantity units (m³, m², kg, etc.)\n- Calculates density and factors\n- Uses AI (Grok) for detailed analysis\n- Collects results from batches\n\nSimply: Analyzes element materials."
},
"typeVersion": 1
},
{
"id": "ebc11ab6-d7db-4a1a-a46d-0938321d3c88",
"name": "CO2 Calculation and Reporting Block",
"type": "n8n-nodes-base.stickyNote",
"position": [
-864,
1488
],
"parameters": {
"color": 5,
"width": 1920,
"height": 416,
"content": "## 🌍 Block 4: CO2 Calculation and Reporting\n\n- Calculates CO2 emissions based on materials, volumes, and factors\n- Creates visualization data (charts, graphs)\n- Generates reports: Markdown, HTML, Excel with multiple sheets\n- Adds statistics and recommendations\n\nSimply: Computes carbon footprint and creates reports."
},
"typeVersion": 1
},
{
"id": "bcb4b571-4db5-4d91-b057-cf3f91f1c16b",
"name": "설정up Instructions",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1216,
-240
],
"parameters": {
"width": 336,
"height": 384,
"content": "## 📝 Setup Instructions\n\n1. In the 'Setup - Define file paths' node, specify:\n - Path to converter (RvtExporter.exe)\n - Path to project file (.rvt)\n - Grouping parameter (group_by, e.g. 'Type Name', 'IfcType' for IFC or other)\n - Country (country for which the values will be calculated, e.g. 'Germany'or 'Brazil')\n\n2. Ensure API keys for OpenAI and Anthropic are set in credentials or just connect other models that you use in your work (of course, these can be open source LLMs)"
},
"typeVersion": 1
},
{
"id": "3abb7e6c-6004-4da0-ba89-f36d893c495f",
"name": "Important 노트s",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1216,
160
],
"parameters": {
"width": 336,
"height": 288,
"content": "## ⚠️ Important Information\n\n- Pipeline uses AI (OpenAI, Grok, Anthropic) - check credits and limits.\n- Revit converter requires downloaded DDC_Converter_Revit\n- Data is aggregated by groups, volumes are already summed - do not multiply by element count\n- Reports are generated in HTML and in Excel wfor detailed analysis"
},
"typeVersion": 1
},
{
"id": "8d7c13c3-9044-45bc-9759-9baf1d175aa1",
"name": "Prepare HTML Path",
"type": "n8n-nodes-base.code",
"position": [
528,
1552
],
"parameters": {
"jsCode": "// Get the project file path from the original setup\nconst projectFile = $node['Setup - Define file paths'].json.project_file;\nconst htmlContent = $node['HTML to Binary'].json.html || $input.first().binary.data;\n\n// Extract directory path from project file\nconst path = projectFile.substring(0, projectFile.lastIndexOf('\\\\'));\n\n// Create filename with timestamp\nconst timestamp = new Date().toISOString().slice(0,10);\nconst htmlFileName = `CO2_Analysis_Report_${timestamp}.html`;\nconst fullPath = `${path}\\\\${htmlFileName}`;\n\nconsole.log('Project file:', projectFile);\nconsole.log('Directory:', path);\nconsole.log('HTML file will be saved to:', fullPath);\n\nreturn [{\n json: {\n html_filename: htmlFileName,\n full_path: fullPath,\n directory: path,\n project_file: projectFile\n },\n binary: $input.first().binary\n}];"
},
"typeVersion": 2
},
{
"id": "e4851576-a8ff-4618-ba9c-cae2ab579d5c",
"name": "Write HTML to Project Folder",
"type": "n8n-nodes-base.writeBinaryFile",
"position": [
704,
1552
],
"parameters": {
"options": {},
"fileName": "={{ $json.full_path }}"
},
"typeVersion": 1
},
{
"id": "7bd64a70-7bc3-47d3-9db8-c766aec135d6",
"name": "Open HTML in Browser",
"type": "n8n-nodes-base.executeCommand",
"position": [
880,
1552
],
"parameters": {
"command": "=start \"\" \"{{ $json.full_path }}\""
},
"typeVersion": 1
},
{
"id": "165b21cb-90a7-43e7-870c-036813bb60b6",
"name": "Prepare Excel Path",
"type": "n8n-nodes-base.code",
"position": [
704,
1712
],
"parameters": {
"jsCode": "// Similar logic for Excel file\nconst projectFile = $node['Setup - Define file paths'].json.project_file;\n\n// Extract directory path from project file\nconst path = projectFile.substring(0, projectFile.lastIndexOf('\\\\'));\n\n// Create filename with timestamp\nconst timestamp = new Date().toISOString().slice(0,10);\nconst excelFileName = `CO2_Analysis_Professional_Report_${timestamp}.xlsx`;\nconst fullPath = `${path}\\\\${excelFileName}`;\n\nconsole.log('Excel file will be saved to:', fullPath);\n\nreturn [{\n json: {\n excel_filename: excelFileName,\n full_path: fullPath,\n directory: path,\n project_file: projectFile\n },\n binary: $input.first().binary\n}];"
},
"typeVersion": 2
},
{
"id": "e1e918c3-c5a3-4382-878a-5cdf19c1fd48",
"name": "Write Excel to Project Folder",
"type": "n8n-nodes-base.writeBinaryFile",
"position": [
880,
1712
],
"parameters": {
"options": {},
"fileName": "={{ $json.full_path }}"
},
"typeVersion": 1
},
{
"id": "319e5fdf-1b31-48c6-9a8f-006c03fc23dd",
"name": "Generate HTML Report",
"type": "n8n-nodes-base.code",
"position": [
192,
1552
],
"parameters": {
"jsCode": "// Generate professional McKinsey/Accenture style HTML report with charts\nconst items = $input.all();\nconst projectTotals = $getWorkflowStaticData('global').projectTotals;\n\n// Get project name from the setup node\nconst projectFilePath = $node['Setup - Define file paths'].json.project_file || '';\nconst projectFileName = projectFilePath.split('\\\\').pop().split('/').pop().replace(/\\.[^/.]+$/, '');\n\n// Calculate key metrics\nconst topMaterial = Object.entries(projectTotals.byMaterial)\n .sort((a, b) => b[1].co2 - a[1].co2)[0];\nconst highImpactItems = items.filter(item => \n parseFloat(item.json['Total CO2 (tonnes CO2e)']) >= projectTotals.totalCO2 * 0.05\n).length;\n\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>Carbon Footprint Analysis | ${projectFileName}</title>\n <script src=\"https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.9.1/chart.min.js\"></script>\n <style>\n * { margin: 0; padding: 0; box-sizing: border-box; }\n \n body {\n font-family: 'Arial', 'Helvetica Neue', sans-serif;\n background: #ffffff;\n color: #2e2e38;\n line-height: 1.6;\n font-size: 13px;\n }\n \n .container {\n max-width: 1100px;\n margin: 0 auto;\n padding: 40px 20px;\n }\n \n /* Header - McKinsey style */\n .header {\n border-bottom: 3px solid #0061a0;\n padding-bottom: 20px;\n margin-bottom: 30px;\n }\n \n .header h1 {\n font-size: 32px;\n font-weight: 300;\n color: #0061a0;\n margin-bottom: 8px;\n letter-spacing: -0.5px;\n }\n \n .header .subtitle {\n color: #696969;\n font-size: 16px;\n font-weight: 400;\n }\n \n /* Executive Summary - Accenture purple accent */\n .executive-summary {\n background: linear-gradient(to right, #460073 0%, #0061a0 100%);\n color: white;\n padding: 30px;\n margin-bottom: 30px;\n position: relative;\n }\n \n .executive-summary h2 {\n font-size: 20px;\n font-weight: 400;\n margin-bottom: 15px;\n text-transform: uppercase;\n letter-spacing: 1px;\n }\n \n .executive-summary p {\n font-size: 15px;\n line-height: 1.8;\n opacity: 0.95;\n }\n \n /* KPI Cards - McKinsey teal */\n .kpi-grid {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));\n gap: 20px;\n margin-bottom: 30px;\n }\n \n .kpi-card {\n background: #ffffff;\n border: 1px solid #e0e0e0;\n padding: 25px;\n position: relative;\n transition: all 0.3s ease;\n }\n \n .kpi-card:hover {\n box-shadow: 0 4px 12px rgba(0,0,0,0.1);\n transform: translateY(-2px);\n }\n \n .kpi-card.primary {\n border-top: 4px solid #00a19a;\n }\n \n .kpi-card.secondary {\n border-top: 4px solid #460073;\n }\n \n .kpi-value {\n font-size: 36px;\n font-weight: 300;\n color: #0061a0;\n margin-bottom: 8px;\n line-height: 1;\n }\n \n .kpi-label {\n font-size: 12px;\n color: #696969;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n font-weight: 600;\n }\n \n .kpi-change {\n position: absolute;\n top: 20px;\n right: 20px;\n font-size: 11px;\n color: #00a19a;\n font-weight: 600;\n }\n \n /* Chart Containers */\n .charts-grid {\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 30px;\n margin-bottom: 30px;\n }\n \n .chart-container {\n background: #ffffff;\n border: 1px solid #e0e0e0;\n padding: 20px;\n position: relative;\n }\n \n .chart-container.full-width {\n grid-column: 1 / -1;\n }\n \n .chart-header {\n margin-bottom: 20px;\n padding-bottom: 10px;\n border-bottom: 1px solid #f0f0f0;\n }\n \n .chart-header h3 {\n font-size: 16px;\n font-weight: 600;\n color: #2e2e38;\n margin: 0;\n }\n \n .chart-header p {\n font-size: 12px;\n color: #696969;\n margin-top: 5px;\n }\n \n .chart-wrapper {\n position: relative;\n height: 300px;\n }\n \n .chart-wrapper.small {\n height: 250px;\n }\n \n /* Insights Section - BCG green */\n .insight-section {\n background: #f8f8f8;\n border-left: 5px solid #009a44;\n padding: 25px 30px;\n margin-bottom: 30px;\n }\n \n .insight-section h3 {\n color: #009a44;\n font-size: 18px;\n font-weight: 600;\n margin-bottom: 12px;\n }\n \n .insight-section p {\n color: #2e2e38;\n font-size: 14px;\n line-height: 1.8;\n }\n \n .insight-highlight {\n color: #0061a0;\n font-weight: 600;\n }\n \n /* Data Tables - Professional style */\n .table-container {\n background: #ffffff;\n border: 1px solid #e0e0e0;\n margin-bottom: 30px;\n overflow: hidden;\n }\n \n .table-header {\n background: #f5f5f5;\n padding: 15px 20px;\n border-bottom: 1px solid #e0e0e0;\n }\n \n .table-header h3 {\n font-size: 16px;\n font-weight: 600;\n color: #2e2e38;\n margin: 0;\n }\n \n table {\n width: 100%;\n border-collapse: collapse;\n font-size: 13px;\n }\n \n th {\n background: #fafafa;\n padding: 12px 15px;\n text-align: left;\n font-weight: 600;\n color: #2e2e38;\n border-bottom: 2px solid #e0e0e0;\n font-size: 12px;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n }\n \n td {\n padding: 12px 15px;\n border-bottom: 1px solid #f0f0f0;\n color: #4a4a4a;\n }\n \n tr:hover {\n background: #f9f9f9;\n }\n \n .value-highlight {\n font-weight: 600;\n color: #0061a0;\n }\n \n /* Impact levels - Traffic light system */\n .impact-critical {\n display: inline-block;\n padding: 3px 8px;\n background: #dc3545;\n color: white;\n font-size: 11px;\n font-weight: 600;\n border-radius: 2px;\n }\n \n .impact-high {\n display: inline-block;\n padding: 3px 8px;\n background: #ff7043;\n color: white;\n font-size: 11px;\n font-weight: 600;\n border-radius: 2px;\n }\n \n .impact-medium {\n display: inline-block;\n padding: 3px 8px;\n background: #ffa726;\n color: white;\n font-size: 11px;\n font-weight: 600;\n border-radius: 2px;\n }\n \n .impact-low {\n display: inline-block;\n padding: 3px 8px;\n background: #66bb6a;\n color: white;\n font-size: 11px;\n font-weight: 600;\n border-radius: 2px;\n }\n \n /* Progress bars */\n .progress-bar {\n background: #e0e0e0;\n height: 6px;\n border-radius: 3px;\n overflow: hidden;\n width: 100px;\n display: inline-block;\n vertical-align: middle;\n margin-left: 10px;\n }\n \n .progress-fill {\n height: 100%;\n background: #00a19a;\n transition: width 0.3s ease;\n }\n \n /* Action items - Professional blue-grey */\n .action-box {\n background: #f8f9fa;\n border-left: 4px solid #0061a0;\n padding: 25px 30px;\n margin: 30px 0;\n }\n \n .action-box h4 {\n color: #2e2e38;\n font-size: 16px;\n font-weight: 600;\n margin-bottom: 15px;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n }\n \n .action-list {\n list-style: none;\n padding: 0;\n }\n \n .action-list li {\n padding: 10px 0;\n padding-left: 30px;\n position: relative;\n color: #4a4a4a;\n line-height: 1.6;\n }\n \n .action-list li:before {\n content: \"→\";\n position: absolute;\n left: 0;\n color: #0061a0;\n font-weight: bold;\n }\n \n /* Footer */\n .footer {\n margin-top: 50px;\n padding-top: 20px;\n border-top: 1px solid #e0e0e0;\n text-align: center;\n color: #696969;\n font-size: 11px;\n }\n \n .footer .logo {\n font-weight: 600;\n color: #0061a0;\n }\n \n /* Print optimization */\n @media print {\n body { background: white; }\n .container { padding: 20px; }\n .table-container { box-shadow: none; border: 1px solid #ddd; }\n .kpi-card { box-shadow: none; }\n }\n \n /* Responsive */\n @media (max-width: 768px) {\n .kpi-grid { grid-template-columns: 1fr; }\n .charts-grid { grid-template-columns: 1fr; }\n .header h1 { font-size: 24px; }\n .kpi-value { font-size: 28px; }\n }\n </style>\n</head>\n<body>\n <div class=\"container\">\n <!-- Header -->\n <div class=\"header\">\n <h1>Carbon Footprint Analysis</h1>\n <div class=\"subtitle\">Project: <strong>${projectFileName}</strong> | Executive Summary Report | ${new Date().toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' })}</div>\n </div>\n \n <!-- Executive Summary -->\n <div class=\"executive-summary\">\n <h2>Executive Summary</h2>\n <p>\n Analysis reveals <strong>${projectTotals.totalCO2.toFixed(1)} tonnes CO2e</strong> of embodied carbon across \n ${projectTotals.totalElements.toLocaleString()} building elements. The assessment identifies ${highImpactItems} \n high-impact element groups contributing over 5% each to total emissions, presenting clear optimization opportunities.\n </p>\n </div>\n \n <!-- KPI Cards -->\n <div class=\"kpi-grid\">\n <div class=\"kpi-card primary\">\n <div class=\"kpi-value\">${projectTotals.totalCO2.toFixed(0)}</div>\n <div class=\"kpi-label\">Total CO2 Emissions</div>\n <div class=\"kpi-change\">tonnes CO2e</div>\n </div>\n <div class=\"kpi-card secondary\">\n <div class=\"kpi-value\">${((topMaterial[1].co2 / projectTotals.totalCO2) * 100).toFixed(0)}%</div>\n <div class=\"kpi-label\">${topMaterial[0]} Impact</div>\n <div class=\"kpi-change\">of total</div>\n </div>\n <div class=\"kpi-card primary\">\n <div class=\"kpi-value\">${(projectTotals.totalCO2 / projectTotals.totalMass).toFixed(1)}</div>\n <div class=\"kpi-label\">Average Intensity</div>\n <div class=\"kpi-change\">kg CO2e/kg</div>\n </div>\n <div class=\"kpi-card secondary\">\n <div class=\"kpi-value\">${Object.keys(projectTotals.byMaterial).length}</div>\n <div class=\"kpi-label\">Material Types</div>\n <div class=\"kpi-change\">identified</div>\n </div>\n </div>\n \n <!-- Charts Section -->\n <div class=\"charts-grid\">\n <!-- Pie Chart - Material Distribution -->\n <div class=\"chart-container\">\n <div class=\"chart-header\">\n <h3>CO2 Emissions by Material Type</h3>\n <p>Proportional distribution of carbon impact</p>\n </div>\n <div class=\"chart-wrapper small\">\n <canvas id=\"materialPieChart\"></canvas>\n </div>\n </div>\n \n <!-- Donut Chart - Impact Concentration -->\n <div class=\"chart-container\">\n <div class=\"chart-header\">\n <h3>Impact Concentration Analysis</h3>\n <p>Top 5 vs. remaining elements</p>\n </div>\n <div class=\"chart-wrapper small\">\n <canvas id=\"concentrationChart\"></canvas>\n </div>\n </div>\n \n <!-- Bar Chart - Top Contributors -->\n <div class=\"chart-container full-width\">\n <div class=\"chart-header\">\n <h3>Top 10 Carbon Contributors</h3>\n <p>Individual element groups ranked by CO2 emissions</p>\n </div>\n <div class=\"chart-wrapper\">\n <canvas id=\"topContributorsBar\"></canvas>\n </div>\n </div>\n </div>\n \n <!-- Key Insight -->\n <div class=\"insight-section\">\n <h3>Primary Finding</h3>\n <p>\n <span class=\"insight-highlight\">${topMaterial[0]}</span> represents the largest carbon impact at \n <span class=\"insight-highlight\">${topMaterial[1].co2.toFixed(1)} tonnes CO2e</span>, accounting for \n <span class=\"insight-highlight\">${((topMaterial[1].co2 / projectTotals.totalCO2) * 100).toFixed(0)}%</span> \n of total emissions across ${topMaterial[1].elements.toLocaleString()} elements. This concentration presents \n the primary opportunity for carbon reduction through material substitution or design optimization.\n </p>\n </div>\n \n <!-- Top Contributors Table -->\n <div class=\"table-container\">\n <div class=\"table-header\">\n <h3>Top 10 Carbon Contributors</h3>\n </div>\n <table>\n <thead>\n <tr>\n <th style=\"width: 60px;\">Rank</th>\n <th>Element Group</th>\n <th>Material Type</th>\n <th style=\"width: 80px;\">Quantity</th>\n <th style=\"width: 100px;\">CO2 (tonnes)</th>\n <th style=\"width: 150px;\">% of Total</th>\n <th style=\"width: 80px;\">Priority</th>\n </tr>\n </thead>\n <tbody>\n ${items.slice(0, 10).map((item, index) => {\n const data = item.json;\n const co2Percent = parseFloat(data['CO2 % of Total']);\n let impactClass = 'impact-low';\n let impactText = 'LOW';\n \n if (co2Percent >= 10) {\n impactClass = 'impact-critical';\n impactText = 'CRITICAL';\n } else if (co2Percent >= 5) {\n impactClass = 'impact-high';\n impactText = 'HIGH';\n } else if (co2Percent >= 2) {\n impactClass = 'impact-medium';\n impactText = 'MEDIUM';\n }\n \n return `\n <tr>\n <td>${index + 1}</td>\n <td class=\"value-highlight\">${data['Element Name']}</td>\n <td>${data['Material (EU Standard)']}</td>\n <td>${data['Element Count']}</td>\n <td class=\"value-highlight\">${parseFloat(data['Total CO2 (tonnes CO2e)']).toFixed(2)}</td>\n <td>\n ${co2Percent.toFixed(1)}%\n <div class=\"progress-bar\">\n <div class=\"progress-fill\" style=\"width: ${Math.min(co2Percent * 5, 100)}%\"></div>\n </div>\n </td>\n <td><span class=\"${impactClass}\">${impactText}</span></td>\n </tr>`;\n }).join('')}\n </tbody>\n </table>\n </div>\n \n <!-- Material Analysis Table -->\n <div class=\"table-container\">\n <div class=\"table-header\">\n <h3>Material Impact Analysis</h3>\n </div>\n <table>\n <thead>\n <tr>\n <th>Material Classification</th>\n <th style=\"width: 100px;\">Elements</th>\n <th style=\"width: 100px;\">Mass (t)</th>\n <th style=\"width: 100px;\">CO2 (t)</th>\n <th style=\"width: 150px;\">% of Total</th>\n <th style=\"width: 120px;\">Intensity</th>\n </tr>\n </thead>\n <tbody>\n ${Object.entries(projectTotals.byMaterial)\n .sort((a, b) => b[1].co2 - a[1].co2)\n .slice(0, 6)\n .map(([material, data]) => {\n const percent = (data.co2 / projectTotals.totalCO2) * 100;\n return `\n <tr>\n <td class=\"value-highlight\">${material}</td>\n <td>${data.elements.toLocaleString()}</td>\n <td>${data.mass.toFixed(1)}</td>\n <td class=\"value-highlight\">${data.co2.toFixed(1)}</td>\n <td>\n ${percent.toFixed(1)}%\n <div class=\"progress-bar\">\n <div class=\"progress-fill\" style=\"width: ${Math.min(percent * 3, 100)}%\"></div>\n </div>\n </td>\n <td>${(data.co2 / data.mass).toFixed(2)} kg/kg</td>\n </tr>`;\n }).join('')}\n </tbody>\n </table>\n </div>\n \n <!-- Cumulative Impact Chart - moved here after Material Analysis -->\n <div class=\"charts-grid\">\n <div class=\"chart-container full-width\">\n <div class=\"chart-header\">\n <h3>Cumulative Carbon Impact</h3>\n <p>Pareto analysis showing concentration of emissions</p>\n </div>\n <div class=\"chart-wrapper\">\n <canvas id=\"cumulativeChart\"></canvas>\n </div>\n </div>\n </div>\n \n <!-- Action Items -->\n <div class=\"action-box\">\n <h4>Recommended Actions</h4>\n <ul class=\"action-list\">\n <li>Prioritize ${topMaterial[0]} optimization - potential ${(topMaterial[1].co2 * 0.2).toFixed(0)}t CO2e reduction with 20% improvement</li>\n <li>Review specification for ${highImpactItems} high-impact element groups (>5% each of total)</li>\n <li>Investigate low-carbon alternatives for top 3 materials: ${Object.entries(projectTotals.byMaterial).sort((a,b) => b[1].co2 - a[1].co2).slice(0,3).map(([m]) => m).join(', ')}</li>\n <li>Focus on elements exceeding project average intensity of ${(projectTotals.totalCO2 / projectTotals.totalMass).toFixed(1)} kg CO2e per kg material</li>\n </ul>\n </div>\n \n <!-- Footer -->\n <div class=\"footer\">\n <p>\n <span class=\"logo\">Carbon Footprint Analysis</span> • \n Embodied Carbon Assessment (A1-A3) • \n Generated ${new Date().toLocaleString()}\n </p>\n <p style=\"margin-top: 8px;\">\n Powered by DataDrivenConstruction.io • \n Detailed calculations available in accompanying Excel workbook\n </p>\n </div>\n </div>\n \n <script>\n // Chart.js global configuration\n Chart.defaults.font.family = \"'Arial', 'Helvetica Neue', sans-serif\";\n Chart.defaults.font.size = 11;\n \n // Professional color palette\n const colors = {\n primary: ['#0061a0', '#00a19a', '#460073', '#009a44', '#ff7043', '#ffa726'],\n secondary: ['#4fc3f7', '#81c784', '#ba68c8', '#ffb74d', '#e57373', '#64b5f6'],\n gradient: ['rgba(0, 97, 160, 0.8)', 'rgba(0, 161, 154, 0.8)', 'rgba(70, 0, 115, 0.8)']\n };\n \n // Material Pie Chart\n const materialData = ${JSON.stringify(Object.entries(projectTotals.byMaterial)\n .sort((a, b) => b[1].co2 - a[1].co2)\n .slice(0, 6)\n .map(([material, data]) => ({\n label: material,\n value: data.co2\n })))};\n \n new Chart(document.getElementById('materialPieChart'), {\n type: 'pie',\n data: {\n labels: materialData.map(d => d.label),\n datasets: [{\n data: materialData.map(d => d.value.toFixed(1)),\n backgroundColor: colors.primary,\n borderColor: '#ffffff',\n borderWidth: 2\n }]\n },\n options: {\n responsive: true,\n maintainAspectRatio: false,\n plugins: {\n legend: {\n position: 'right',\n labels: {\n padding: 10,\n usePointStyle: true,\n font: { size: 11 }\n }\n },\n tooltip: {\n callbacks: {\n label: function(context) {\n const total = context.dataset.data.reduce((a, b) => parseFloat(a) + parseFloat(b), 0);\n const percent = ((context.parsed / total) * 100).toFixed(1);\n return context.label + ': ' + context.parsed + 't (' + percent + '%)';\n }\n }\n }\n }\n }\n });\n \n // Concentration Donut Chart\n const top5Total = ${items.slice(0, 5).reduce((sum, item) => \n sum + parseFloat(item.json['Total CO2 (tonnes CO2e)']), 0).toFixed(1)};\n const remainingTotal = ${(projectTotals.totalCO2 - items.slice(0, 5).reduce((sum, item) => \n sum + parseFloat(item.json['Total CO2 (tonnes CO2e)']), 0)).toFixed(1)};\n \n new Chart(document.getElementById('concentrationChart'), {\n type: 'doughnut',\n data: {\n labels: ['Top 5 Elements', 'Remaining Elements'],\n datasets: [{\n data: [top5Total, remainingTotal],\n backgroundColor: ['#0061a0', '#e0e0e0'],\n borderColor: '#ffffff',\n borderWidth: 2\n }]\n },\n options: {\n responsive: true,\n maintainAspectRatio: false,\n cutout: '60%',\n plugins: {\n legend: {\n position: 'bottom',\n labels: {\n padding: 15,\n usePointStyle: true,\n font: { size: 11 }\n }\n },\n tooltip: {\n callbacks: {\n label: function(context) {\n const percent = ((context.parsed / ${projectTotals.totalCO2}) * 100).toFixed(1);\n return context.label + ': ' + context.parsed + 't (' + percent + '%)';\n }\n }\n }\n }\n }\n });\n \n // Top Contributors Bar Chart\n const topItems = ${JSON.stringify(items.slice(0, 10).map(item => ({\n name: item.json['Element Name'],\n co2: parseFloat(item.json['Total CO2 (tonnes CO2e)'])\n })))};\n \n new Chart(document.getElementById('topContributorsBar'), {\n type: 'bar',\n data: {\n labels: topItems.map(d => d.name),\n datasets: [{\n label: 'CO2 Emissions (tonnes)',\n data: topItems.map(d => d.co2),\n backgroundColor: topItems.map((d, i) => {\n const percent = (d.co2 / ${projectTotals.totalCO2}) * 100;\n if (percent >= 10) return '#dc3545';\n if (percent >= 5) return '#ff7043';\n if (percent >= 2) return '#ffa726';\n return '#66bb6a';\n }),\n borderColor: 'rgba(0, 0, 0, 0.1)',\n borderWidth: 1\n }]\n },\n options: {\n responsive: true,\n maintainAspectRatio: false,\n indexAxis: 'y',\n plugins: {\n legend: {\n display: false\n },\n tooltip: {\n callbacks: {\n afterLabel: function(context) {\n const percent = ((context.parsed.x / ${projectTotals.totalCO2}) * 100).toFixed(1);\n return percent + '% of total';\n }\n }\n }\n },\n scales: {\n x: {\n grid: {\n color: 'rgba(0, 0, 0, 0.05)'\n },\n ticks: {\n callback: function(value) {\n return value + 't';\n }\n }\n },\n y: {\n grid: {\n display: false\n },\n ticks: {\n font: {\n size: 10\n }\n }\n }\n }\n }\n });\n \n // Cumulative Impact Area Chart\n const allItems = ${JSON.stringify(items.map(item => ({\n name: item.json['Element Name'],\n co2: parseFloat(item.json['Total CO2 (tonnes CO2e)'])\n })))};\n \n let cumulative = 0;\n const cumulativeData = allItems.map((item, index) => {\n cumulative += item.co2;\n return {\n x: index + 1,\n y: cumulative,\n percent: (cumulative / ${projectTotals.totalCO2}) * 100\n };\n });\n \n new Chart(document.getElementById('cumulativeChart'), {\n type: 'line',\n data: {\n labels: cumulativeData.map(d => 'Element ' + d.x),\n datasets: [{\n label: 'Cumulative CO2',\n data: cumulativeData.map(d => d.y),\n borderColor: '#0061a0',\n backgroundColor: 'rgba(0, 97, 160, 0.1)',\n fill: true,\n tension: 0.4,\n pointRadius: 0,\n pointHoverRadius: 4\n }, {\n label: 'Cumulative %',\n data: cumulativeData.map(d => d.percent),\n borderColor: '#00a19a',\n backgroundColor: 'transparent',\n yAxisID: 'y1',\n borderDash: [5, 5],\n fill: false,\n tension: 0.4,\n pointRadius: 0,\n pointHoverRadius: 4\n }]\n },\n options: {\n responsive: true,\n maintainAspectRatio: false,\n interaction: {\n mode: 'index',\n intersect: false\n },\n plugins: {\n legend: {\n position: 'top',\n labels: {\n usePointStyle: true,\n padding: 15\n }\n },\n tooltip: {\n callbacks: {\n title: function(context) {\n const index = context[0].dataIndex;\n if (index < allItems.length) {\n return allItems[index].name;\n }\n return 'Element ' + (index + 1);\n }\n }\n }\n },\n scales: {\n x: {\n display: false\n },\n y: {\n type: 'linear',\n display: true,\n position: 'left',\n grid: {\n color: 'rgba(0, 0, 0, 0.05)'\n },\n ticks: {\n callback: function(value) {\n return value.toFixed(0) + 't';\n }\n },\n title: {\n display: true,\n text: 'Cumulative CO2 (tonnes)'\n }\n },\n y1: {\n type: 'linear',\n display: true,\n position: 'right',\n grid: {\n drawOnChartArea: false\n },\n ticks: {\n callback: function(value) {\n return value.toFixed(0) + '%';\n }\n },\n title: {\n display: true,\n text: 'Cumulative Percentage'\n }\n }\n }\n }\n });\n </script>\n</body>\n</html>`;\n\nreturn [{\n json: {\n html: html,\n reportType: 'mckinsey-accenture-professional',\n timestamp: new Date().toISOString()\n }\n}];"
},
"typeVersion": 2
},
{
"id": "8d2144e0-ea60-46c3-ab58-1f2e3a988d2d",
"name": "HTML to Binary",
"type": "n8n-nodes-base.code",
"position": [
352,
1552
],
"parameters": {
"jsCode": "\n// Convert HTML to binary for file output\nconst html = $input.first().json.html;\nconst fileName = `CO2_Analysis_Report_${new Date().toISOString().slice(0,10)}.html`;\n\n// Return with 'data' as the default binary field name\nreturn [{\n json: {\n fileName: fileName // Also pass filename in json for backup\n },\n binary: {\n data: {\n data: Buffer.from(html).toString('base64'),\n mimeType: 'text/html',\n fileName: fileName,\n fileExtension: 'html'\n }\n }\n}];"
},
"typeVersion": 2
},
{
"id": "b3a16d39-4c38-4c35-b810-1eeb3325694f",
"name": "메모",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1216,
464
],
"parameters": {
"width": 340,
"height": 116,
"content": "⭐ **If you find our tools helpful**, please consider **starring our repository** on [GitHub](https://github.com/datadrivenconstruction/cad2data-Revit-IFC-DWG-DGN-pipeline-with-conversion-validation-qto). \n\nYour support helps us improve and continue developing open solutions for the community!\n"
},
"typeVersion": 1
},
{
"id": "3089d1b0-7633-44c4-81ec-16b06cd61369",
"name": "Conversion Block1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-864,
-352
],
"parameters": {
"color": 5,
"width": 1064,
"height": 96,
"content": "# Carbon Footprint CO2 Estimator for Revit and IFC with AI (LLM)\nDataDrivenConstruction [GitHub](https://github.com/datadrivenconstruction/cad2data-Revit-IFC-DWG-DGN-pipeline-with-conversion-validation-qto)"
},
"typeVersion": 1
},
{
"id": "6cdf5726-dbbc-420b-9feb-c00a007e2a25",
"name": "OpenAI 채팅 모델",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
"position": [
304,
1264
],
"parameters": {
"model": {
"__rl": true,
"mode": "list",
"value": "gpt-3.5-turbo",
"cachedResultName": "gpt-3.5-turbo"
},
"options": {}
},
"credentials": {
"openAiApi": {
"id": "5SwKOx6OOukR6C0w",
"name": "OpenAi account n8n"
}
},
"typeVersion": 1.2
},
{
"id": "f16d5db4-57a8-49fa-b678-d69b45e15d9a",
"name": "xAI Grok Chat Model",
"type": "@n8n/n8n-nodes-langchain.lmChatXAiGrok",
"position": [
480,
1264
],
"parameters": {
"model": "grok-4-0709",
"options": {}
},
"credentials": {
"xAiApi": {
"id": "JKhw9fFrSig9QNQB",
"name": "xAi account"
}
},
"typeVersion": 1
},
{
"id": "ff1a82e6-2138-45f3-a51e-4ce140f718e1",
"name": "메모2",
"type": "n8n-nodes-base.stickyNote",
"position": [
-336,
-208
],
"parameters": {
"color": 4,
"height": 384,
"content": "## ⬇️ Only modify the variables here \n— everything else works automatically"
},
"typeVersion": 1
},
{
"id": "91417501-7993-4c62-bb22-095b30ce3c1a",
"name": "On the standard 3D View",
"type": "n8n-nodes-base.if",
"position": [
-256,
768
],
"parameters": {
"conditions": {
"boolean": [
{
"value1": "={{ $json['On the standard 3D View'] }}",
"value2": true
}
]
}
},
"typeVersion": 1
},
{
"id": "a607b5db-6257-4ba3-99b7-a31e5779e0ea",
"name": "Non-3D View Elements Output",
"type": "n8n-nodes-base.set",
"position": [
-80,
848
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "message",
"name": "message",
"type": "string",
"value": "Elements not visible in standard 3D view"
},
{
"id": "filtered_count",
"name": "filtered_count",
"type": "number",
"value": "={{ $input.all().length }}"
},
{
"id": "reason",
"name": "reason",
"type": "string",
"value": "Parameter 'On the standard 3D View' is not True"
}
]
}
},
"typeVersion": 3.4
}
],
"active": false,
"pinData": {},
"settings": {
"executionOrder": "v1"
},
"versionId": "9380188d-a427-41be-9690-38a99e3a7a6b",
"connections": {
"40d06cd0-e57e-4d9d-8557-a06f64126d43": {
"main": [
[
{
"node": "731a2f76-60b4-4b4c-8e56-465c0ea6ad5c",
"type": "main",
"index": 0
}
]
]
},
"8d2144e0-ea60-46c3-ab58-1f2e3a988d2d": {
"main": [
[
{
"node": "8d7c13c3-9044-45bc-9759-9baf1d175aa1",
"type": "main",
"index": 0
}
]
]
},
"Set Parameters": {
"main": [
[
{
"node": "62b7db30-b6f2-4608-bcd8-f866759bcf56",
"type": "main",
"index": 0
}
]
]
},
"62b7db30-b6f2-4608-bcd8-f866759bcf56": {
"main": [
[
{
"node": "40d06cd0-e57e-4d9d-8557-a06f64126d43",
"type": "main",
"index": 0
}
]
]
},
"AI Agent Enhanced": {
"main": [
[
{
"node": "32640ecd-b337-4745-bbe2-072e60163dcf",
"type": "main",
"index": 0
}
]
]
},
"a4d43d4c-ef26-4d96-b8df-88cdff94a9b3": {
"main": [
[
{
"node": "82b3f0eb-07d8-4a97-82a0-bbc38c7d040a",
"type": "main",
"index": 0
}
]
]
},
"4a82e1e2-4c87-4132-82f0-bfcacf07ca38": {
"main": [
[
{
"node": "4bf02c30-f53f-4345-a564-9f6b8f7f8a14",
"type": "main",
"index": 0
}
],
[
{
"node": "3b60c81d-ec57-4183-b6e0-03fdf6636739",
"type": "main",
"index": 0
}
]
]
},
"8d7c13c3-9044-45bc-9759-9baf1d175aa1": {
"main": [
[
{
"node": "e4851576-a8ff-4618-ba9c-cae2ab579d5c",
"type": "main",
"index": 0
}
]
]
},
"2c3a57c2-66c2-414a-82e0-8f0df26cbfc8": {
"main": [
[
{
"node": "1f3e2b55-6c9d-4466-801b-d8b31792a907",
"type": "main",
"index": 0
}
]
]
},
"4d379a3c-ab28-450b-a7a1-0eb28b706376": {
"main": [
[
{
"node": "a4d43d4c-ef26-4d96-b8df-88cdff94a9b3",
"type": "main",
"index": 0
}
]
]
},
"165b21cb-90a7-43e7-870c-036813bb60b6": {
"main": [
[
{
"node": "e1e918c3-c5a3-4382-878a-5cdf19c1fd48",
"type": "main",
"index": 0
}
]
]
},
"b2423ed4-6d42-4b31-a1c1-db9affbf720b": {
"main": [
[
{
"node": "36bb02f7-baba-48c7-aad7-4fe2b057e6be",
"type": "main",
"index": 0
}
]
]
},
"15da077e-8560-4bf9-b610-0199f66292f8": {
"main": [
[
{
"node": "e7df68eb-4420-48b5-8465-b80c49f0122f",
"type": "main",
"index": 0
}
]
]
},
"7ae11a97-c8f9-4554-a1ae-0fc22c5ef6f9": {
"main": [
[
{
"node": "b2423ed4-6d42-4b31-a1c1-db9affbf720b",
"type": "main",
"index": 0
}
]
]
},
"f16d5db4-57a8-49fa-b678-d69b45e15d9a": {
"ai_languageModel": [
[
{
"node": "AI Agent Enhanced",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"82b3f0eb-07d8-4a97-82a0-bbc38c7d040a": {
"main": [
[
{
"node": "165b21cb-90a7-43e7-870c-036813bb60b6",
"type": "main",
"index": 0
}
]
]
},
"319e5fdf-1b31-48c6-9a8f-006c03fc23dd": {
"main": [
[
{
"node": "8d2144e0-ea60-46c3-ab58-1f2e3a988d2d",
"type": "main",
"index": 0
}
]
]
},
"3e9cd8b6-3117-493b-97f7-c9d6a889e55d": {
"main": [
[
{
"node": "7ae11a97-c8f9-4554-a1ae-0fc22c5ef6f9",
"type": "main",
"index": 0
}
],
[
{
"node": "ffc937c8-4ca6-4963-a24f-114f45b5345b",
"type": "main",
"index": 0
}
]
]
},
"10caf6f7-ad6b-4de2-9e63-8b06b5609414": {
"main": [
[
{
"node": "435f2cb9-9ccb-4f32-ba95-6bb0126a73d2",
"type": "main",
"index": 0
}
]
]
},
"86512d4b-52b7-46ac-ac59-61ed748b3045": {
"main": [
[
{
"node": "ff967309-0ef4-4b49-abf1-b4c3ff6d48ac",
"type": "main",
"index": 0
}
]
]
},
"4bf02c30-f53f-4345-a564-9f6b8f7f8a14": {
"main": [
[
{
"node": "Merge - Continue workflow",
"type": "main",
"index": 0
}
]
]
},
"7bbf3d9d-4406-4fa3-9ca6-49eeaea551ff": {
"main": [
[
{
"node": "10caf6f7-ad6b-4de2-9e63-8b06b5609414",
"type": "main",
"index": 0
}
]
]
},
"ff967309-0ef4-4b49-abf1-b4c3ff6d48ac": {
"main": [
[
{
"node": "ceeaa6f3-781e-421f-bae1-c6ccb784ecbd",
"type": "main",
"index": 0
}
]
]
},
"c8ae79e4-3a12-4b9e-bcc2-129adf5ecf25": {
"main": [
[
{
"node": "60bcb5b5-2f61-4314-94a2-7d0bf53427fe",
"type": "main",
"index": 0
}
]
]
},
"3b60c81d-ec57-4183-b6e0-03fdf6636739": {
"main": [
[
{
"node": "3b014ecd-d104-4d18-8d78-260e45b9b61a",
"type": "main",
"index": 0
}
]
]
},
"91417501-7993-4c62-bb22-095b30ce3c1a": {
"main": [
[
{
"node": "86512d4b-52b7-46ac-ac59-61ed748b3045",
"type": "main",
"index": 0
}
],
[
{
"node": "a607b5db-6257-4ba3-99b7-a31e5779e0ea",
"type": "main",
"index": 0
}
]
]
},
"32640ecd-b337-4745-bbe2-072e60163dcf": {
"main": [
[
{
"node": "2c3a57c2-66c2-414a-82e0-8f0df26cbfc8",
"type": "main",
"index": 0
}
]
]
},
"36bb02f7-baba-48c7-aad7-4fe2b057e6be": {
"main": [
[
{
"node": "AI Agent Enhanced",
"type": "main",
"index": 0
}
]
]
},
"e7df68eb-4420-48b5-8465-b80c49f0122f": {
"main": [
[
{
"node": "319e5fdf-1b31-48c6-9a8f-006c03fc23dd",
"type": "main",
"index": 0
},
{
"node": "4d379a3c-ab28-450b-a7a1-0eb28b706376",
"type": "main",
"index": 0
}
]
]
},
"1f3e2b55-6c9d-4466-801b-d8b31792a907": {
"main": [
[
{
"node": "15da077e-8560-4bf9-b610-0199f66292f8",
"type": "main",
"index": 0
}
],
[
{
"node": "7ae11a97-c8f9-4554-a1ae-0fc22c5ef6f9",
"type": "main",
"index": 0
}
]
]
},
"731a2f76-60b4-4b4c-8e56-465c0ea6ad5c": {
"main": [
[
{
"node": "7bbf3d9d-4406-4fa3-9ca6-49eeaea551ff",
"type": "main",
"index": 0
}
]
]
},
"435f2cb9-9ccb-4f32-ba95-6bb0126a73d2": {
"main": [
[
{
"node": "91417501-7993-4c62-bb22-095b30ce3c1a",
"type": "main",
"index": 0
}
]
]
},
"Merge - Continue workflow": {
"main": [
[
{
"node": "Set Parameters",
"type": "main",
"index": 0
}
]
]
},
"Setup - Define file paths": {
"main": [
[
{
"node": "c8ae79e4-3a12-4b9e-bcc2-129adf5ecf25",
"type": "main",
"index": 0
}
]
]
},
"760e0a5b-86a1-4515-b5bb-58a88448eba5": {
"main": [
[
{
"node": "Merge - Continue workflow",
"type": "main",
"index": 1
}
]
]
},
"e4851576-a8ff-4618-ba9c-cae2ab579d5c": {
"main": [
[
{
"node": "7bd64a70-7bc3-47d3-9db8-c766aec135d6",
"type": "main",
"index": 0
}
]
]
},
"60bcb5b5-2f61-4314-94a2-7d0bf53427fe": {
"main": [
[
{
"node": "4a82e1e2-4c87-4132-82f0-bfcacf07ca38",
"type": "main",
"index": 0
}
]
]
},
"ceeaa6f3-781e-421f-bae1-c6ccb784ecbd": {
"main": [
[
{
"node": "3e9cd8b6-3117-493b-97f7-c9d6a889e55d",
"type": "main",
"index": 0
}
]
]
},
"3b014ecd-d104-4d18-8d78-260e45b9b61a": {
"main": [
[
{
"node": "760e0a5b-86a1-4515-b5bb-58a88448eba5",
"type": "main",
"index": 0
}
],
[
{
"node": "Set xlsx_filename after success",
"type": "main",
"index": 0
}
]
]
},
"Set xlsx_filename after success": {
"main": [
[
{
"node": "Merge - Continue workflow",
"type": "main",
"index": 1
}
]
]
},
"When clicking ‘Execute workflow’": {
"main": [
[
{
"node": "Setup - Define file paths",
"type": "main",
"index": 0
}
]
]
}
}
}자주 묻는 질문
이 워크플로우를 어떻게 사용하나요?
위의 JSON 구성 코드를 복사하여 n8n 인스턴스에서 새 워크플로우를 생성하고 "JSON에서 가져오기"를 선택한 후, 구성을 붙여넣고 필요에 따라 인증 설정을 수정하세요.
이 워크플로우는 어떤 시나리오에 적합한가요?
고급 - AI 요약, 멀티모달 AI
유료인가요?
이 워크플로우는 완전히 무료이며 직접 가져와 사용할 수 있습니다. 다만, 워크플로우에서 사용하는 타사 서비스(예: OpenAI API)는 사용자 직접 비용을 지불해야 할 수 있습니다.
관련 워크플로우 추천
n8n_6_ LLM을 사용하여 Revit와 IFC의 건설 비용 추정
GPT-4와 Claude를 사용하여 Revit/IFC 모델을 기반으로 건설 비용을 추정
If
Set
Code
+
If
Set
Code
55 노드Artem Boiko
AI 요약
n8n_3_CAD-BIM-批量변환器-管道
批量 CAD/BIM 파일을 XLSX/DAE로 변환, 검증과 보고서 포함
If
Set
Code
+
If
Set
Code
82 노드Artem Boiko
문서 추출
시각화 참조 라이브러리에서 n8n 노드를 탐색
可视化 참조 라이브러리에서 n8n 노드를 탐색
If
Ftp
Set
+
If
Ftp
Set
113 노드I versus AI
기타
완전한 B2B 판매 프로세스: Apollo 잠재 고객 생성, Mailgun 프로모션 및 AI 응답 관리
완전한 B2B 판매 프로세스: Apollo 잠재 고객 생성, Mailgun 확장 및 AI 응답 관리
If
Set
Code
+
If
Set
Code
116 노드Paul
콘텐츠 제작
YNAB 자동 예산
GPT-5-Mini로 YNAB 거래 자동 분류 및 Discord 알림 전송
If
Set
Merge
+
If
Set
Merge
29 노드spencer owen
AI 요약
고객에게 인보이스 자동 발송
OCR.Space, GPT-4 및 Google Drive에서 Gmail로 인보이스 처리 자동화
If
Set
Code
+
If
Set
Code
29 노드Michael Taleb
AI 요약
워크플로우 정보
난이도
고급
노드 수55
카테고리2
노드 유형16
저자
Artem Boiko
@datadrivenconstructionFounder DataDrivenConstruction.io | AEC Tech Consultant & Automation Expert | Bridging Software and Construction
외부 링크
n8n.io에서 보기 →
이 워크플로우 공유