{
  "id": "40UdEGFo6xcsiP60",
  "name": "n8n_4_Validation_CAD_BIM_Revit_IFC_DWG",
  "tags": [],
  "nodes": [
    {
      "id": "4b870d62-e3be-4c76-9d71-43aef3a21d44",
      "name": "Start - Click to begin",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [
        560,
        272
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "ab1dbae1-42fe-4123-ac17-ecf410e62c72",
      "name": "Setup - Define file paths",
      "type": "n8n-nodes-base.set",
      "position": [
        800,
        272
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "9cbd4ec9-df24-41e8-b47a-720a4cdb733b",
              "name": "path_to_converter",
              "type": "string",
              "value": "C:\\Users\\Artem Boiko\\Documents\\GitHub\\cad2data-Revit-IFC-DWG-DGN-pipeline-with-conversion-validation-qto\\DDC_Converter_Revit\\RvtExporter.exe"
            },
            {
              "id": "aa834467-80fb-476a-bac1-6728478834f0",
              "name": "project_file",
              "type": "string",
              "value": "C:\\Users\\Artem Boiko\\Desktop\\n8n\\cad2data-Revit-IFC-DWG-DGN-pipeline-with-conversion-validation-qto-main\\cad2data-Revit-IFC-DWG-DGN-pipeline-with-conversion-validation-qto-main\\Sample_Projects\\2023 racbasicsampleproject.rvt"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "dea38ca4-33e5-43a8-bede-e5ff37f4116c",
      "name": "Sticky Note - Start",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        528,
        48
      ],
      "parameters": {
        "width": 190,
        "height": 444,
        "content": "## \ud83d\ude80 START WORKFLOW\n\nClick \"Execute Workflow\" button to begin the validation process"
      },
      "typeVersion": 1
    },
    {
      "id": "46bd811d-00c9-4c14-b622-4981dcf8cca0",
      "name": "Read Project Excel File",
      "type": "n8n-nodes-base.readBinaryFile",
      "position": [
        2400,
        384
      ],
      "parameters": {
        "filePath": "={{ $json[\"xlsx_filename\"] }}"
      },
      "typeVersion": 1
    },
    {
      "id": "4f191ee6-712a-4bf2-a2ee-6332716ffdec",
      "name": "Set Validation Rules Path",
      "type": "n8n-nodes-base.set",
      "position": [
        2400,
        208
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "validation-rules-path-id",
              "name": "validation_rules_path",
              "type": "string",
              "value": "C:\\Users\\Artem Boiko\\Desktop\\n8n\\cad2data-Revit-IFC-DWG-DGN-pipeline-with-conversion-validation-qto-main\\cad2data-Revit-IFC-DWG-DGN-pipeline-with-conversion-validation-qto-main\\n8n_3_DDC_BIM_Requirements_Table_for_Revit_IFC_DWG.xlsx"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "69a54929-3ec3-45d1-98fc-8a74072a8026",
      "name": "Sticky Note - Validation Rules",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2320,
        -144
      ],
      "parameters": {
        "color": 4,
        "width": 220,
        "height": 680,
        "content": "## \ud83d\udccb VALIDATION RULES\n\n**Specify the path to rules file:**\n`validation_rules_path`\n\nThis file contains validation criteria"
      },
      "typeVersion": 1
    },
    {
      "id": "d731282b-9ac8-4dc4-a53e-e41176a9cf2e",
      "name": "Read Validation Rules File",
      "type": "n8n-nodes-base.readBinaryFile",
      "position": [
        2640,
        288
      ],
      "parameters": {
        "filePath": "={{ $json[\"validation_rules_path\"] }}"
      },
      "typeVersion": 1
    },
    {
      "id": "ba8aeb4b-cca4-44e7-bd9e-43ce2fad7024",
      "name": "Merge Excel Files",
      "type": "n8n-nodes-base.merge",
      "position": [
        2864,
        400
      ],
      "parameters": {},
      "typeVersion": 3
    },
    {
      "id": "20a8b8e1-6153-4f99-85fe-5085fac971bd",
      "name": "Validate - Enhanced",
      "type": "n8n-nodes-base.code",
      "position": [
        3040,
        288
      ],
      "parameters": {
        "language": "python",
        "pythonCode": "def _disable_n8n_mitigations():\n    import sys\n\n    # n8n 1.99: blocked `os` module: https://github.com/n8n-io/n8n/pull/15970\n    sys.meta_path = [finder for finder in sys.meta_path if \"ImportBlocker\" not in str(finder)]\n\n    # n8n 1.102: blocked `js` module: https://github.com/n8n-io/n8n/pull/16957\n    import js\n    sys.modules.pop(\"js\", None)\n\n\n_disable_n8n_mitigations()\n\nimport micropip\nawait micropip.install(\"openpyxl\")\nfrom io import BytesIO\nimport pandas as pd\nimport datetime\nimport base64\nimport os\nfrom openpyxl import load_workbook\nfrom openpyxl.styles import Alignment, PatternFill\n\n# Get the uploaded Excel files\ninput_data = _input.all()\n\n# Get project Excel data (from Read Project Excel File node)\nproject_binary = input_data[0][\"binary\"]\nproject_b64_excel = project_binary[\"data\"][\"data\"]\nproject_excel_bytes = BytesIO(base64.b64decode(project_b64_excel))\n\n# Get validation rules Excel data (from Read Validation Rules File node)\nvalidation_binary = input_data[1][\"binary\"]\nvalidation_b64_excel = validation_binary[\"data\"][\"data\"]\nvalidation_excel_bytes = BytesIO(base64.b64decode(validation_b64_excel))\n\n# Get the validation rules path from the previous node to determine output directory\nvalidation_rules_path = input_data[1][\"json\"][\"validation_rules_path\"]\noutput_directory = os.path.dirname(validation_rules_path)\nprint(f\"Output directory: {output_directory}\")\n\n# Load project data\ndf = pd.read_excel(project_excel_bytes)\nprint(f\"Loaded {len(df)} rows from project file\")\n\n# Handle duplicate columns\nduplicate_cols = df.columns[df.columns.duplicated(keep=False)].unique().tolist()\nif duplicate_cols:\n    print(f\"WARNING: Duplicate columns found: {duplicate_cols}\")\n\n# Clean column names\ncolumn_mapping = {col: col.split(' : ')[0] if ':' in col else col for col in df.columns}\ncleaned_columns = list(column_mapping.values())\n\n# Load and prepare workbook\nwb = load_workbook(validation_excel_bytes)\nws = wb.active\n\n# Unmerge cells in columns D-G\nfor mr in list(ws.merged_cells.ranges):\n    if any(col in range(mr.min_col, mr.max_col + 1) for col in [4, 5, 6, 7]):\n        print(f\"Unmerging: {mr}\")\n        ws.unmerge_cells(str(mr))\n\n# Get category column\ncategory_col = ws['B2'].value\nprint(f\"Category column: {category_col}\")\n\n# Find category column in data\nif category_col not in df.columns:\n    for orig, cleaned in column_mapping.items():\n        if cleaned == category_col:\n            category_col = orig\n            break\n    if category_col not in df.columns:\n        raise ValueError(f\"Category column '{category_col}' not found\")\n\nprint(f\"Data type: {df[category_col].dtype}, Unique values: {df[category_col].nunique()}\")\n\n# Read validation rules from the Excel\nvalidation_excel_bytes.seek(0)  # Reset the stream\ndf_excel = pd.read_excel(validation_excel_bytes)\nvalid_mask = df_excel.iloc[1:, 1].notna() & df_excel.iloc[1:, 2].notna()\nvalidation_data = pd.DataFrame({\n    'row': df_excel.index[1:][valid_mask] + 2,\n    'group': df_excel.iloc[1:, 1][valid_mask].values,\n    'param': df_excel.iloc[1:, 2][valid_mask].values\n})\n\n# Define fills\nred_fill = PatternFill(start_color='FFCCCC', end_color='FFCCCC', fill_type='solid')\ngreen_fill = PatternFill(start_color='CCFFCC', end_color='CCFFCC', fill_type='solid')\n\nprint(f\"Processing {len(validation_data)} rules...\")\n\n# Helper function\ndef get_best_column(df, param, group_df):\n    matches = [i for i, col in enumerate(df.columns) if cleaned_columns[i] == param]\n    \n    if not matches:\n        return param if param in df.columns else None\n    \n    if len(matches) == 1:\n        return df.columns[matches[0]]\n    \n    print(f\"  Found {len(matches)} columns named '{param}'\")\n    best_idx, best_count = matches[0], -1\n    \n    for idx in matches:\n        col = df.columns[idx]\n        if col in group_df.columns:\n            count = group_df[col].notna().sum()\n            print(f\"    Column {idx}: {count} values\")\n            if count > best_count:\n                best_idx, best_count = idx, count\n    \n    return df.columns[best_idx]\n\n# Process validation rules\nfor _, row in validation_data.iterrows():\n    r, grp, prm = row['row'], str(row['group']).strip(), str(row['param']).strip()\n    \n    try:\n        # Filter by category\n        cat_df = df[df[category_col] == grp]\n        \n        # Try alternative matching if no exact match\n        if len(cat_df) == 0 and ':' in grp:\n            cat_df = df[df[category_col].astype(str).str.contains(grp, na=False, regex=False)]\n        \n        total = len(cat_df)\n        \n        # Get column and calculate metrics\n        col = get_best_column(df, prm, cat_df)\n        \n        if col and col in cat_df.columns:\n            valid_df = cat_df[cat_df[col].notna() & ~cat_df[col].isin([0, 0.0, \"0\", \"0.0\", \"0,0\"])]\n            filled = len(valid_df)\n            fill_pct = filled / total if total > 0 else 0\n            unique_cnt = valid_df[col].nunique()\n            unique_vals = valid_df[col].unique()[:10]\n            unique_str = \", \".join(map(str, unique_vals))\n            if len(unique_vals) > 10:\n                unique_str += f\"... ({valid_df[col].nunique()} total)\"\n        else:\n            filled = fill_pct = unique_cnt = 0\n            unique_str = \"\"\n        \n        # Write to Excel\n        ws[f'D{r}'] = total\n        ws[f'D{r}'].alignment = Alignment(horizontal='center')\n        \n        ws[f'E{r}'].value = fill_pct\n        ws[f'E{r}'].number_format = '0.00%'\n        ws[f'E{r}'].alignment = Alignment(horizontal='center')\n        ws[f'E{r}'].fill = green_fill if fill_pct > 0 else red_fill\n        \n        ws[f'F{r}'] = unique_cnt\n        ws[f'F{r}'].alignment = Alignment(horizontal='center')\n        \n        ws[f'G{r}'] = unique_str\n        ws[f'G{r}'].alignment = Alignment(horizontal='left')\n        \n    except Exception as e:\n        print(f\"  ERROR: {type(e).__name__}: {str(e)}\")\n        ws[f'D{r}'] = 0\n        ws[f'E{r}'].value = 0\n        ws[f'E{r}'].fill = red_fill\n        ws[f'F{r}'] = 0\n        ws[f'G{r}'] = f\"ERROR: {str(e)}\"\n\n# Save to bytes & return to n8n\nwith BytesIO() as out_stream:\n    wb.save(out_stream)\n    out_stream.seek(0)\n    result_b64 = base64.b64encode(out_stream.read()).decode()\n\noutput_timestamped = f\"DDC_Revit_IFC_Validation__{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx\"\n\n# Use the same directory as the validation rules file\nfull_output_path = os.path.join(output_directory, output_timestamped)\n\nreturn [{\n    \"json\": {\n        \"filename\": output_timestamped,\n        \"full_path\": full_output_path\n    },\n    \"binary\": {\n        \"data\": {\n            \"data\":          result_b64,\n            \"fileName\":      output_timestamped,\n            \"fileExtension\": \"xlsx\",\n            \"mimeType\":      \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\"\n        }\n    }\n}]\n"
      },
      "typeVersion": 2
    },
    {
      "id": "1d601482-2b90-4d52-91b7-4e7579a88db2",
      "name": "Sticky Note - Validation",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2560,
        48
      ],
      "parameters": {
        "width": 620,
        "height": 596,
        "content": "## \ud83e\udd16 AUTOMATED VALIDATION\n\nProcessing includes:\n- Project data analysis\n- Rules-based checking\n- Report creation with color coding\n\n\ud83d\udfe9 Green = data present\n\ud83d\udfe5 Red = data missing"
      },
      "typeVersion": 1
    },
    {
      "id": "fde712a8-5bb1-4604-bc51-0c635fbc4c0a",
      "name": "Sticky Note - Conversion",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        976,
        48
      ],
      "parameters": {
        "width": 1320,
        "height": 596,
        "content": "## \ud83d\udd04 CONVERSION\n\nAutomatic conversion Project files \u2192 Excel\n\nIf file already exists, conversion is skipped"
      },
      "typeVersion": 1
    },
    {
      "id": "8c98aae6-074a-4701-99d9-7c49ae13e682",
      "name": "Sticky Note - Save & Open",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3200,
        48
      ],
      "parameters": {
        "color": 6,
        "width": 420,
        "height": 596,
        "content": "## \ud83d\udcc2 SAVE & OPEN\n\n\u2705 Report saved automatically\n\u2705 Excel opens for viewing\n\nFilename includes date and time"
      },
      "typeVersion": 1
    },
    {
      "id": "6fce729b-f210-407e-9de2-81a193715ca5",
      "name": "Save Validation Report1",
      "type": "n8n-nodes-base.writeBinaryFile",
      "position": [
        3280,
        288
      ],
      "parameters": {
        "options": {},
        "fileName": "={{ $json.full_path }}"
      },
      "typeVersion": 1
    },
    {
      "id": "8d09f3bc-8478-48a5-a235-460597226737",
      "name": "Open Excel Report1",
      "type": "n8n-nodes-base.executeCommand",
      "position": [
        3488,
        288
      ],
      "parameters": {
        "command": "=start \"\" \"{{ $json.full_path }}\""
      },
      "typeVersion": 1
    },
    {
      "id": "c70924fa-ecbc-48bb-b66c-98afe5947994",
      "name": "Create - Excel filename",
      "type": "n8n-nodes-base.set",
      "position": [
        1024,
        272
      ],
      "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": "2f4617b8-f146-4fd6-bee2-020a8609a92c",
      "name": "Check - Does Excel file exist?",
      "type": "n8n-nodes-base.readBinaryFile",
      "position": [
        1216,
        272
      ],
      "parameters": {
        "filePath": "={{ $json[\"xlsx_filename\"] }}"
      },
      "typeVersion": 1,
      "continueOnFail": true,
      "alwaysOutputData": true
    },
    {
      "id": "aa925149-5394-4db4-bdc2-9116f9b76700",
      "name": "If - File exists?",
      "type": "n8n-nodes-base.if",
      "position": [
        1376,
        272
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "e7fb1577-e753-43f5-9f5a-4d5285aeb96e",
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "leftValue": "={{ $binary.data ? true : false }}",
              "rightValue": "={{ true }}"
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "18206815-fe18-4e53-970d-85d7dda0d8c0",
      "name": "Extract - Run converter",
      "type": "n8n-nodes-base.executeCommand",
      "position": [
        1600,
        368
      ],
      "parameters": {
        "command": "=\"{{$node[\"Setup - Define file paths\"].json[\"path_to_converter\"]}}\" \"{{$node[\"Setup - Define file paths\"].json[\"project_file\"]}}\""
      },
      "typeVersion": 1,
      "continueOnFail": true
    },
    {
      "id": "c701d878-85d1-4122-b5cb-59c1edc2f835",
      "name": "Info - Skip conversion",
      "type": "n8n-nodes-base.set",
      "position": [
        1680,
        192
      ],
      "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": "026e9414-30a8-4e99-bb9f-c91cddb8dbce",
      "name": "Check - Did extraction succeed?",
      "type": "n8n-nodes-base.if",
      "position": [
        1760,
        368
      ],
      "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": "362d1574-f7e0-4c7c-a0f2-27496d62cf16",
      "name": "Error - Show what went wrong",
      "type": "n8n-nodes-base.set",
      "position": [
        1920,
        288
      ],
      "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": "c8f21f46-0647-4819-ae0e-419d9a46f206",
      "name": "Set xlsx_filename after success",
      "type": "n8n-nodes-base.set",
      "position": [
        1920,
        448
      ],
      "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": "b502d5d2-8530-427e-9390-7c11012bd06a",
      "name": "Merge - Continue workflow",
      "type": "n8n-nodes-base.merge",
      "position": [
        2160,
        208
      ],
      "parameters": {},
      "typeVersion": 3
    },
    {
      "id": "b236040c-8651-41dd-a6b1-63da8db3d4cd",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        976,
        672
      ],
      "parameters": {
        "width": 540,
        "height": 300,
        "content": "## \u26a0\ufe0f Troubleshooting\nIf you encounter errors during conversion, be sure to reference the executable inside the **`datadrivenlibs`** folder. Use this path:\n\n```text\n\"DDC_Exporter_XXXXXXX\\datadrivenlibs\\RvtExporter.exe\"\n```\ninstead of:\n```text\n\"DDC_Exporter_XXXXXXX\\RvtExporter.exe\"\n```"
      },
      "typeVersion": 1
    },
    {
      "id": "cae2d3e4-07de-45c9-9b45-da19789cabb4",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2576,
        672
      ],
      "parameters": {
        "width": 620,
        "height": 268,
        "content": "## \u26a0\ufe0f Troubleshooting\nIn n8n versions 1.98.0\u20131.101.x, the Python Code node (Pyodide) completely blocks the `os` module, causing this error:\n\n### \u2705 Solution  \nTo avoid this error, **use latest n8n version 1.97** where the `os` module is not blocked.  \nYou can launch it with the following command:\n\n```bash\nnpx n8n@latest\n```"
      },
      "typeVersion": 1
    },
    {
      "id": "079841ea-bc60-47c7-ab3c-a7e9d3e59d2c",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3280,
        -80
      ],
      "parameters": {
        "width": 340,
        "height": 116,
        "content": "\u2b50 **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": "389526e6-34a8-4cb2-bb68-c9cfd28ed566",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        736,
        -288
      ],
      "parameters": {
        "color": 4,
        "width": 224,
        "height": 176,
        "content": "## \u2b07\ufe0f Only modify the variables here  \n\u2014 everything else works automatically"
      },
      "typeVersion": 1
    },
    {
      "id": "c41d11fb-f654-49e9-a2f0-ac81a01eff02",
      "name": "Sticky Note - Settings",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        736,
        -96
      ],
      "parameters": {
        "color": 4,
        "width": 220,
        "height": 628,
        "content": "## \u2699\ufe0f SETTINGS\n\n**Update file paths here:**\n- `path_to_converter` - path to converter\n- `project_file` - path to your project file (IFC/DWG/RVT)"
      },
      "typeVersion": 1
    },
    {
      "id": "730b1426-7b20-4eb0-8f1e-b70ae0bd9fed",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2320,
        -336
      ],
      "parameters": {
        "color": 4,
        "width": 224,
        "height": 176,
        "content": "## \u2b07\ufe0f Only modify the variables here  \n\u2014 everything else works automatically"
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "3f498ad6-c521-472a-a619-d6b1ccc418e5",
  "connections": {
    "If - File exists?": {
      "main": [
        [
          {
            "node": "Info - Skip conversion",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Extract - Run converter",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge Excel Files": {
      "main": [
        [
          {
            "node": "Validate - Enhanced",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate - Enhanced": {
      "main": [
        [
          {
            "node": "Save Validation Report1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Info - Skip conversion": {
      "main": [
        [
          {
            "node": "Merge - Continue workflow",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Start - Click to begin": {
      "main": [
        [
          {
            "node": "Setup - Define file paths",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create - Excel filename": {
      "main": [
        [
          {
            "node": "Check - Does Excel file exist?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract - Run converter": {
      "main": [
        [
          {
            "node": "Check - Did extraction succeed?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Project Excel File": {
      "main": [
        [
          {
            "node": "Merge Excel Files",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save Validation Report1": {
      "main": [
        [
          {
            "node": "Open Excel Report1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge - Continue workflow": {
      "main": [
        [
          {
            "node": "Read Project Excel File",
            "type": "main",
            "index": 0
          },
          {
            "node": "Set Validation Rules Path",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Validation Rules Path": {
      "main": [
        [
          {
            "node": "Read Validation Rules File",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Setup - Define file paths": {
      "main": [
        [
          {
            "node": "Create - Excel filename",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Validation Rules File": {
      "main": [
        [
          {
            "node": "Merge Excel Files",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Error - Show what went wrong": {
      "main": [
        [
          {
            "node": "Merge - Continue workflow",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Check - Does Excel file exist?": {
      "main": [
        [
          {
            "node": "If - File exists?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check - Did extraction succeed?": {
      "main": [
        [
          {
            "node": "Error - Show what went wrong",
            "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
          }
        ]
      ]
    }
  }
}