# Revit 2025 API cheatsheet (IronPython / pyRevit) Copy-paste patterns that are known to work on Revit 2025. Everything assumes: ```python from pyrevit import revit, forms, script from Autodesk.Revit import DB, UI from Autodesk.Revit.DB import * doc, uidoc = revit.doc, revit.uidoc ``` --- ## Collecting ```python # all instances of a category walls = FilteredElementCollector(doc)\ .OfCategory(BuiltInCategory.OST_Walls)\ .WhereElementIsNotElementType()\ .ToElements() # by .NET class levels = FilteredElementCollector(doc).OfClass(Level).ToElements() # types, not instances wall_types = FilteredElementCollector(doc)\ .OfCategory(BuiltInCategory.OST_Walls)\ .WhereElementIsElementType().ToElements() # only what is visible in a view in_view = FilteredElementCollector(doc, doc.ActiveView.Id)\ .OfCategory(BuiltInCategory.OST_Doors)\ .WhereElementIsNotElementType().ToElements() # elements on a level lvl_filter = ElementLevelFilter(level.Id) on_level = FilteredElementCollector(doc).WherePasses(lvl_filter).ToElements() # by parameter value (fast, server-side) rule = ParameterFilterRuleFactory.CreateEqualsRule( ElementId(BuiltInParameter.ALL_MODEL_MARK), 'A-01') by_mark = FilteredElementCollector(doc)\ .WherePasses(ElementParameterFilter(rule)).ToElements() ``` `.ToElements()` materialises; `.ToElementIds()` is cheaper if ids are enough. Collectors are lazy — iterate once. --- ## Parameters ```python prm = el.LookupParameter('Comments') # by name (locale!) prm = el.get_Parameter(BuiltInParameter.ALL_MODEL_INSTANCE_COMMENTS) # robust # read, by storage type {StorageType.String: prm.AsString, StorageType.Integer: prm.AsInteger, StorageType.Double: prm.AsDouble, StorageType.ElementId: prm.AsElementId}[prm.StorageType]() prm.AsValueString() # what the UI shows, units included # write (inside a transaction) prm.Set('new value') ``` Prefer `BuiltInParameter` over `LookupParameter` — parameter *names* are localised and change between templates. `vibe.p()` / `vibe.set_p()` accept either. Type vs instance: `doc.GetElement(el.GetTypeId())` gets the type; type parameters live there, not on the instance. --- ## Transactions ```python t = Transaction(doc, 'Do the thing') t.Start() try: ... t.Commit() except Exception: t.RollBack() raise # pyRevit equivalent with revit.Transaction('Do the thing'): ... # local helper from vibe import tx with tx('Do the thing'): ... # several transactions, one undo step with TransactionGroup(doc, 'Batch') as g: # or vibe.txg('Batch') ... ``` Rules: no nested `Transaction` (use `SubTransaction`); a transaction cannot span a modal dialog; anything that changes the model — including creating a view, setting a parameter, deleting — needs one. --- ## Units — internal is always decimal feet ```python UnitUtils.ConvertToInternalUnits(3000, UnitTypeId.Millimeters) # mm -> feet UnitUtils.ConvertFromInternalUnits(length, UnitTypeId.Meters) # feet -> m UnitUtils.ConvertFromInternalUnits(area, UnitTypeId.SquareMeters) # what the document is set to display doc.GetUnits().GetFormatOptions(SpecTypeId.Length).GetUnitTypeId() ``` `DisplayUnitType` / `UnitType` were removed in Revit 2022. Use `ForgeTypeId` (`UnitTypeId.*`, `SpecTypeId.*`). --- ## Creating elements ```python # level lvl = Level.Create(doc, ft(4000)) # wall line = Line.CreateBound(XYZ(0, 0, 0), XYZ(ft(5000), 0, 0)) wall = Wall.Create(doc, line, wall_type.Id, level.Id, ft(3000), 0.0, False, True) # family instance symbol = FilteredElementCollector(doc)\ .OfCategory(BuiltInCategory.OST_Doors)\ .WhereElementIsElementType().FirstElement() if not symbol.IsActive: symbol.Activate(); doc.Regenerate() # required before first placement inst = doc.Create.NewFamilyInstance( XYZ(0, 0, 0), symbol, level, Structure.StructuralType.NonStructural) # hosted on a wall inst = doc.Create.NewFamilyInstance( point, symbol, wall, level, Structure.StructuralType.NonStructural) # text note TextNote.Create(doc, view.Id, XYZ(0, 0, 0), 'hello', text_type.Id) # delete doc.Delete(el.Id) # returns the ids actually deleted ``` `symbol.Activate()` + `doc.Regenerate()` before the first placement of an inactive symbol, or `NewFamilyInstance` throws. --- ## Views, sheets, schedules ```python views = FilteredElementCollector(doc).OfClass(View)\ .WhereElementIsNotElementType() plans = [v for v in views if v.ViewType == ViewType.FloorPlan and not v.IsTemplate] sheets = FilteredElementCollector(doc).OfClass(ViewSheet)\ .WhereElementIsNotElementType().ToElements() sheet = ViewSheet.Create(doc, titleblock_type_id) sheet.SheetNumber, sheet.Name = 'A-101', 'Ground Floor' Viewport.Create(doc, sheet.Id, view.Id, XYZ(1, 1, 0)) uidoc.ActiveView = some_view # switch the active view uidoc.Selection.SetElementIds(List[ElementId]([el.Id])) # needs System.Collections.Generic ``` --- ## Rooms, geometry, location ```python rooms = FilteredElementCollector(doc)\ .OfCategory(BuiltInCategory.OST_Rooms)\ .WhereElementIsNotElementType().ToElements() rooms = [r for r in rooms if r.Area > 0] # unplaced rooms have Area 0 loc = el.Location if isinstance(loc, LocationPoint): pt = loc.Point elif isinstance(loc, LocationCurve): crv = loc.Curve; start, end = crv.GetEndPoint(0), crv.GetEndPoint(1) bb = el.get_BoundingBox(None) # None = model coords; a view = view crop center = (bb.Min + bb.Max) / 2 opts = Options() opts.ComputeReferences, opts.DetailLevel = True, ViewDetailLevel.Fine geo = el.get_Geometry(opts) solids = [g for g in geo if isinstance(g, Solid) and g.Volume > 0] ``` --- ## .NET interop from IronPython ```python from System.Collections.Generic import List ids = List[ElementId]([e.Id for e in els]) # APIs that want ICollection import clr clr.AddReference('RevitAPI') clr.AddReference('System.Windows.Forms') # out parameters come back as a tuple ok, value = some_api.TryGetValue(key) ``` Revit APIs typed `ICollection` will not accept a Python list — wrap it in `List[ElementId]`. --- ## pyRevit UI ```python forms.alert('Done', title='Tool', warn_icon=False) forms.alert('Nothing selected', exitscript=True) choice = forms.SelectFromList.show(items, multiselect=True, button_name='OK') text = forms.ask_for_string(default='A-', prompt='Sheet prefix') path = forms.pick_file(file_ext='xlsx') level = forms.select_levels() sheets = forms.select_sheets() output = script.get_output() output.print_md('### Heading') output.print_table(table_data=rows, columns=['A', 'B']) output.linkify(el.Id) # clickable, selects the element in Revit with forms.ProgressBar(title='{value}/{max_value}') as pb: for i, el in enumerate(els): pb.update_progress(i + 1, len(els)) ``` --- ## Gotchas that cost hours - Parameter names are **localised**; `BuiltInParameter` is not. - `el.Name` throws on some element types — wrap in try/except. - Elements from a **linked** model are read-only; get them via `RevitLinkInstance.GetLinkDocument()`, and transform coordinates with `link_instance.GetTotalTransform()`. - Deleting while iterating a collector corrupts the iteration — materialise ids first, then delete. - `doc.Regenerate()` after geometry changes if you need to read the result in the same transaction. - Worksharing: check `el.Pinned` and element ownership before batch edits. - A `FilteredElementCollector` over a view id also returns hidden-in-view elements unless you check `el.IsHidden(view)`.