You know the model now. Today you drive it with code. The point isn't to become a Revit developer in an afternoon — it's to know the shape of the platform well enough to recognize which problems are worth automating and which tool to reach for.
Pick the tool before you write anything
| Use | When |
|---|---|
| Schedule export | Somebody wants numbers. Genuinely most requests. |
| Dynamo | One-off bulk data operations, geometry generation, quick "read a spreadsheet and set parameters" jobs. Ships in the box. |
| pyRevit | A repeatable tool for yourself or your team. Real code, no compile step, hot reload, a ribbon button in minutes. |
| Compiled add-in (C#) | It needs to ship, be versioned, have real UI, integrate external services, or perform at scale. |
The failure mode is skipping the first two rows. A 200-line C# add-in to fix parameter values on 40 elements is a bad trade; a Dynamo graph does it in ten minutes and you throw it away.
Two facts about the Revit API that explain everything
Learn these before you write a line, because every error you'll hit comes from violating one of them.
1. All model changes happen inside a Transaction
using (Transaction t = new Transaction(doc, "Rename rooms"))
{
t.Start();
// ...modify the model...
t.Commit();
}Try to modify anything outside an open transaction and you get:
Autodesk.Revit.Exceptions.InvalidOperationException:
Attempting to modify the model outside of transaction.Transactions are what make Revit's undo stack work — the transaction's name is
what appears in the undo list. Nest them with SubTransaction, group them with
TransactionGroup, and roll back with t.RollBack() when something fails partway.
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 or from a modeless window's event
handler. When you need to — a dockable panel, a long-running task, a web request
coming back — you use ExternalEvent: you raise the event, and Revit calls your
handler when it's safe.
This constraint shapes every non-trivial add-in's architecture. Fight it and you get random crashes that don't reproduce.
Querying: FilteredElementCollector
Every read starts here. The pattern is: build a collector, apply filters, get elements.
from pyrevit import revit, DB, script
doc = revit.doc
out = script.get_output()
doors = (DB.FilteredElementCollector(doc)
.OfCategory(DB.BuiltInCategory.OST_Doors)
.WhereElementIsNotElementType() # instances, not types
.ToElements())
counts = {}
for door in doors:
type_name = (doc.GetElement(door.GetTypeId())
.get_Parameter(DB.BuiltInParameter.SYMBOL_NAME_PARAM)
.AsString())
counts[type_name] = counts.get(type_name, 0) + 1
out.print_md("## Doors: {} total".format(len(doors)))
for name, n in sorted(counts.items()):
out.print_md("- **{}** — {}".format(name, n))Three things in there that will save you time:
WhereElementIsNotElementType()vs.WhereElementIsElementType()— the difference between placed instances and the type definitions. Forgetting this is the most common beginner bug: your door count comes back as 4 because you counted types.- Order your filters cheap-to-expensive. Category and class filters are quick filters — they run against the element record without expanding it. Parameter value filters are slow filters and expand every element. Always narrow with a quick filter first.
BuiltInCategoryandBuiltInParameterare the enums for everything Revit ships with. Learn to find names in them; it's most of the API's surface area.
For a scoped query, pass a view id: FilteredElementCollector(doc, view.Id) only
collects what's visible in that view — much faster and often what you actually
want.
Writing: a real pyRevit script
Here's something genuinely useful — renumber the selected doors sequentially in click order:
from pyrevit import revit, DB, forms
doc = revit.doc
selection = revit.get_selection()
doors = [e for e in selection
if e.Category
and e.Category.Id.IntegerValue == int(DB.BuiltInCategory.OST_Doors)]
if not doors:
forms.alert("Select some doors first.", exitscript=True)
start = forms.ask_for_string(default="101", prompt="Starting number:")
if not start:
script_exit = forms.alert("Cancelled.", exitscript=True)
number = int(start)
# pyRevit's Transaction context manager commits on success, rolls back on error.
with revit.Transaction("RB: Renumber doors"):
for door in doors:
param = door.get_Parameter(DB.BuiltInParameter.ALL_MODEL_MARK)
if param and not param.IsReadOnly:
param.Set(str(number))
number += 1
forms.alert("Renumbered {} doors.".format(len(doors)))Notice revit.Transaction as a context manager — pyRevit wraps start/commit/
rollback for you, and an exception inside the block rolls back cleanly. That's the
kind of ergonomics that makes pyRevit worth using over raw API code.
Drop that file into a pyRevit extension as RB.tab/Tools.panel/Renumber.pushbutton/script.py
and you have a ribbon button.
Getting and setting parameters
The three ways to reach a parameter, in order of how much you should trust them:
# 1. Built-in enum — stable across versions and languages. Best.
p = element.get_Parameter(DB.BuiltInParameter.ALL_MODEL_MARK)
# 2. Shared parameter GUID — stable for custom parameters. Best for your own data.
p = element.get_Parameter(System.Guid("....-....-...."))
# 3. By name — breaks under localization and typos. Convenient, fragile.
p = element.LookupParameter("Mark")Then read with the right accessor for the storage type — AsString(),
AsDouble(), AsInteger(), AsElementId(), or AsValueString() for the
formatted display value.
The units trap.
AsDouble()on a length returns decimal feet, always, regardless of your project units. Set values in feet too, or convert withUnitUtils.ConvertToInternalUnits(value, UnitTypeId.Millimeters). This is the single most common bug in Revit scripts and it produces models that are subtly, catastrophically the wrong size.
A compiled add-in, minimally
When you graduate to C#, an add-in is two things: a class implementing
IExternalCommand, and a .addin manifest telling Revit where to find it.
using Autodesk.Revit.Attributes;
using Autodesk.Revit.DB;
using Autodesk.Revit.UI;
[Transaction(TransactionMode.Manual)]
public class CountWalls : IExternalCommand
{
public Result Execute(ExternalCommandData data, ref string message, ElementSet elements)
{
Document doc = data.Application.ActiveUIDocument.Document;
var walls = new FilteredElementCollector(doc)
.OfClass(typeof(Wall))
.WhereElementIsNotElementType()
.Cast<Wall>()
.ToList();
double totalLength = walls.Sum(w =>
w.get_Parameter(BuiltInParameter.CURVE_ELEM_LENGTH).AsDouble());
TaskDialog.Show("Walls",
$"{walls.Count} walls, {totalLength:F1} ft total.");
return Result.Succeeded;
}
}<?xml version="1.0" encoding="utf-8"?>
<RevitAddIns>
<AddIn Type="Command">
<Name>Count Walls</Name>
<Assembly>C:\RevitAddins\CountWalls.dll</Assembly>
<AddInId>0f8a4c1e-6b3d-4a2f-9c8e-1d5b7a9e3f21</AddInId>
<FullClassName>CountWalls</FullClassName>
<VendorId>RBC</VendorId>
</AddIn>
</RevitAddIns>The manifest goes in %AppData%\Autodesk\Revit\Addins\<version>\. AddInId must
be a unique GUID — generate a new one per add-in, don't copy this.
[Transaction(TransactionMode.Manual)] means you manage transactions yourself,
which is what you want. This command only reads, so it doesn't open one.
The .NET version trap
This is the thing that will actually stop you:
| Revit | .NET target |
|---|---|
| 2024 and earlier | .NET Framework 4.8 (net48) |
| 2025, 2026 | .NET 8 (net8.0-windows) |
| 2027 | .NET 10 (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. Same for Dynamo packages: once a package migrates to .NET 8 for Dynamo 3.x, it stops loading in older Dynamo.
The standard solution is one project multi-targeting several frameworks with conditional compilation, producing a DLL per Revit version. See the version matrix.
Dynamo, quickly
Manage → Dynamo. It's visual: nodes with inputs and outputs, wired together, run
top to bottom.
The nodes worth knowing on day one:
Categories→All Elements of Category— yourFilteredElementCollector.Element.GetParameterValueByName/Element.SetParameterByName— read and write.Setwraps its own transaction.Python Scriptnode — an escape hatch when the visual graph gets silly. You get the same API, and you choose IronPython or CPython.Data.ImportExcel/Data.ExportExcel— the reason most people open Dynamo at all.- List levels and lacing — the two concepts that make Dynamo click. Lacing (shortest/longest/cross-product) controls how two lists pair up; list levels control which nesting depth a node operates on. Every "why did I get one result instead of fifty" question is one of these two.
Run mode: switch from Automatic to Manual immediately. Automatic re-runs on every edit, which on a real model means modifying the document while you're still building the graph.
What's new, and what it means
Revit 2027 shipped a built-in AI Assistant (tech preview) that includes an MCP client, so Revit can talk to external Model Context Protocol servers. Autodesk has stated an intent to support both local and cloud MCP servers, but the Revit MCP server itself is still forthcoming rather than shipped. Treat it as a direction, not a platform to build on yet.
The stable, useful surfaces for getting data out of Revit and into other systems
remain: the desktop API, .nwc/IFC exports, and cloud-side APIs (Autodesk Platform
Services, the AEC Data Model) for model data without a Revit session.
Today's exercise
- Dynamo, read. Open Dynamo, set run mode to Manual. Build:
Categories (Doors)→All Elements of Category→Element.GetParameterValueByNamewith nameMark→ aWatchnode. Run. You should see your door marks. - Dynamo, write. Extend it:
Element.SetParameterByNamesettingCommentsto"Reviewed"on all doors. Run it. Go look at your door schedule. ThenCtrl+Zin Revit — confirm it undoes as one step. - Install pyRevit. Get the current release for your Revit version, install, restart Revit, confirm the pyRevit tab appears.
- Interactive console. pyRevit's console/REPL — get
doc, printdoc.Title, and collect all walls. Get comfortable poking at the model live. - Write the door-count script from above as a pyRevit script and run it.
- Write the renumber script from above. Select three doors, run it, verify the
marks changed and that
Ctrl+Zundoes it in one step. That undo behaviour is your transaction working. - Break it deliberately. Remove the
with revit.Transaction(...)wrapper and run it. Read the exception. Now you'll recognize it forever. - Units. Write a three-line script that prints a wall's length via
AsDouble()and viaAsValueString(). Note that one is feet and the other respects project units. This is the bug you'll otherwise ship. - Check your target. Find your Revit version's .NET target in the
version matrix and note it somewhere. If you plan
to build a C# add-in, this is the first line of your
.csproj.
Where to go from here
You've finished the bootcamp. The honest next steps:
- Model something real. Competence comes from a project with a deadline, not from more tutorials.
- Read your firm's template and standards with the understanding you now have. It'll make sense, and you'll see the decisions in it.
- Build one small tool for something that annoyed you this week. That's how people actually learn the API.
- Keep the reference library open. The concepts pages are written to be re-read, and the things that trip you up in month two are in there.