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 Descriptionsimport clr
clr.AddReference(‘RevitServices’)
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManagerclr.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 Nonedef 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_roomsdef 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