Revit API returns wrong connected duct after programmatic damper insertion (Connector.AllRefs issue)

I’m facing an issue in Revit + Dynamo (Python) related to MEP connector topology after inserting a damper programmatically.

  • I have a duct run with a shoe fitting.
  • Before inserting a damper, using connector traversal correctly returns:
    • Duct 1 ↔ Shoe ↔ Duct 3 (expected)

After damper insertion

  • I insert a damper using a custom Dynamo node:
    • Node: MEPFitting.ByPointsAndCurve (from MEPover package)
  • This splits the original duct into:
    • Duct 1 → Damper → Duct 2

When I query connected elements of the shoe using:

  • Connector.AllRefs
  • or Connector.IsConnected

I still get:

  • Duct 1 + Duct 3

But the correct result should be:

  • Duct 2 + Duct 3

This issue does NOT happen when:

  • The damper is placed manually in Revit
  • The same connected-elements logic is run afterward

I am guessing that the system won’t update until you commit the transaction which adds ducts 2 into the document.

Something like group of nodes to add duct 2 and the connector > Transaction.End > Transaction.Start > group of nodes to get the connected ducts should do the trick. You’ll also need to ensure that the these are wired in sequence, not disconnected as in yoru screenshot as otherwise the connected parts group might finish before the new ducts are added.

1 Like

If you are running all of these nodes within one script and placing nodes over to the right away from this creation set of nodes.

The order of run time is not decided by placement of nodes on the screen and could be run in any order. The main thing is that anything that is connected will run in order of how they are all connected.

Plus you may need to end the transaction sometimes to get the revit database to update which could be a area to look into.

1 Like

@jacob.small @Brendan_Cassidy
I’m running two separate dynamo scripts. first script places dampers only and in second script I was checking my connected elements

1 Like

Ah - looks like the node MEPFitting.ByPointsAndCurve isn’t actually connecting things. If you get connected elements in the UI do they show as connected?

yes they are properly connected to damper

Hmmm… I’m out of ideas.

Best to post a sample model and graphs so someone can have a look with real data.

R2025 test Project1.rvt (9.1 MB)
test 02.dyn (5.2 KB)
test 01.dyn (16.2 KB)

test files

Hate to ask an obvious question, but have you confirmed that the element id you’re seeing actually belongs to Duct 1 and not Duct 2 by confirming element ids in Revit? Curve elements “split” in the direction of the curve, so it could be that your duct is actually generating right to left and Duct 3 connects to Duct 1 which connects to Duct 2.

1 Like

Yes I have confirmed, connected element ids belongs to duct1 and duct3 only

This looks like “one of those things” that Revit just natively handles in the background for you. That type of fitting just uses a “referential” connection. There’s no connector on the duct to tell the fitting how it’s connected (or even where) - just that they are connected somehow. So when you modify the duct to now be two separate elements, there’s no process to update that connection to a new connector since there wasn’t one to begin with. Revit apparently handles this for you when doing this through the UI, but the API method doesn’t. You will have to include the process of reassigning the connected duct to your automation.

Hi @vishalghuge2500 i have just tried your rvt, seems work here if i understand :wink:


you could try this one and see if it could be better..
damper.dyn (34.8 KB)

PS.remember i had that for long time ago, and if remember right :wink: it could help set your insert damper to a random size and setparameter back again…yep it was strange :wink: :wink: but not sure it could help in your case

thanks @sovitek
Issue was with the duct splitting logic, i have updated the code

import clr
import math

# Revit Services
clr.AddReference("RevitServices")
import RevitServices
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager
doc = DocumentManager.Instance.CurrentDBDocument

# Revit API
clr.AddReference("RevitAPI")
from Autodesk.Revit.DB import *
from Autodesk.Revit.DB.Mechanical import *

# Geometry Conversion
clr.AddReference("RevitNodes")
import Revit
clr.ImportExtensions(Revit.Elements)
clr.ImportExtensions(Revit.GeometryConversion)

# ------------------------------------------------------
# 1. INPUT HANDLING (Standardizes Single vs List)
# ------------------------------------------------------
def to_list(input_item):
    if isinstance(input_item, list):
        return input_item
    if hasattr(input_item, "__iter__") and not isinstance(input_item, str):
        return input_item
    return [input_item]

ducts_in = to_list(UnwrapElement(IN[0]))
points_in = to_list(IN[1]) 
fam_type = UnwrapElement(IN[2]) # Single Family Type

if isinstance(fam_type, list): fam_type = fam_type[0]

# ------------------------------------------------------
# 2. HELPER FUNCTIONS
# ------------------------------------------------------
def IsParallel(dir1,dir2):
    if dir1.Normalize().IsAlmostEqualTo(dir2.Normalize()): return True
    if dir1.Normalize().Negate().IsAlmostEqualTo(dir2.Normalize()): return True
    return False

def GetDirection(faminstance):
    for c in faminstance.MEPModel.ConnectorManager.Connectors:
        return c.CoordinateSystem.BasisZ

