​If you’ve spent any time managing BIM information for a COBie delivery, you’ll know that the standard tools don’t always play ball. Whether it’s the way Revit concatenates system family names or the struggle to map components to rooms across linked models, there’s often a gap between what the software provides and what the EIR (Exchange Information Requirements) demands.

​Recently, while working on a major update in Revit 2025, I hit a familiar wall—literally. When setting up the COBie Extension, the “Family and Type” setting worked perfectly for loadable families like doors and FFE. However, for system families like walls and ceilings, Revit insisted on printing the category as a prefix. We ended up with “Wall_TOD_Wall” instead of the clean “TOD_Wall” our naming convention required.

​Instead of falling back on the old manual Excel “export-fix-import” dance, I decided to take a “vibe coding” approach with Dynamo and Python. Here’s how we solved three of the biggest COBie headaches in one surgical strike.

​1. The Naming Logic: System vs. Loadable

​The first challenge was distinguishing between element types. We needed system families to show only the Type Name, while loadable families required the Family + Type format.

​By using a Python script within Dynamo, we could treat these separately. For system families, we bypassed Revit’s concatenation and pulled the clean type name. For loadable families, we maintained the full descriptive string. To ensure absolute uniqueness (a COBie must), we then appended the Revit Element ID as a suffix, separated by a hyphen.

​2. Bridging the Linked Model Gap

​In many FFE models, the rooms don’t actually exist in the host file—they live in the architectural link. Standard COBie tools often struggle to “see” these linked rooms, leading to hundreds of “Blanked” values in the COBie.Component.Space parameter.

​The solution was to make our script “Link-Aware.” We developed a logic that:

  • ​Identifies the Active View Phase to ensure we only look at “New Construction” rooms.
  • ​Transforms the coordinates of the architectural link into the host model’s space.
  • ​Performs a proximity check (within a 40ft radius) to find the nearest room location point.

​This took our success rate from nearly 80% “Blanked” to over 92% automated matching.

​3. Streamlining Descriptions with Uniclass

​Manual data entry is the enemy of information management. Since we already had high-quality data in our Classification.Uniclass.Pr.Description parameter, it made sense to map this directly to COBie.Type.Description and COBie.Component.Description.

​By integrating this into the same script, we ensured that every time we updated the names and spaces, the descriptions remained perfectly synced with our classification system.

​The Result: Clean Data, Less Friction

​By the end of the process, we moved from 2,415 blank values to just 3—outliers that were physically too far from any room to be matched.

​This is the power of “Vibe Coding.” As BIM Information Managers, we don’t need to be full-stack developers, but having a working knowledge of Python and the Revit API allows us to build the “bridge” that standard extensions often miss.

You can review the python code below if you wish to use it.

# -*- coding: utf-8 -*-
# Dynamo Python Script for Revit 2025 (CPython 3)
# Logic: Unified COBie Update for Name, Type, Space Mapping (including Links), and Descriptions

import clr

clr.AddReference(‘RevitServices’)
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager

clr.AddReference(‘RevitAPI’)
from Autodesk.Revit.DB import (
    FilteredElementCollector, BuiltInCategory, XYZ,
    BuiltInParameter, Wall, FamilyInstance, ElementId,
    RevitLinkInstance, Transform
)

doc = DocumentManager.Instance.CurrentDBDocument

# —- Settings —-
NAME_PARAM = “COBie.Component.Name”
TYPE_PARAM = “COBie.Type.Name”
SPACE_PARAM = “COBie.Component.Space”
# New Description Parameters
COMP_DESC_PARAM = “COBie.Component.Description”
TYPE_DESC_PARAM = “COBie.Type.Description”
SOURCE_DESC_PARAM = “Classification.Uniclass.Pr.Description”

UNASSIGNED = “”       
SEARCH_RADIUS_FT = 40.0

# —- Helpers —-
def tolist(x):
    if hasattr(x, ‘__iter__’) and not isinstance(x, str): return x
    return [x]

def get_active_view_phase():
    “””Get the Phase of the current active view.”””
    try:
        view = doc.ActiveView
        phase_param = view.get_Parameter(BuiltInParameter.VIEW_PHASE)
        if phase_param:
            return doc.GetElement(phase_param.AsElementId())
    except:
        pass
    return None

def get_rooms_from_all_sources(active_phase):
    “””
    Collects rooms from the host document and all linked documents.
    Returns a list of tuples: (Room, Transform)
    “””
    all_source_rooms = []
    if not active_phase: return []
   
    # 1. Host Rooms
    host_rooms = FilteredElementCollector(doc).OfCategory(BuiltInCategory.OST_Rooms).WhereElementIsNotElementType()
    for r in host_rooms:
        if r.Location:
            p_param = r.get_Parameter(BuiltInParameter.ROOM_PHASE)
            if p_param and p_param.AsElementId() == active_phase.Id:
                all_source_rooms.append((r, Transform.Identity))
               
    # 2. Linked Rooms
    links = FilteredElementCollector(doc).OfClass(RevitLinkInstance)
    for link in links:
        link_doc = link.GetLinkDocument()
        if not link_doc: continue
       
        link_transform = link.GetTotalTransform()
       
        # We try to match the phase name from host to link
        link_phases = link_doc.Phases
        target_link_phase = None
        for lp in link_phases:
            if lp.Name == active_phase.Name:
                target_link_phase = lp
                break
       
        if target_link_phase:
            link_rooms = FilteredElementCollector(link_doc).OfCategory(BuiltInCategory.OST_Rooms).WhereElementIsNotElementType()
            for lr in link_rooms:
                if lr.Location:
                    lp_param = lr.get_Parameter(BuiltInParameter.ROOM_PHASE)
                    if lp_param and lp_param.AsElementId() == target_link_phase.Id:
                        all_source_rooms.append((lr, link_transform))
                       
    return all_source_rooms

