Skip to content
RevitBootcamp2027 edition
Automation8 min readAutomation

Revit API primer

Transactions, FilteredElementCollector, parameters and units, the single-threaded rule, element ids, and the errors you'll hit first.

Written for someone who can already program. This is the shape of the platform and the handful of constraints that produce almost every error you'll hit.

The object model, minimally

Application                 the Revit session
  Document                  one .rvt (or .rfa)
    Element                 everything is an Element
      ├─ ElementType        the "type" (Category → Family → Type → Instance)
      ├─ Parameter          named values on an element
      ├─ Category           classification
      ├─ View               a view is an Element too
      └─ Geometry           GeometryElement → Solid → Face → Edge → Curve
UIApplication / UIDocument  the UI wrapper — selection, prompts, active view

Two things to internalize:

  • Everything is an Element with an ElementId. Views, levels, materials, sheets, and view templates are all elements in the document.
  • Document is model access; UIDocument is user access. You need UIDocument for selection and prompting, Document for everything else. Add-ins get both from ExternalCommandData.

Rule 1: all model changes happen inside a Transaction

using (Transaction t = new Transaction(doc, "Renumber doors"))
{
    t.Start();
    foreach (var door in doors)
        door.get_Parameter(BuiltInParameter.ALL_MODEL_MARK).Set(mark);
    t.Commit();
}

Without it:

Autodesk.Revit.Exceptions.InvalidOperationException:
Attempting to modify the model outside of transaction.
  • The transaction name is what appears in Revit's undo list. Name it for the user, not for yourself.
  • t.RollBack() discards everything since Start(). Use it when a partial change would be worse than none.
  • SubTransaction nests inside an open transaction for finer rollback control.
  • TransactionGroup wraps multiple transactions into a single undo entry, with Assimilate() to merge them or RollBack() to discard the lot.
  • [Transaction(TransactionMode.Manual)] on your command class is what you want — you manage transactions. ReadOnly for commands that only read.
  • Failure handling: t.SetFailureHandlingOptions() lets you suppress or handle the warning dialogs Revit would otherwise pop up mid-transaction. Essential for batch work, because a modal dialog in a loop hangs Revit.

In pyRevit, use the context manager and get the same behaviour for free:

with revit.Transaction("Renumber doors"):
    ...   # rolls back if an exception escapes the block

Rule 2: the API is single-threaded

All model access happens on Revit's main thread, in a valid API context. You cannot touch the document from a background thread, a timer, or a modeless window's event handler.

When you need to — a dockable panel, a long-running task, a web response arriving — use ExternalEvent:

public class MyHandler : IExternalEventHandler
{
    public string GetName() => "MyHandler";
    public void Execute(UIApplication app)
    {
        // valid API context — safe to open a transaction here
    }
}
 
// once, at startup:
var handler = new MyHandler();
ExternalEvent ev = ExternalEvent.Create(handler);
 
// from anywhere, any thread:
ev.Raise();   // Revit calls Execute() when it's safe

Ignore this and you get crashes that don't reproduce. It is the single biggest architectural constraint on any non-trivial add-in.

The older Idling event is the historical version of the same idea and still works for deferred work; ExternalEvent is the modern answer.

Querying: FilteredElementCollector

Every read starts here.

var doors = new FilteredElementCollector(doc)
    .OfCategory(BuiltInCategory.OST_Doors)
    .WhereElementIsNotElementType()
    .ToElements();

The instance/type distinction

.WhereElementIsNotElementType()   // placed instances
.WhereElementIsElementType()      // the type definitions

Forgetting this is the most common beginner bug: your door count comes back as 4 because you counted types, not doors.

Quick filters vs. slow filters

Order matters, a lot:

Kind Examples Cost
Quick OfCategory, OfClass, OwnedByView, WhereElementIsNotElementType, BoundingBoxIntersectsFilter Reads the element record only
Slow ElementParameterFilter, FamilyInstanceFilter, anything evaluating a parameter value Expands every element

Always narrow with a quick filter first:

// good
new FilteredElementCollector(doc)
    .OfCategory(BuiltInCategory.OST_Walls)      // quick
    .WhereElementIsNotElementType()             // quick
    .WherePasses(fireRatingFilter)              // slow, but on a small set
 