def GetClosestDirection(faminstance, lineDirection):
    conndir = None
    flat_linedir = XYZ(lineDirection.X,lineDirection.Y,0).Normalize()
    for conn in faminstance.MEPModel.ConnectorManager.Connectors:
        conndir = conn.CoordinateSystem.BasisZ
        if flat_linedir.IsAlmostEqualTo(conndir):
            return conndir
    return conndir

def SetInstanceElevation(inst, targetZ, level):
    offset = targetZ - level.Elevation
    for pid in (
        BuiltInParameter.INSTANCE_FREE_HOST_OFFSET_PARAM,
        BuiltInParameter.INSTANCE_ELEVATION_PARAM,
        BuiltInParameter.FAMILY_BASE_LEVEL_OFFSET_PARAM
    ):
        p = inst.get_Parameter(pid)
        if p and not p.IsReadOnly:
            try:
                p.Set(offset)
                return
            except:
                pass

def to_xyz(pt):
    if hasattr(pt, "ToXyz"): return pt.ToXyz()
    return pt

def get_connectors(element):
    if isinstance(element, FamilyInstance):
        return [c for c in element.MEPModel.ConnectorManager.Connectors]
    elif isinstance(element, Duct):
        return [c for c in element.ConnectorManager.Connectors]
    return []

def connect_duct_to_damper(duct_elem, damper_elem):
    """Connects a duct to a damper if their connectors are co-located."""
    duct_conns = get_connectors(duct_elem)
    damper_conns = get_connectors(damper_elem)
    
    for dc in duct_conns:
        for fc in damper_conns:
            if dc.Origin.DistanceTo(fc.Origin) < 0.01:
                if not dc.IsConnectedTo(fc):
                    try:
                        dc.ConnectTo(fc)
                        return 
                    except:
                        pass

# ------------------------------------------------------
# 3. PLACEMENT & ROTATION LOGIC
# ------------------------------------------------------
tempfamtype = None
xAxis = XYZ(1,0,0)

def placeFitting(duct, point_xyz, familytype, lineDirection):
    toggle = False
    isVertical = False
    
    global tempfamtype
    global xAxis
    
    if tempfamtype == None:
        tempfamtype = familytype
        toggle = True
    elif tempfamtype.Id != familytype.Id:
        toggle = True
        tempfamtype = familytype

    level = duct.ReferenceLevel
    width = 4
    height = 4
    radius = 2
    round = False

    connectors = duct.ConnectorManager.Connectors
    for c in connectors:
        if c.ConnectorType != ConnectorType.End: continue
        shape = c.Shape
        if shape == ConnectorProfileType.Round:
            radius = c.Radius
            round = True    
            break
        elif shape == ConnectorProfileType.Rectangular or shape == ConnectorProfileType.Oval:
            if abs(lineDirection.Z) == 1:
                isVertical = True
                yDir = c.CoordinateSystem.BasisY
            width = c.Width
            height = c.Height
            break
            
    # Place Instance
    placementPoint = XYZ(point_xyz.X, point_xyz.Y, level.Elevation)
    newfam = doc.Create.NewFamilyInstance(
        placementPoint, familytype, level, Structure.StructuralType.NonStructural
    )
    
    SetInstanceElevation(newfam, point_xyz.Z, level)
    doc.Regenerate()

    # Rotation
    transform = newfam.GetTransform()
    axis = Line.CreateUnbound(transform.Origin, transform.BasisZ)

    if toggle:
        xAxis = GetDirection(newfam)

    zAxis = XYZ(0,0,1)
    
    if isVertical:
        angle = xAxis.AngleOnPlaneTo(yDir,zAxis)
    else:
        angle = xAxis.AngleOnPlaneTo(lineDirection,zAxis)
    
    ElementTransformUtils.RotateElement(doc,newfam.Id,axis,angle)
    doc.Regenerate()
    
    if lineDirection.Z != 0:
        newAxis = GetClosestDirection(newfam,lineDirection)
        yAxis = newAxis.CrossProduct(zAxis)
        angle2 = newAxis.AngleOnPlaneTo(lineDirection,yAxis)
        axis2 = Line.CreateUnbound(transform.Origin, yAxis)
        ElementTransformUtils.RotateElement(doc,newfam.Id,axis2,angle2)
    
    # Set Sizes
    famconns = newfam.MEPModel.ConnectorManager.Connectors
    if round:
        for conn in famconns:
            if not IsParallel(lineDirection,conn.CoordinateSystem.BasisZ): continue
            if conn.Shape != shape: continue
            try: conn.Radius = radius
            except: pass
    else:
        for conn in famconns:
            if not IsParallel(lineDirection,conn.CoordinateSystem.BasisZ): continue
            if conn.Shape != shape: continue
            try: 
                conn.Width = width
                conn.Height = height
            except: pass

    return newfam

# ------------------------------------------------------
# 4. MAIN EXECUTION LOOP
# ------------------------------------------------------
TransactionManager.Instance.EnsureInTransaction(doc)

