This workflow corresponds to n8n.io template #7653 — we link there as the canonical source.
This workflow follows the Agent → Anthropic Chat recipe pattern — see all workflows that pair these two integrations.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"id": "UgJXCTdg8rea9Skt",
"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// \u0418\u0449\u0435\u043c \u043f\u043e\u043b\u0435 \u043a\u0430\u0442\u0435\u0433\u043e\u0440\u0438\u0438\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 // \u041f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u0441\u0447\u0438\u0442\u0430\u0435\u043c \u0441\u0442\u0440\u043e\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u043c \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u043c \u0435\u0441\u043b\u0438 \u043d\u0435 \u043e\u0447\u0435\u0432\u0438\u0434\u043d\u043e \u043e\u0431\u0440\u0430\u0442\u043d\u043e\u0435\n isBuildingElement = true;\n confidence = 70;\n reason = `Category '${categoryValue}' assumed as building element (no clear match)`;\n }\n }\n } else {\n // \u0415\u0441\u043b\u0438 \u043d\u0435\u0442 \u043a\u0430\u0442\u0435\u0433\u043e\u0440\u0438\u0438, \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0435\u043c \u043d\u0430\u043b\u0438\u0447\u0438\u0435 \u043e\u0431\u044a\u0435\u043c\u043d\u044b\u0445 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432\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": {
"name": "<your credential>"
}
},
"typeVersion": 1.3
},
{
"id": "10caf6f7-ad6b-4de2-9e63-8b06b5609414",
"name": "Process AI Response1",
"type": "n8n-nodes-base.code",
"position": [
592,
496
],
"parameters": {
"jsCode": "// \u041e\u0431\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0435\u043c \u043e\u0442\u0432\u0435\u0442 AI \u0438 \u043f\u0440\u0438\u043c\u0435\u043d\u044f\u0435\u043c \u043f\u0440\u0430\u0432\u0438\u043b\u0430\n const aiResponse = $input.first().json;\n const headerData = $node['Extract Headers and Data1'].json;\n\n// \u0418\u0437\u0432\u043b\u0435\u043a\u0430\u0435\u043c \u043f\u0440\u0430\u0432\u0438\u043b\u0430 \u0438\u0437 AI \u043e\u0442\u0432\u0435\u0442\u0430\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 // \u0418\u0449\u0435\u043c JSON \u0432 \u043e\u0442\u0432\u0435\u0442\u0435\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// \u0421\u043e\u0437\u0434\u0430\u0435\u043c \u0444\u0438\u043d\u0430\u043b\u044c\u043d\u044b\u0435 \u043f\u0440\u0430\u0432\u0438\u043b\u0430 \u0441 \u0434\u0435\u0444\u043e\u043b\u0442\u0430\u043c\u0438\n const finalRules = {};\n headerData.headers.forEach(header => {\n if (aiRules[header]) {\n finalRules[header] = aiRules[header];\n } else {\n // \u041f\u0440\u0438\u043c\u0435\u043d\u044f\u0435\u043c \u043f\u0440\u0430\u0432\u0438\u043b\u0430 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e\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// \u0412\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u043c \u0432\u0441\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0434\u043b\u044f \u0433\u0440\u0443\u043f\u043f\u0438\u0440\u043e\u0432\u043a\u0438\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 // \u041e\u043a\u0440\u0443\u0433\u043b\u044f\u0435\u043c \u0434\u043e 2 \u0437\u043d\u0430\u043a\u043e\u0432 \u043f\u043e\u0441\u043b\u0435 \u0437\u0430\u043f\u044f\u0442\u043e\u0439 \u0435\u0441\u043b\u0438 \u043d\u0443\u0436\u043d\u043e\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// \u0421\u043e\u0440\u0442\u0438\u0440\u0443\u0435\u043c \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\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": {
"name": "<your credential>"
}
},
"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 Agent 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 Chat Model1",
"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": {
"name": "<your credential>"
}
},
"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\u00b3)']) || 0;\n const area = parseFloat(data['Area (m\u00b2)']) || 0;\n const material = data['Material (EU Standard)'] || 'Unknown';\n const category = data['Element Category'] || 'Unknown';\n const impact = data['Impact Category'] || 'Unknown';\n \n // Update totals\n projectTotals.totalElements += elementCount;\n projectTotals.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\u00b3)': projectTotals.totalVolume.toFixed(2),\n 'Project Total Area (m\u00b2)': 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': '\u2713',\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': '\u2713',\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 ? '\u2713' : '\u26a0',\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 ? '\u2713' : '\u26a0',\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': '\u2713',\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': '\u2713',\n 'Notes': 'Distinct material classifications'\n },\n {\n 'Category': 'VOLUMETRIC DATA',\n 'Metric': 'Total Volume',\n 'Value': formatNumber(projectTotals.totalVolume, 2),\n 'Unit': 'm\u00b3',\n 'Benchmark': 'N/A',\n 'Status': '\u2713',\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\u00b2',\n 'Benchmark': 'N/A',\n 'Status': '\u2713',\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\u00b3)'] || 0, 3),\n 'Area_m2': formatNumber(data['Area (m\u00b2)'] || 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\u00b3)'] || 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)**: YOUR_AWS_SECRET_KEY_HERE.\n- **German (\u00d6KOBAUDAT)**: Mineralische/Metalle/Holz/D\u00e4mmstoffe/etc.\n- **US (MasterFormat)**: Division classifications\n\n### 3. Quantity Analysis\n- Primary quantity with unit (m\u00b3, m\u00b2, m, kg, pieces) - use aggregated total from input\n- Calculation method used (e.g., 'Direct from provided total volume')\n- Confidence level (0-100%)\n- Data quality assessment\n\n### 4. CO2 Emissions Calculation\n- Material density (kg/m\u00b3)\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\u00b2)': analysis.quantities?.raw_dimensions?.area || originalData['Area'] || 0,\n 'Volume (m\u00b3)': analysis.quantities?.raw_dimensions?.volume || originalData['Volume'] || 0,\n \n // CO2 Analysis\n 'Material Density (kg/m\u00b3)': 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": "Setup - 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"
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
anthropicApiopenAiApixAiApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Estimate embodied carbon (CO2e) for grouped BIM/CAD elements. The workflow accepts an existing XLSX (grouped element data) or, if missing, can trigger a local RvtExporter.exe to generate one. It detects category fields, filters out non-building elements, infers aggregation rules…
Source: https://n8n.io/workflows/7653/ — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
Estimate material price and total cost for grouped BIM/CAD elements using an LLM-driven analysis. The workflow accepts an existing XLSX (from your model) or, if missing, can trigger a local RvtExporte
This workflow is for beauty salons who want consistent, high‑quality social media content without writing every post manually. It also suits agencies and automation builders who manage multiple beauty
This workflow is designed for marketers, content creators, agencies, and solo founders who want to publish long‑form posts with visuals on autopilot using n8n and AI agents.
Who Is This For?
Episode 15: Startup ideas for YC RFS. Uses lmChatOpenAi, googleSheets, agent, informationExtractor. Event-driven trigger; 33 nodes.