// bad — expands every element in the document
new FilteredElementCollector(doc)
    .WherePasses(fireRatingFilter)

Scope it to a view

new FilteredElementCollector(doc, view.Id)   // only what's visible in that view

Much faster, and frequently what you actually want — it respects the view's visibility, crop, and filters.

Useful specific collectors

// all instances of a specific family type
new FilteredElementCollector(doc)
    .WherePasses(new FamilyInstanceFilter(doc, symbolId));
 
// elements intersecting a bounding box
new FilteredElementCollector(doc)
    .WherePasses(new BoundingBoxIntersectsFilter(outline));
 
// elements in a given design option
new FilteredElementCollector(doc)
    .WherePasses(new ElementDesignOptionFilter(optionId));
 
// everything on a level
new FilteredElementCollector(doc)
    .WherePasses(new ElementLevelFilter(levelId));

BuiltInCategory and BuiltInParameter are enormous enums covering everything Revit ships with. Learning to find names in them is most of the API's surface area — and RevitLookup (a free add-in) is how you find them: select an element, and it shows you every property, parameter, and enum value.

Parameters

Three ways to reach one, best first:

// 1. Built-in enum — stable across versions and language packs
Parameter p = element.get_Parameter(BuiltInParameter.ALL_MODEL_MARK);
 
// 2. Shared parameter GUID — stable for your own data
Parameter p = element.get_Parameter(new Guid("9c2f4b7e-...."));
 
// 3. By name — convenient, breaks under localization and typos
Parameter p = element.LookupParameter("Mark");

Then read with the accessor matching the storage type:

switch (p.StorageType)
{
    case StorageType.String:    p.AsString();     break;
    case StorageType.Double:    p.AsDouble();     break;   // internal units!
    case StorageType.Integer:   p.AsInteger();    break;   // also Yes/No: 0 or 1
    case StorageType.ElementId: p.AsElementId();  break;
}
p.AsValueString();   // formatted per project units — for display

Check p.IsReadOnly before setting. Type parameters reached from an instance are read-only; get the type element and set it there.

Units: the bug that ships

AsDouble() returns decimal feet for any length, square feet for any area, and cubic feet for volume — regardless of project units. Always.

// convert out
double mm = UnitUtils.ConvertFromInternalUnits(p.AsDouble(), UnitTypeId.Millimeters);
 
// convert in
p.Set(UnitUtils.ConvertToInternalUnits(3000, UnitTypeId.Millimeters));

UnitTypeId is the modern API (Revit 2021+); older code uses DisplayUnitType and UnitUtils.ConvertToInternalUnits(value, DisplayUnitType.DUT_MILLIMETERS). If you're maintaining cross-version code, this is one of the APIs that changed.

Angles are in radians. This catches people too.

ElementId vs. UniqueId

element.Id          // ElementId — an integer, unique within THIS document only
element.UniqueId    // string GUID-ish — stable across sessions and export
  • ElementId is what Manage → Select by ID uses and what warnings report. Not stable across files or copy/paste.
  • UniqueId persists, survives IFC export, and is what you store in an external database.

In Revit 2024+, ElementId moved from int to long internally (ElementId.Value rather than ElementId.IntegerValue). This broke a lot of add-ins and scripts — it's why pyRevit needed an int64 fix for Revit 2026. If you see IntegerValue in old code, that's a migration point.

Creating elements

Creation is via static Create methods on the element classes, not constructors:

Wall wall = Wall.Create(doc, curve, levelId, structural: false);
Floor floor = Floor.Create(doc, profileLoops, floorTypeId, levelId);
FamilyInstance fi = doc.Create.NewFamilyInstance(point, symbol, level, StructuralType.NonStructural);
ViewSheet sheet = ViewSheet.Create(doc, titleBlockTypeId);
Viewport vp = Viewport.Create(doc, sheet.Id, viewId, location);

The family symbol must be activated before you place it, which is a classic gotcha:

if (!symbol.IsActive) { symbol.Activate(); doc.Regenerate(); }

doc.Regenerate() forces Revit to recompute — needed when you create something and then immediately query its geometry in the same transaction.

Geometry, briefly

Options opts = new Options { ComputeReferences = true, DetailLevel = ViewDetailLevel.Fine };
GeometryElement geo = element.get_Geometry(opts);
 
