Pyrevit script for level changing without moving

Hello everyone, I’m trying to create a pyRevit script that lets me pick one or more elements, choose a new target Level, and have those elements reassigned to that Level without moving vertically in the model — meaning their absolute world elevation stays the same by automatically adjusting the offset value, is that possible in any sort of way?

Hi @MARK-A,
Welcome,

I would suggest having a look at LevelAutoSet | Revit | Autodesk App Store and not wasting time and effort on this script as this cheap tool does so much in that regard _ unless you have time and the purpose of the tool is mostly to learn how to make it.

1 Like

Thank you for your reply, I’ll check it out, but yes as you said, it’s also for learning purpose.

Hi @MARK-A

for learning purpose, maybe you can try to look on this tool:

1 Like

Thank you very much, very useful, I’ll put my code here if I was able to do it.

Here is the script I made with the help of AI, it works great with floors and walls, unfortunately I’m not able to make it work with other structural elements such as stairs and columns and beams. any feedback or tips is appreciated

"""
Change element levels while maintaining position
Supports Floors & Walls (for now)
Author: Mark Alkazzi
"""

from Autodesk.Revit.DB import *
from pyrevit import revit, forms

doc = revit.doc
uidoc = revit.uidoc

def pick_elements():
    """Get currently selected elements"""
    selection_ids = uidoc.Selection.GetElementIds()
    if not selection_ids:
        forms.alert("Please select elements first!", exitscript=True)
    return [doc.GetElement(id) for id in selection_ids]

def pick_target_level():
    """Present user with level selection dialog"""
    levels = list(FilteredElementCollector(doc).OfClass(Level))
    level_names = [lvl.Name for lvl in levels]
    
    # Use the correct SelectFromList.show() method
    # When multiselect=False, it returns the selected item directly (not in a list)
    selected_level_name = forms.SelectFromList.show(
        level_names, 
        title='Select Target Level',
        button_name='Select Level',
        multiselect=False
    )
    
    # Handle the case where user cancels or no selection is made
    if selected_level_name:
        # Find the level object that matches the selected name
        for lvl in levels:
            if lvl.Name == selected_level_name:
                return lvl
    
    return None

def change_element_level(element, target_level):
    """Change element level while maintaining world position"""
    try:
        # Check if element has a LevelId parameter
        if not hasattr(element, 'LevelId') or not element.LevelId:
            print("Warning: Element {} does not have a valid LevelId".format(element.Id))
            return False
            
        current_level = doc.GetElement(element.LevelId)
        if not current_level:
            print("Warning: Could not find current level for element {}".format(element.Id))
            return False
            
        # Skip if already on target level
        if current_level.Id == target_level.Id:
            print("Element {} is already on target level".format(element.Id))
            return True
            
        delta = current_level.Elevation - target_level.Elevation
        
        if isinstance(element, Floor):
            # Change floor level and adjust height above level offset
            level_param = element.get_Parameter(BuiltInParameter.LEVEL_PARAM)
            offset_param = element.get_Parameter(BuiltInParameter.FLOOR_HEIGHTABOVELEVEL_PARAM)
            
            if level_param and offset_param:
                level_param.Set(target_level.Id)
                new_offset = offset_param.AsDouble() + delta
                offset_param.Set(new_offset)
                print("Successfully changed floor level: Element {}".format(element.Id))
                return True
                
        elif isinstance(element, Wall):
            # Change wall base constraint and adjust base offset
            base_param = element.get_Parameter(BuiltInParameter.WALL_BASE_CONSTRAINT)
            offset_param = element.get_Parameter(BuiltInParameter.WALL_BASE_OFFSET)
            
            if base_param and offset_param:
                base_param.Set(target_level.Id)
                new_offset = offset_param.AsDouble() + delta
                offset_param.Set(new_offset)
                print("Successfully changed wall level: Element {}".format(element.Id))
                return True
                
        elif isinstance(element, FamilyInstance):
            # Handle columns and other family instances
            if hasattr(element, 'StructuralType') and element.StructuralType.ToString() == "Column":
                # Structural column
                level_param = element.get_Parameter(BuiltInParameter.FAMILY_LEVEL_PARAM)
                offset_param = element.get_Parameter(BuiltInParameter.FAMILY_BASE_LEVEL_OFFSET_PARAM)
                
                if level_param and offset_param:
                    level_param.Set(target_level.Id)
                    new_offset = offset_param.AsDouble() + delta
                    offset_param.Set(new_offset)
                    print("Successfully changed column level: Element {}".format(element.Id))
                    return True
            else:
                # General family instance
                level_param = element.get_Parameter(BuiltInParameter.FAMILY_LEVEL_PARAM)
                if level_param:
                    # Check if element has free host offset parameter
                    offset_param = element.get_Parameter(BuiltInParameter.INSTANCE_FREE_HOST_OFFSET_PARAM)
                    if offset_param:
                        level_param.Set(target_level.Id)
                        new_offset = offset_param.AsDouble() + delta
                        offset_param.Set(new_offset)
                        print("Successfully changed family instance level: Element {}".format(element.Id))
                        return True
                    else:
                        # Just change level without offset adjustment
                        level_param.Set(target_level.Id)
                        print("Changed family instance level (no offset): Element {}".format(element.Id))
                        return True
                        
        elif element.Category and element.Category.Name == "Stairs":
            # Basic stairs support - may need adjustment based on stair type
            level_param = element.get_Parameter(BuiltInParameter.STAIRS_BASE_LEVEL_PARAM)
            offset_param = element.get_Parameter(BuiltInParameter.STAIRS_BASE_OFFSET)
            
            if level_param:
                level_param.Set(target_level.Id)
                if offset_param:
                    new_offset = offset_param.AsDouble() + delta
                    offset_param.Set(new_offset)
                print("Successfully changed stairs level: Element {}".format(element.Id))
                return True
                
        print("Warning: Element type not fully supported or parameters not accessible: {}".format(element.Category.Name if element.Category else "Unknown"))
        return False
        
    except Exception as e:
        print("Error processing element {}: {}".format(element.Id, str(e)))
        return False

def main():
    """Main script execution"""
    # Get selected elements
    elements = pick_elements()
    
    # Get target level
    target_level = pick_target_level()
    if not target_level:
        forms.alert("No level selected. Operation cancelled.")
        return
    
    # Process elements within transaction
    with revit.Transaction("Change Element Levels"):
        success_count = 0
        total_count = len(elements)
        
        for element in elements:
            if change_element_level(element, target_level):
                success_count += 1
            else:
                print("Failed to process element: {} (ID: {})".format(
                    element.Category.Name if element.Category else "Unknown", 
                    element.Id
                ))
        
        print("Successfully processed {}/{} elements".format(success_count, total_count))
        
        if success_count > 0:
            forms.alert("Successfully changed level for {}/{} elements to '{}'".format(
                success_count, total_count, target_level.Name))
        else:
            forms.alert("No elements were successfully processed. Check output for details.")

# Execute main function
if __name__ == '__main__':
    main()

What error messages are you getting?

for columns : Error processing element 2083213: The parameter is read-only.

for beams :Failed to process element: Structural Framing (ID: 2237700)
Warning: Could not find current level for element 2237714

for stairs: Warning: Could not find current level for element 4395551
Failed to process element: Stairs (ID: 4395551)

this should help Fixes column level and offset in Revit using Dynamo · GitHub

Base Level parameters aren’t the same depending on the object type/category
I suggest you explore the elements you want to host differently using the RevitLookup tool

thanks Jean-marc I’ll check it out when I have more time for it, for now I’m happy with it since it works with the walls and Floors, and the others I’ll do manually