One of the common frustrations when preparing COBie data is populating the NominalHeight, NominalLength, and NominalWidth fields. These are required in the COBie Type worksheet, but Revit families don’t always provide an obvious way to get those values. Some families have reference planes with labelled dimensions, while others don’t. And even if they do, you’re left manually checking, entering, or mapping the parameters.
That’s where Dynamo and a little bit of Python scripting can save a huge amount of time.
The challenge
Revit stores family geometry internally, but it doesn’t expose a direct “bounding box size” parameter in the user interface. You can measure instances with dimensions or reference planes, but there’s no quick way to batch-extract the overall X, Y, and Z extents and push them into COBie fields.
If you’ve ever had to deliver COBie on a healthcare project or any facilities-focused job, you’ll know how tedious this can become.
The solution: Dynamo + Python
By combining Dynamo’s ability to select and iterate through elements with a small Python script that taps the Revit API, you can automatically:
Calculate the Length, Width, and Height of each family type from its bounding box or geometry,
Convert those values into the units you want (mm, m, ft, inches),
Write them straight into the COBie.Type.NominalLength, NominalWidth, and NominalHeight parameters.
The Python code
Here’s the script you can paste directly into a Dynamo Python node. It works in Revit 2019 and newer (IronPython 2.7 or CPython3 in later versions).
# Dynamo Python (Revit 2019+) — Robust BBox -> COBie.Type Nominal params
# – Handles bbox=None by falling back to geometry extents
# – Writes once per FamilySymbol (Type)
# – Skips linked instances
# – Table output with Status column
import clr
# RevitServices / RevitAPI
clr.AddReference(“RevitServices”)
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager
clr.AddReference(“RevitAPI”)
from Autodesk.Revit.DB import (
Element, StorageType, FamilySymbol, RevitLinkInstance, Options,
Solid, Mesh, GeometryInstance, Transform, XYZ
)
doc = DocumentManager.Instance.CurrentDBDocument
# ———- Helpers ———-
def ensure_list(x):
if x is None: return []
return x if isinstance(x, list) else [x]
def unwrap(elem):
try: return UnwrapElement(elem)
except: return elem
def unit_factor(to_units):
m = (to_units or “mm”).lower()
if m == “mm”: return 304.8
if m == “m”: return 0.3048
if m == “inch”: return 12.0
if m == “ft”: return 1.0
return 304.8
def to_internal_feet(value, from_units):
m = (from_units or “mm”).lower()
if m == “mm”: return value / 304.8
if m == “m”: return value / 0.3048
if m == “inch”: return value / 12.0
if m == “ft”: return value
return value / 304.8
def combine_minmax(cur_min, cur_max, pt):
if cur_min is None:
return XYZ(pt.X, pt.Y, pt.Z), XYZ(pt.X, pt.Y, pt.Z)
return XYZ(min(cur_min.X, pt.X), min(cur_min.Y, pt.Y), min(cur_min.Z, pt.Z)), \
XYZ(max(cur_max.X, pt.X), max(cur_max.Y, pt.Y), max(cur_max.Z, pt.Z))
def extents_from_minmax(pt_min, pt_max):
if (pt_min is None) or (pt_max is None): return None
dx = abs(pt_max.X – pt_min.X)
dy = abs(pt_max.Y – pt_min.Y)
dz = abs(pt_max.Z – pt_min.Z)
return (dx, dy, dz)
def try_bbox(elem):
try:
bb = elem.get_BoundingBox(None)
if bb and bb.Min and bb.Max:
return (abs(bb.Max.X – bb.Min.X), abs(bb.Max.Y – bb.Min.Y), abs(bb.Max.Z – bb.Min.Z)), “bbox:model”
except: pass
try:
bb = elem.get_BoundingBox(doc.ActiveView)
if bb and bb.Min and bb.Max:
return (abs(bb.Max.X – bb.Min.X), abs(bb.Max.Y – bb.Min.Y), abs(bb.Max.Z – bb.Min.Z)), “bbox:view”
except: pass
return None, “bbox:none”
def try_geometry_extents(elem):
try:
opt = Options()
geo = elem.get_Geometry(opt)
if geo is None: return None, “geom:none”
def walk_geom(geom_elem, xform):
gmin, gmax = None, None
for gobj in geom_elem:
if isinstance(gobj, Solid) and gobj.Faces.Size > 0:
bb = gobj.GetBoundingBox()
if bb and bb.Min and bb.Max:
corners = [
XYZ(bb.Min.X, bb.Min.Y, bb.Min.Z),
XYZ(bb.Max.X, bb.Max.Y, bb.Max.Z)
]
for c in corners:
wc = xform.OfPoint(c)
gmin, gmax = combine_minmax(gmin, gmax, wc)
elif isinstance(gobj, Mesh):
for i in range(gobj.Vertices.Count):
wc = xform.OfPoint(gobj.Vertices[i])
gmin, gmax = combine_minmax(gmin, gmax, wc)
elif isinstance(gobj, GeometryInstance):
inst_xf = xform.Multiply(gobj.Transform)
mmin, mmax = walk_geom(gobj.GetInstanceGeometry(), inst_xf)
if mmin: gmin, gmax = combine_minmax(gmin, gmax, mmin)
if mmax: gmin, gmax = combine_minmax(gmin, gmax, mmax)
return gmin, gmax
mn, mx = walk_geom(geo, Transform.Identity)
if mn is None: return None, “geom:empty”
return extents_from_minmax(mn, mx), “geom:ok”
except:
return None, “geom:err”
def set_type_param(sym, name, value_feet):
if not sym or not name: return False
p = sym.LookupParameter(name)
if p and not p.IsReadOnly:
if p.StorageType == StorageType.Double:
try: return p.Set(value_feet)
except: return False
else:
try: return p.Set(str(value_feet))
except: return False
return False
# ———- Inputs ———-
elements_in = ensure_list(IN[0] if len(IN) > 0 else [])
units = IN[1] if (len(IN) > 1 and IN[1]) else “mm”
factor = unit_factor(units)
table = [[“ElementId”,”UniqueId”,”TypeName”,”Method”,”Length(“+units+”)”,”Width(“+units+”)”,”Height(“+units+”)”,”Units”,”Status”]]
processed_type_ids = set()
TransactionManager.Instance.EnsureInTransaction(doc)
for e in elements_in:
e = unwrap(e)
if not isinstance(e, Element): continue
if isinstance(e, RevitLinkInstance):
table.append([e.Id.IntegerValue, e.UniqueId, “RevitLinkInstance”, “n/a”, None, None, None, units, “Skipped: Linked instance”])
continue
sym = getattr(e, “Symbol”, None)
sym_name = None
try: sym_name = sym.Family.Name + ” : ” + sym.Name if sym else None
except: pass
sizes_ft, which = try_bbox(e)
if sizes_ft is None: sizes_ft, which = try_geometry_extents(e)
if sizes_ft is None:
table.append([e.Id.IntegerValue, e.UniqueId, sym_name, which, None, None, None, units, “No geometry/bbox”])
continue
L_ft, W_ft, H_ft = sizes_ft
L_u, W_u, H_u = L_ft * factor, W_ft * factor, H_ft * factor
status = “Computed only”
if sym and isinstance(sym, FamilySymbol):
type_id = sym.Id.IntegerValue
if type_id not in processed_type_ids:
set_type_param(sym, “COBie.Type.NominalLength”, to_internal_feet(L_u, units))
set_type_param(sym, “COBie.Type.NominalWidth”, to_internal_feet(W_u, units))
set_type_param(sym, “COBie.Type.NominalHeight”, to_internal_feet(H_u, units))
processed_type_ids.add(type_id)
status = “Wrote Type params”
else:
status = “Type already written”
table.append([e.Id.IntegerValue, e.UniqueId, sym_name, which, L_u, W_u, H_u, units, status])
TransactionManager.Instance.TransactionTaskDone()
OUT = table
Why it matters for Information Management
This may sound like a small automation, but it has a big impact on the quality and efficiency of COBie deliverables:
Consistency: Every type gets dimensions calculated from the same logic.
Speed: Hundreds of families can be processed in seconds.
Compliance: COBie sheets are populated correctly, reducing the chance of validation failures later.
Reusability: Once you have this Dynamo graph set up, it can be reused on every project.
For anyone acting as Lead Appointed Party or managing an Asset Information Model, small wins like this keep the COBie process on track without burning design team hours on data entry.
Wrapping up
COBie has a reputation for being painful, but often it’s the “little things” like NominalLength, Width, and Height that cause the biggest bottlenecks. By using Dynamo to calculate and populate these automatically, you reduce friction for your design team and raise the overall quality of your deliverables.
Next step: Copy the code above into a Dynamo Python node, connect a list of family instances, and test it on your own project. You’ll be surprised how quickly those COBie fields fill themselves.

Leave a comment