foreach (GeometryObject obj in geo)
{
    if (obj is Solid solid && solid.Volume > 0)
        foreach (Face face in solid.Faces) { /* ... */ }
 
    if (obj is GeometryInstance inst)   // family instances nest their geometry
        foreach (GeometryObject nested in inst.GetInstanceGeometry()) { /* ... */ }
}

Two traps: family instance geometry is nested inside a GeometryInstance and you must recurse into it, and a Solid with zero volume is common and meaningless — filter it out.

LocationCurve and LocationPoint are the cheap way to get a wall's line or a column's point without touching geometry at all. Prefer them.

Add-in scaffolding

Command.cs
using Autodesk.Revit.Attributes;
using Autodesk.Revit.DB;
using Autodesk.Revit.UI;
 
[Transaction(TransactionMode.Manual)]
[Regeneration(RegenerationOption.Manual)]
public class CountWalls : IExternalCommand
{
    public Result Execute(ExternalCommandData data, ref string message, ElementSet elements)
    {
        UIDocument uidoc = data.Application.ActiveUIDocument;
        Document doc = uidoc.Document;
 
        var walls = new FilteredElementCollector(doc)
            .OfClass(typeof(Wall))
            .WhereElementIsNotElementType()
            .Cast<Wall>()
            .ToList();
 
        double feet = walls.Sum(w =>
            w.get_Parameter(BuiltInParameter.CURVE_ELEM_LENGTH).AsDouble());
 
        TaskDialog.Show("Walls", $"{walls.Count} walls, {feet:F1} ft total.");
        return Result.Succeeded;
    }
}
CountWalls.addin
<?xml version="1.0" encoding="utf-8"?>
<RevitAddIns>
  <AddIn Type="Command">
    <Name>Count Walls</Name>
    <Assembly>C:\RevitAddins\2027\CountWalls.dll</Assembly>
    <AddInId>0f8a4c1e-6b3d-4a2f-9c8e-1d5b7a9e3f21</AddInId>
    <FullClassName>CountWalls</FullClassName>
    <VendorId>RBC</VendorId>
    <VendorDescription>Revit Bootcamp</VendorDescription>
  </AddIn>
</RevitAddIns>
  • Manifest goes in %AppData%\Autodesk\Revit\Addins\<version>\ (per-user) or %ProgramData%\Autodesk\Revit\Addins\<version>\ (all users).
  • AddInId must be a unique GUID per add-in. Generate a new one; don't copy this.
  • Use IExternalApplication instead when you need startup hooks or ribbon UI: OnStartup / OnShutdown, application.CreateRibbonTab, CreateRibbonPanel, AddItem(new PushButtonData(...)).
  • Reference assemblies should be Copy Local = false — Revit loads its own.

The .NET version trap

Revit Target framework
2024 and earlier net48
2025, 2026 net8.0-windows
2027 net10.0-windows

An add-in built against the wrong target silently fails to load. No error, no dialog — your button just isn't there. Multi-target one project and ship a DLL per version. Details and the conditional-compilation pattern in the version matrix.

The errors you'll hit, in order

Error Cause
Attempting to modify the model outside of transaction No open Transaction
Starting a new transaction is not permitted A transaction is already open (or you're inside an IUpdater)
Element count is suspiciously small Missing WhereElementIsNotElementType()
Values are wrong by a factor of ~304.8 Internal units — feet, not millimetres
The family symbol is not active Call symbol.Activate() then doc.Regenerate()
Add-in doesn't appear at all Wrong .NET target, bad manifest path, or a load exception (check the journal)
Random crashes that don't reproduce Touching the document off the main thread — use ExternalEvent
IntegerValue doesn't exist Revit 2024+ — ElementId.Value, it's a long now
Modal dialog hangs a batch loop Set failure handling options on the transaction
Geometry loop finds nothing Family instance geometry is nested in a GeometryInstance

Learning resources

  • RevitLookup — install it first. A database browser for the selected element. You cannot learn this API efficiently without it.
  • The Revit API docs shipped with the SDK, plus the online API reference — dry but complete.
  • Revit API forum on the Autodesk community — the highest-signal place for specific problems.
  • The pyRevit interactive console — the fastest feedback loop that exists for poking at the model.
  • RevitAPI.chm in the SDK, and the Autodesk.Revit.SDK NuGet package for version-pinned reference assemblies.