Files
willtools-demo/docs/revit-api-cheatsheet.md
will 25592fa6c2
ci / test (push) Successful in 6m14s
WillTools 2026-09-09 -- clean snapshot of the development repo for review
History-free snapshot of the private development repository at 77a82eb.
Code, docs and the public catalogue only: no plant or job knowledge
layers, no client, job or producer names (tools/scrub.py clean against
the widened term list; a second word-level grep clean). The intro deck is
left out because its slide images cannot be checked for job drawings.
2026-09-09 08:35:15 -05:00

7.5 KiB

Revit 2025 API cheatsheet (IronPython / pyRevit)

Copy-paste patterns that are known to work on Revit 2025. Everything assumes:

from pyrevit import revit, forms, script
from Autodesk.Revit import DB, UI
from Autodesk.Revit.DB import *
doc, uidoc = revit.doc, revit.uidoc

Collecting

# 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

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

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

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

# 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

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

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

from System.Collections.Generic import List
ids = List[ElementId]([e.Id for e in els])       # APIs that want ICollection<ElementId>

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<ElementId> will not accept a Python list — wrap it in List[ElementId].


pyRevit UI

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).