def get_element_center(el):
    “””Find the best representative 3D point for an element.”””
    try:
        bb = el.get_BoundingBox(None)
        if bb:
            return bb.Min.Add(bb.Max).Multiply(0.5)
        loc = el.Location
        if hasattr(loc, ‘Point’):
            return loc.Point
        if hasattr(loc, ‘Curve’):
            return loc.Curve.Evaluate(0.5, True)
    except:
        pass
    return None

# —- MAIN EXECUTION —-
# IN[0] = System Elements, IN[1] = Loadable Elements
sys_input = tolist(IN[0]) if len(IN) > 0 else []
load_input = tolist(IN[1]) if len(IN) > 1 else []

system_items = [UnwrapElement(e) for e in sys_input if e]
loadable_items = [UnwrapElement(e) for e in load_input if e]

active_phase = get_active_view_phase()
room_data_list = get_rooms_from_all_sources(active_phase)

TransactionManager.Instance.EnsureInTransaction(doc)

# PROCESS ALL ITEMS IN ONE LOOP LOGIC
def process_element(el, is_system):
    try:
        e_type = doc.GetElement(el.GetTypeId())
        if not e_type: return
       
        t_name = e_type.get_Parameter(BuiltInParameter.SYMBOL_NAME_PARAM).AsString()
       
        # 1. GENERATE NAMES
        if is_system:
            base_name = t_name # Type Only
        else:
            f_name = e_type.get_Parameter(BuiltInParameter.ALL_MODEL_FAMILY_NAME).AsString()
            base_name = “{}_{}”.format(f_name, t_name) # Family_Type
           
        comp_name = “{}-{}”.format(base_name, el.Id.IntegerValue)
       
        # 2. UPDATE NAME PARAMETERS
        p_comp = el.LookupParameter(NAME_PARAM)
        if p_comp and not p_comp.IsReadOnly: p_comp.Set(comp_name)
       
        p_type = e_type.LookupParameter(TYPE_PARAM)
        if p_type and not p_type.IsReadOnly: p_type.Set(base_name)
       
        # 3. UPDATE DESCRIPTION PARAMETERS
        p_source = e_type.LookupParameter(SOURCE_DESC_PARAM)
        if p_source and p_source.HasValue:
            desc_val = p_source.AsString()
            pt_desc = e_type.LookupParameter(TYPE_DESC_PARAM)
            if pt_desc and not pt_desc.IsReadOnly: pt_desc.Set(desc_val)
            pc_desc = el.LookupParameter(COMP_DESC_PARAM)
            if pc_desc and not pc_desc.IsReadOnly: pc_desc.Set(desc_val)
       
        # 4. FIND SPACE (including Links)
        found_room = None
        target_pt = get_element_center(el)
       
        if target_pt and room_data_list:
            best_dist = SEARCH_RADIUS_FT
            for r, transform in room_data_list:
                # Get room location and transform it to host coordinates
                room_loc_host = transform.OfPoint(r.Location.Point)
                dist = target_pt.DistanceTo(room_loc_host)
                if dist < best_dist:
                    best_dist = dist
                    found_room = r
       
        # 5. UPDATE SPACE PARAMETER
        p_space = el.LookupParameter(SPACE_PARAM)
        if p_space and not p_space.IsReadOnly:
            val = found_room.get_Parameter(BuiltInParameter.ROOM_NUMBER).AsString() if found_room else UNASSIGNED
            p_space.Set(val)
           
        return True
    except:
        return False

# Execute
for s in system_items: process_element(s, True)
for l in loadable_items: process_element(l, False)

TransactionManager.Instance.TransactionTaskDone()

OUT = “COBie Master Update Complete. Processed: {}”.format(len(system_items) + len(loadable_items))

Leave a comment

I’m William

But feel free to call me Willy. I qualified with a BSc (Hons) in Architectural Technology and worked as an Architectural Technologist for over 15 years before moving into BIM Information Management. Since 2015, I’ve been working with BIM and digital construction workflows, and in 2023 I stepped into my current role as a BIM Information Manager. I am also BRE ISO 19650-2 certified, reflecting my commitment to best-practice information management. On this blog, I share insights on BIM and Information Management, along with personal reflections on investing and balancing professional life with family.

Husband | Dad | Dog Owner | Curious Mind