Navisworks GUID in PowerBI for connecting clash data to model

Hi,

I am trying to connect the Navisworks clash report to the federated model published in Speckle via the Navisworks connector in PowerBI.

I would like to utilize the GUID field which Navisworks generates under the Item Panel to connect the model to the clash report. I can see that the GUID field is available in the Speckle web viewer if I navigate to the collection. However, it is not available as a property of the NavisworksObject nested below it.

When I import the model into PowerBI using the Speckle connector I only get the NavisworksObjects but not the collections. Is there a way to get the GUID from the collection in PowerBI?

I could use the element ID for the mapping instead but in scenarios where a revit link is placed multiple times in the federated model (Eg. a typical floor being repeated across a high rise. The Element ID field is unique to Revit… using the GUID gives me the option to use it for elements from other authoring tools such as Civil 3D which may be federated in Navisworks.

Collection Properties

In the clash report I recommend exporting the Element ID - The Navisworks connector aggregates this from first selectable object to the geometry object.

It is possible to make a series of self-joins in powerquery but using elementID works best.

Let me know if you’re using a source appended into Navisworks that don’t have this property.

Our new website hasn’t fully migrated all the tutorials we have, and sadly clash data in PowerBI is one of them. I can dig it out as source material and repost here tomorrow

Hi Jonathon

I am currently following the process you have suggested but in my opinion Element ID is not the best approach because it does not work with objects appended from other authoring platforms such as Civil 3D or Plant 3D.

If I have a link appended in Navisworks which is repeated multiple times such as a typical floor of a building then the Element ID will also be repeated multiple times.

I do have Civil 3D models appended in Navisworks and they will not have the Element ID property.

Is there any means to access the GUID from the collection in PowerBI?

I believe we’re filtering out all the collections if the loaded data contains DataObjects in Power BI. In this case, Navisworks publishes data objects so Collections are excluded from the end result. This is something we can enable on our end so you’d have access to collections but i’m more curious about how you are planning to use it? Are you going to use this field to filter colelctions? To color them?

Hi Muchahit,

I plan to export the GUID field as part of the clash report and use it to connect the Speckle model to the clash report to filter objects when clashes are selected.

Regards,
Ritesh

There’s a way, but it’s not exactly straightforward yet.

A bit of context on why the parent GUID isn’t available:

To make Power BI and other analysis workflows easier (which have become the main use case for Navisworks and Speckle), the connector defaults to publishing a flat list of all visible leaf nodes in the hierarchy.

This simplifies things for most reporting needs, but it omits any data stored on parent or grandparent nodes. To compensate, the connector merges properties from ancestors into the leaf node. However, not all properties get carried over. Specifically, the Navisworks GUID is one of those that is not transferred when flattening the hierarchy.

The good news is that you can enable Preserve Hierarchy in the publish settings. This includes all collection objects in the stream. From what I can see, it looks like you’re already doing this.

To work with this in Power BI, you can use the applicationId, which represents the full path to an object in the tree. Navisworks GUIDs are historically unstable and tend to change when files are refreshed or reappended. That’s one reason why previously resolved clashes sometimes reappear. The applicationId is more stable and predictable, so we rely on it as the unique identifier.

A common pattern: the applicationId of a parent (for example, a Floor collection) is usually the same as the child’s, just one digit shorter. You can create a custom column in Power BI that trims the last digit from the child’s applicationId, then self-join back onto the list to retrieve the parent. That allows you to recover the GUID and link it to your clash report.

As Bilal pointed out here, recent changes to the connector now filter out collection objects by default. This isn’t specific to Navisworks, but it does affect workflows that rely on parent-child joins. Everything is still retrievable with the correct settings and queries, although we may have made it feel a bit trickier than it used to be.

We’re actively exploring improvements, including:

  • Making it easier to work with hierarchy in Power BI
  • Improving property merging logic
  • Possibly supporting clash data publishing from Navisworks to Speckle in the future, so clash and model data can be analysed together using the same structure

At this point, a demo dashboard from me might be necessary after all. Watch this space — or feel free to nudge me if you need it sooner.

Hey @Ritesh_Redkar

I’m about to push a “hotfix” that doesn’t exactly fix anything per se, but adds a capability to the default flat list behaviour.

It turns out that some of my earlier lore was incorrect - the connector wasn’t properly merging special-case non-dictionary properties (specifically GUIDs) from ancestors to children. This hotfix addresses that gap, so you should be able to revert to not exporting the whole hierarchy and still have access to the ancestor GUIDs you need for your clash data connections.

Your dataset should be helpful again with the simpler flat structure!

Watch this space for an update.

2 Likes

Thanks Jonathon! Appreciate the proactive support and guidance from the Speckle team!

1 Like

Have you already worked out the clash report as a table source?

That was a somewhat painful experience earlier as I’m putting a demo together:

let
    // Load XML source
    Source = Xml.Document(File.Contents("C:\Users\jonat\Desktop\Snowdon.xml")),
    
    // Navigate through XML structure to get clash tests
    #"Expanded Level1" = Table.ExpandTableColumn(Source, "Value", {"Name", "Value"}, {"Level1_Name", "Level1_Value"}),
    #"Expanded Level2" = Table.ExpandTableColumn(#"Expanded Level1", "Level1_Value", {"Name", "Value"}, {"Level2_Name", "Level2_Value"}),
    #"Expanded Level3" = Table.ExpandTableColumn(#"Expanded Level2", "Level2_Value", {"Name", "Attributes", "Value"}, {"Level3_Name", "Level3_Attributes", "Level3_Value"}),
    
    // Filter to get individual clash tests
    ClashTests = Table.SelectRows(#"Expanded Level3", each ([Level2_Name] = "clashtests" and [Level3_Name] = "clashtest")),
    
    // Extract clash test attributes from the attributes table
    #"Added ClashTestName" = Table.AddColumn(ClashTests, "ClashTestName", each 
        try Table.SelectRows([Level3_Attributes], each [Name] = "name"){0}[Value] otherwise null
    ),
    
    // Get clash results and expand to clash groups
    #"Added ClashGroups" = Table.AddColumn(#"Added ClashTestName", "ClashGroups", each
        let
            ClashTestValueTable = try [Level3_Value] otherwise #table({},{})
        in
            try Table.SelectRows(ClashTestValueTable, each [Name] = "clashresults"){0}[Value] otherwise #table({},{})
    ),
    
    // Filter out tests with no results and expand clash groups
    #"Filtered Results" = Table.SelectRows(#"Added ClashGroups", each Table.RowCount([ClashGroups]) > 0),
    #"Expanded ClashGroups" = Table.ExpandTableColumn(#"Filtered Results", "ClashGroups", {"Name", "Attributes", "Value"}, {"GroupElement", "GroupAttributes", "GroupValue"}),
    
    // Extract clash group information
    #"Added GroupName" = Table.AddColumn(#"Expanded ClashGroups", "ClashGroupName", each 
        try Table.SelectRows([GroupAttributes], each [Name] = "name"){0}[Value] otherwise null
    ),
    #"Added GroupStatus" = Table.AddColumn(#"Added GroupName", "GroupStatus", each 
        try Table.SelectRows([GroupAttributes], each [Name] = "status"){0}[Value] otherwise null
    ),
    
    // Get individual clash results from within each group
    #"Added IndividualClashes" = Table.AddColumn(#"Added GroupStatus", "IndividualClashes", each
        let
            GroupValueTable = try [GroupValue] otherwise #table({},{})
        in
            try Table.SelectRows(GroupValueTable, each [Name] = "clashresults"){0}[Value] otherwise #table({},{})
    ),
    
    // Expand individual clash results
    #"Expanded IndividualClashes" = Table.ExpandTableColumn(#"Added IndividualClashes", "IndividualClashes", {"Name", "Attributes", "Value"}, {"ClashResultElement", "ClashResultAttributes", "ClashResultValue"}),
    
    // Extract individual clash result information
    #"Added ClashResultName" = Table.AddColumn(#"Expanded IndividualClashes", "ClashResultName", each 
        try Table.SelectRows([ClashResultAttributes], each [Name] = "name"){0}[Value] otherwise null
    ),
    #"Added ResultStatus" = Table.AddColumn(#"Added ClashResultName", "ResultStatus", each 
        try Table.SelectRows([ClashResultAttributes], each [Name] = "status"){0}[Value] otherwise null
    ),
    
    // Extract created date from individual clash result
    #"Added CreatedDate" = Table.AddColumn(#"Added ResultStatus", "CreatedDate", each
        let
            ClashResultValueTable = try [ClashResultValue] otherwise #table({},{})
        in
            try 
                let
                    CreatedDateRow = Table.SelectRows(ClashResultValueTable, each [Name] = "createddate"){0},
                    DateTable = CreatedDateRow[Value]{0},
                    DateAttributes = DateTable[Attributes],
                    Year = try Number.FromText(Table.SelectRows(DateAttributes, each [Name] = "year"){0}[Value]) otherwise 2025,
                    Month = try Number.FromText(Table.SelectRows(DateAttributes, each [Name] = "month"){0}[Value]) otherwise 1,
                    Day = try Number.FromText(Table.SelectRows(DateAttributes, each [Name] = "day"){0}[Value]) otherwise 1,
                    Hour = try Number.FromText(Table.SelectRows(DateAttributes, each [Name] = "hour"){0}[Value]) otherwise 0,
                    Minute = try Number.FromText(Table.SelectRows(DateAttributes, each [Name] = "minute"){0}[Value]) otherwise 0,
                    Second = try Number.FromText(Table.SelectRows(DateAttributes, each [Name] = "second"){0}[Value]) otherwise 0
                in
                    #datetime(Year, Month, Day, Hour, Minute, Second)
            otherwise null
    ),
    
    // Get clash objects from individual clash results
    #"Added ClashObjects" = Table.AddColumn(#"Added CreatedDate", "ClashObjects", each
        let
            ClashResultValueTable = try [ClashResultValue] otherwise #table({},{})
        in
            try Table.SelectRows(ClashResultValueTable, each [Name] = "clashobjects"){0}[Value] otherwise #table({},{})
    ),
    
    // Expand clash objects (this creates one row per clash object)
    #"Expanded ClashObjects" = Table.ExpandTableColumn(#"Added ClashObjects", "ClashObjects", {"Name", "Value"}, {"ObjectElement", "ObjectValue"}),
    
    // Add index to identify first vs second clash object
    #"Added Index" = Table.AddIndexColumn(#"Expanded ClashObjects", "ObjectIndex", 0, 1, Int64.Type),
    
    // Add Left/Right based on object position (first = Left, second = Right)
    #"Added LeftRight" = Table.AddColumn(#"Added Index", "LeftRight", each 
        let
            // Group by clash result to get object position within each clash
            ClashResultKey = [ClashTestName] & "|" & [ClashGroupName] & "|" & [ClashResultName],
            CurrentIndex = [ObjectIndex]
        in
            // For each clash result, alternate between Left and Right
            if Number.Mod(CurrentIndex, 2) = 0 then "Left" else "Right"
    ),
    
    // Extract Element ID
    #"Added ElementID" = Table.AddColumn(#"Added LeftRight", "ElementID", each
        let
            ObjectValueTable = try [ObjectValue] otherwise #table({},{})
        in
            try 
                let
                    ObjectAttrRow = Table.SelectRows(ObjectValueTable, each [Name] = "objectattribute"){0},
                    ObjectAttrTable = ObjectAttrRow[Value],
                    ValueRow = Table.SelectRows(ObjectAttrTable, each [Name] = "value"){0}
                in
                    ValueRow[Value]
            otherwise null
    ),
    
    // Extract Layer (direct value)
    #"Added Layer" = Table.AddColumn(#"Added ElementID", "Layer", each
        let
            ObjectValueTable = try [ObjectValue] otherwise #table({},{})
        in
            try Table.SelectRows(ObjectValueTable, each [Name] = "layer"){0}[Value] otherwise null
    ),
    
    // Extract all smart tags as separate columns
    #"Added ItemName" = Table.AddColumn(#"Added Layer", "ItemName", each
        let
            ObjectValueTable = try [ObjectValue] otherwise #table({},{})
        in
            try 
                let
                    SmartTagsRow = Table.SelectRows(ObjectValueTable, each [Name] = "smarttags"){0},
                    SmartTagsTable = SmartTagsRow[Value],
                    ItemNameTag = List.Select(Table.ToRecords(SmartTagsTable), each 
                        let
                            TagTable = [Value],
                            NameRow = Table.SelectRows(TagTable, each [Name] = "name"){0}
                        in
                            NameRow[Value] = "Item Name"
                    ){0},
                    ValueRow = Table.SelectRows(ItemNameTag[Value], each [Name] = "value"){0}
                in
                    ValueRow[Value]
            otherwise null
    ),
    
    #"Added ItemGUID" = Table.AddColumn(#"Added ItemName", "ItemGUID", each
        let
            ObjectValueTable = try [ObjectValue] otherwise #table({},{})
        in
            try 
                let
                    SmartTagsRow = Table.SelectRows(ObjectValueTable, each [Name] = "smarttags"){0},
                    SmartTagsTable = SmartTagsRow[Value],
                    ItemGUIDTag = List.Select(Table.ToRecords(SmartTagsTable), each 
                        let
                            TagTable = [Value],
                            NameRow = Table.SelectRows(TagTable, each [Name] = "name"){0}
                        in
                            NameRow[Value] = "Item GUID"
                    ){0},
                    ValueRow = Table.SelectRows(ItemGUIDTag[Value], each [Name] = "value"){0}
                in
                    ValueRow[Value]
            otherwise null
    ),
    
    #"Added SourceFileName" = Table.AddColumn(#"Added ItemGUID", "SourceFileName", each
        let
            ObjectValueTable = try [ObjectValue] otherwise #table({},{})
        in
            try 
                let
                    SmartTagsRow = Table.SelectRows(ObjectValueTable, each [Name] = "smarttags"){0},
                    SmartTagsTable = SmartTagsRow[Value],
                    SourceFileTag = List.Select(Table.ToRecords(SmartTagsTable), each 
                        let
                            TagTable = [Value],
                            NameRow = Table.SelectRows(TagTable, each [Name] = "name"){0}
                        in
                            NameRow[Value] = "Item Source File Name"
                    ){0},
                    ValueRow = Table.SelectRows(SourceFileTag[Value], each [Name] = "value"){0}
                in
                    ValueRow[Value]
            otherwise null
    ),
    
    // Select final columns - one row per clash object
    #"Selected Columns" = Table.SelectColumns(#"Added SourceFileName", {
        "ClashTestName", 
        "ClashGroupName", 
        "ClashResultName",
        "ResultStatus", 
        "CreatedDate",
        "ElementID", 
        "Layer",
        "ItemName", 
        "ItemGUID", 
        "SourceFileName",
        "LeftRight"
    }),
    
    // Clean up data types
    #"Changed Types" = Table.TransformColumnTypes(#"Selected Columns", {
        {"ClashTestName", type text},
        {"ClashGroupName", type text},
        {"ClashResultName", type text},
        {"ResultStatus", type text},
        {"CreatedDate", type datetime},
        {"ElementID", type text},
        {"Layer", type text},
        {"ItemName", type text},
        {"ItemGUID", type text},
        {"SourceFileName", type text}
    }),
    #"Renamed Columns" = Table.RenameColumns(#"Changed Types",{{"ItemGUID", "GUID"}})
    
in
    #"Renamed Columns"
2 Likes

Thanks a lot for providing this script. It has been challenging to parse the XML from the clash report in PowerQuery.

@Jhacin_Gnanadavid

2 Likes

A low-priority change to the Navisworks Connector will be to send from the Clash Manager. I’m currently scoping out what this looks like.

1 Like

That would be brilliant! The data from the clash manager is the key to multi-disciplinary co-ordination and typically it is only available to the BIM Managers / Leads.

If we could surface the data to make it more widely accessible across the project team it would streamline co-ordination.