# Activate Family Once
if not fam_type.IsActive:
    fam_type.Activate()
    doc.Regenerate()

output_list = []

# Iterate through Ducts
for i, duct in enumerate(ducts_in):
    placed_dampers_for_this_duct = []
    
    try:
        # Handle mapped points (Single point or List of points)
        pt_data = points_in[i] if i < len(points_in) else None
        
        # Normalize points to a list of XYZs
        xyz_points = []
        if isinstance(pt_data, list):
            for p in pt_data: xyz_points.append(to_xyz(p))
        else:
            xyz_points.append(to_xyz(pt_data))
            
        # Sort points from Start to End so we process downstream correctly
        loc_curve = duct.Location.Curve
        duct_start = loc_curve.GetEndPoint(0)
        xyz_points.sort(key=lambda p: p.DistanceTo(duct_start))
        
        current_duct_element = duct
        
        # Process each point for this duct
        for xyz_pt in xyz_points:
            
            # Re-fetch curve (it changes after every split!)
            curr_curve = current_duct_element.Location.Curve
            lineDirection = curr_curve.Direction 
            
            proj_result = curr_curve.Project(xyz_pt)
            
            if proj_result:
                center_point = proj_result.XYZPoint
                
                # A. PLACE & ROTATE
                damper = placeFitting(current_duct_element, center_point, fam_type, lineDirection)
                doc.Regenerate()
                
                # B. GET CUT POINTS
                damper_conns = get_connectors(damper)
                curr_start = curr_curve.GetEndPoint(0)
                sorted_conns = sorted(damper_conns, key=lambda c: c.Origin.DistanceTo(curr_start))
                
                cut_pt_A = sorted_conns[0].Origin
                cut_pt_B = sorted_conns[-1].Origin 
                
                # C. SPLIT 1
                proj_A = curr_curve.Project(cut_pt_A)
                p1 = proj_A.XYZPoint if proj_A else cut_pt_A
                
                new_id_1 = MechanicalUtils.BreakCurve(doc, current_duct_element.Id, p1)
                
                # D. SPLIT 2 (Try Both Logic)
                ducts_to_check = [current_duct_element.Id, new_id_1]
                final_duct_ids = []
                second_split_success = False
                
                for d_id in ducts_to_check:
                    d_elem = doc.GetElement(d_id)
                    if not d_elem: continue
                    
                    if not second_split_success:
                        try:
                            c_curve = d_elem.Location.Curve
                            proj_B = c_curve.Project(cut_pt_B)
                            if proj_B:
                                p2 = proj_B.XYZPoint
                                # Prevent zero-length split errors
                                if not (p2.IsAlmostEqualTo(c_curve.GetEndPoint(0)) or p2.IsAlmostEqualTo(c_curve.GetEndPoint(1))):
                                    new_id_2 = MechanicalUtils.BreakCurve(doc, d_id, p2)
                                    final_duct_ids.append(d_id)
                                    final_duct_ids.append(new_id_2)
                                    second_split_success = True
                                    continue 
                        except: pass
                    final_duct_ids.append(d_id)
                
                # E. FIND MIDDLE & DELETE
                duct_objs = [doc.GetElement(ide) for ide in final_duct_ids]
                mid_duct = None
                side_ducts = []
                
                closest_dist = 10000.0
                
                for d in duct_objs:
                    if d is None: continue
                    crv = d.Location.Curve
                    mid_pt = (crv.GetEndPoint(0) + crv.GetEndPoint(1)) / 2
                    dist = mid_pt.DistanceTo(center_point)
                    
                    if dist < 0.5: 
                        if dist < closest_dist:
                            if mid_duct: side_ducts.append(mid_duct)
                            mid_duct = d
                            closest_dist = dist
                        else:
                            side_ducts.append(d)
                    else:
                        side_ducts.append(d)
                
                if mid_duct: doc.Delete(mid_duct.Id)
                
                # F. CONNECT
                for d in side_ducts:
                    connect_duct_to_damper(d, damper)
                
                placed_dampers_for_this_duct.append(damper)
                
                # G. UPDATE CURRENT DUCT (For next point in loop)
                # The duct continuing downstream is the one furthest from the original start
                # or simply the one that isn't the "upstream" piece.
                # A safe heuristic: The duct whose *Start Point* is closest to the damper is the downstream one.
                
                downstream_duct = None
                min_dist_to_damper = 10000.0
                
                for d in side_ducts:
                    try:
                        # Check start point of this duct segment
                        d_start = d.Location.Curve.GetEndPoint(0)
                        dist = d_start.DistanceTo(center_point)
                        if dist < min_dist_to_damper:
                            min_dist_to_damper = dist
                            downstream_duct = d
                    except: pass
                
                if downstream_duct:
                    current_duct_element = downstream_duct
                    
    except Exception as e:
        placed_dampers_for_this_duct.append("Error: " + str(e))
        
    output_list.append(placed_dampers_for_this_duct)

TransactionManager.Instance.TransactionTaskDone()

OUT = output_list