Creating C# Plugins for Revit: A Complete Developer Guide
Learn how to build custom C# plugins for Autodesk Revit. Covers project setup, Revit API fundamentals, external commands, UI integration, and deployment.
Go deeper with Archgyan Academy
Structured BIM and Revit learning paths for architects and students.
Introduction
Autodesk Revit is one of the most widely used BIM tools in the AEC industry, but its out-of-the-box functionality only goes so far. Every firm has unique workflows, naming standards, and automation needs that Revit’s built-in features cannot address. This is where C# plugin development becomes a game-changer.
By writing custom plugins with C# and the Revit API, you can automate repetitive tasks, enforce company standards, extract data for reporting, and build entirely new tools that integrate directly into the Revit interface. Whether you want to batch-rename families, auto-generate sheets, or validate model quality before handoff, the Revit API gives you full programmatic access to the BIM model.
This guide walks you through the entire process of creating a Revit plugin in C# - from setting up your development environment to deploying your finished add-in. You do not need prior Revit API experience, but a basic understanding of C# syntax and object-oriented programming will help you follow along.
Prerequisites and Development Environment Setup
Before writing any code, you need the right tools installed and configured. The Revit API is a .NET Framework library, so your development stack must match.
Required Software
- Visual Studio 2022 (Community edition is free) with the ”.NET Desktop Development” workload installed
- Autodesk Revit 2024 or 2025 installed on the same machine (the API version must match your target Revit version)
- .NET Framework 4.8 (Revit 2024/2025 target this framework version)
Revit API Assemblies
The two core assemblies you will reference in every Revit plugin project are:
| Assembly | Location | Purpose |
|---|---|---|
RevitAPI.dll | C:\Program Files\Autodesk\Revit 2025\ | Core API - elements, documents, geometry, parameters |
RevitAPIUI.dll | C:\Program Files\Autodesk\Revit 2025\ | UI API - ribbons, dialogs, selection, task dialogs |
These DLLs ship with every Revit installation. You reference them in your project but never copy them to your output folder - Revit loads them at runtime.
Optional But Recommended
- RevitLookup - an open-source Revit add-in that lets you inspect the Revit database interactively. Install it from the RevitLookup GitHub repository. This tool is indispensable for understanding element hierarchies, parameter names, and family structures while developing.
- Add-In Manager - allows you to load and reload plugins without restarting Revit during development.
Creating Your First Revit Plugin Project
Let’s build a simple plugin that counts all wall elements in the current Revit model and displays the result in a dialog.
Step 1 - Create the Visual Studio Project
- Open Visual Studio 2022 and select Create a new project
- Choose Class Library (.NET Framework) - not .NET Core or .NET Standard
- Set the framework to .NET Framework 4.8
- Name the project
MyFirstRevitPlugin - Choose a location and click Create
Step 2 - Add Revit API References
- In Solution Explorer, right-click References and select Add Reference
- Click Browse and navigate to
C:\Program Files\Autodesk\Revit 2025\ - Select both
RevitAPI.dllandRevitAPIUI.dll - Click Add, then OK
- For each reference, set Copy Local to
Falsein the Properties panel. This is critical - if you copy these DLLs to your output folder, Revit may load conflicting versions and crash
Step 3 - Write the External Command
Replace the default class file contents with:
using Autodesk.Revit.Attributes;
using Autodesk.Revit.DB;
using Autodesk.Revit.UI;
namespace MyFirstRevitPlugin
{
[Transaction(TransactionMode.ReadOnly)]
public class WallCounter : IExternalCommand
{
public Result Execute(
ExternalCommandData commandData,
ref string message,
ElementSet elements)
{
// Get the active document
Document doc = commandData.Application
.ActiveUIDocument.Document;
// Collect all wall instances in the model
FilteredElementCollector collector =
new FilteredElementCollector(doc);
IList<Element> walls = collector
.OfClass(typeof(Wall))
.WhereElementIsNotElementType()
.ToElements();
// Display the count
TaskDialog.Show("Wall Counter",
$"This model contains {walls.Count} wall instances.");
return Result.Succeeded;
}
}
}
Key points about this code:
IExternalCommandis the interface every Revit command plugin must implement. It requires a singleExecutemethod.[Transaction(TransactionMode.ReadOnly)]tells Revit this command only reads the model - it will not modify anything. UseTransactionMode.Manualwhen you need to make changes.FilteredElementCollectoris the primary way to query elements from the Revit database. It is fast and memory-efficient because it filters at the database level rather than loading all elements into memory.TaskDialogis Revit’s built-in dialog class. For simple messages, it works likeMessageBoxbut follows Revit’s UI conventions.
Step 4 - Build the Project
Press Ctrl+Shift+B to build. The output DLL will be in bin\Debug\MyFirstRevitPlugin.dll.
The Add-In Manifest File
Revit discovers plugins through .addin manifest files placed in specific folders. Without this file, Revit does not know your plugin exists.
Create a file named MyFirstRevitPlugin.addin with the following content:
<?xml version="1.0" encoding="utf-8"?>
<RevitAddIns>
<AddIn Type="Command">
<Name>Wall Counter</Name>
<Assembly>C:\RevitPlugins\MyFirstRevitPlugin.dll</Assembly>
<FullClassName>MyFirstRevitPlugin.WallCounter</FullClassName>
<AddInId>A1B2C3D4-E5F6-7890-ABCD-EF1234567890</AddInId>
<VendorId>YourCompany</VendorId>
<VendorDescription>Your Company Name</VendorDescription>
</AddIn>
</RevitAddIns>
Important fields:
Assembly- the full path to your compiled DLL. Update this to match your actual output location.FullClassName- the namespace-qualified class name that implementsIExternalCommand.AddInId- a unique GUID. Generate one in Visual Studio using Tools > Create GUID or useGuid.NewGuid()in C#. Every command needs its own unique GUID.
Where to Place the Manifest
Copy the .addin file to one of these locations:
| Location | Scope |
|---|---|
C:\ProgramData\Autodesk\Revit\Addins\2025\ | All users on this machine |
%AppData%\Autodesk\Revit\Addins\2025\ | Current user only |
The folder name (2025) must match your Revit version. After placing the file, restart Revit. Your command will appear under the Add-Ins tab in the External Tools dropdown.
Understanding Revit Transactions
Any operation that modifies the Revit model must be wrapped in a Transaction. This is one of the most common sources of errors for beginners - attempting to change an element without an open transaction will throw an InvalidOperationException.
Basic Transaction Pattern
[Transaction(TransactionMode.Manual)]
public class RenameWalls : IExternalCommand
{
public Result Execute(
ExternalCommandData commandData,
ref string message,
ElementSet elements)
{
Document doc = commandData.Application
.ActiveUIDocument.Document;
FilteredElementCollector collector =
new FilteredElementCollector(doc);
IList<Element> walls = collector
.OfClass(typeof(Wall))
.WhereElementIsNotElementType()
.ToElements();
using (Transaction tx = new Transaction(doc, "Rename Walls"))
{
tx.Start();
foreach (Element wall in walls)
{
Parameter commentParam = wall.LookupParameter("Comments");
if (commentParam != null && !commentParam.IsReadOnly)
{
commentParam.Set("Reviewed");
}
}
tx.Commit();
}
TaskDialog.Show("Done",
$"Updated comments on {walls.Count} walls.");
return Result.Succeeded;
}
}
Transaction Rules to Remember
- Always use
TransactionMode.Manualwhen your command modifies the model - Call
tx.Start()before any modifications andtx.Commit()after - Use a
usingblock so the transaction is disposed even if an exception occurs - Give each transaction a descriptive name - this becomes the undo label in Revit’s Edit menu
- Never nest transactions - use
SubTransactionorTransactionGroupfor complex multi-step operations - If something goes wrong, call
tx.RollBack()instead oftx.Commit()to undo all changes within that transaction
Error Handling Pattern
A production-quality command should wrap the transaction in try-catch:
using (Transaction tx = new Transaction(doc, "My Operation"))
{
tx.Start();
try
{
// Modify elements here
tx.Commit();
}
catch (Exception ex)
{
tx.RollBack();
message = ex.Message;
return Result.Failed;
}
}
Working with the FilteredElementCollector
The FilteredElementCollector is the workhorse of the Revit API. Understanding how to use it efficiently is essential for plugin performance.
Common Filter Patterns
// All doors in the model
var doors = new FilteredElementCollector(doc)
.OfCategory(BuiltInCategory.OST_Doors)
.WhereElementIsNotElementType()
.ToElements();
// All floor types (family types, not instances)
var floorTypes = new FilteredElementCollector(doc)
.OfClass(typeof(FloorType))
.ToElements();
// All elements in a specific view
var viewElements = new FilteredElementCollector(doc, viewId)
.WhereElementIsNotElementType()
.ToElements();
// Rooms with a specific parameter value
var largeRooms = new FilteredElementCollector(doc)
.OfCategory(BuiltInCategory.OST_Rooms)
.WhereElementIsNotElementType()
.Cast<Room>()
.Where(r => r.Area > 50.0)
.ToList();
Performance Best Practices
- Apply quick filters first.
OfClass()andOfCategory()are “quick filters” that run at the database level before elements are loaded into memory. Always use these before slower LINQ queries. - Avoid
.ToElements()when you only need a count. Use.GetElementCount()instead. - Scope to a view when possible. Passing a
viewIdto the collector limits the search to elements visible in that view, which is significantly faster for large models. - Combine filters with
IntersectionFilterinstead of running multiple collectors:
var categoryFilter =
new ElementCategoryFilter(BuiltInCategory.OST_Walls);
var classFilter =
new ElementClassFilter(typeof(FamilyInstance));
var combinedFilter =
new LogicalAndFilter(categoryFilter, classFilter);
var results = new FilteredElementCollector(doc)
.WherePasses(combinedFilter)
.ToElements();
Adding a Ribbon Button and Custom UI
The External Tools dropdown works for testing, but a proper plugin should have its own ribbon tab with buttons. This requires implementing IExternalApplication instead of (or alongside) IExternalCommand.
Creating a Ribbon Tab
public class MyApp : IExternalApplication
{
public Result OnStartup(UIControlledApplication app)
{
// Create a custom ribbon tab
string tabName = "My Tools";
app.CreateRibbonTab(tabName);
// Create a panel within the tab
RibbonPanel panel = app.CreateRibbonPanel(tabName, "Utilities");
// Get the path to this assembly
string assemblyPath =
Assembly.GetExecutingAssembly().Location;
// Create a button for the WallCounter command
PushButtonData buttonData = new PushButtonData(
"WallCounter", // internal name
"Count\nWalls", // button label (use \n for two lines)
assemblyPath,
"MyFirstRevitPlugin.WallCounter" // full class name
);
// Set button icon (32x32 for large, 16x16 for small)
buttonData.LargeImage = new BitmapImage(
new Uri("pack://application:,,,/MyFirstRevitPlugin;component/Resources/icon32.png")
);
buttonData.ToolTip = "Counts all wall instances in the current model";
PushButton button = panel.AddItem(buttonData) as PushButton;
return Result.Succeeded;
}
public Result OnShutdown(UIControlledApplication app)
{
return Result.Succeeded;
}
}
Updating the Manifest for an Application
When you use IExternalApplication, the .addin manifest changes slightly:
<RevitAddIns>
<AddIn Type="Application">
<Name>My Tools</Name>
<Assembly>C:\RevitPlugins\MyFirstRevitPlugin.dll</Assembly>
<FullClassName>MyFirstRevitPlugin.MyApp</FullClassName>
<AddInId>B2C3D4E5-F6A7-8901-BCDE-F12345678901</AddInId>
<VendorId>YourCompany</VendorId>
<VendorDescription>Your Company Name</VendorDescription>
</AddIn>
</RevitAddIns>
Note that the Type is now Application instead of Command. You can include both Application and Command entries in the same .addin file.
Adding Icons to Your Buttons
- Add a
Resourcesfolder to your Visual Studio project - Add 32x32 and 16x16 PNG images for your button icons
- Set each image’s Build Action to
Resourcein the Properties panel - Reference them using the
pack://URI scheme as shown above
A button without an icon still works but looks unprofessional. Aim for simple, recognizable icons that communicate the command’s purpose at a glance.
Reading and Writing Parameters
Parameters are how data is stored on Revit elements. Understanding how to read and write them is fundamental to most plugins.
Built-In Parameters vs Shared Parameters
// Reading a built-in parameter by BuiltInParameter enum
Parameter levelParam = wall.get_Parameter(
BuiltInParameter.WALL_BASE_CONSTRAINT);
string levelName = levelParam.AsValueString();
// Reading a shared/project parameter by name
Parameter customParam = wall.LookupParameter("Fire Rating");
if (customParam != null)
{
string value = customParam.AsString();
}
// Writing a parameter value (must be inside a Transaction)
Parameter comments = wall.LookupParameter("Comments");
if (comments != null && !comments.IsReadOnly)
{
comments.Set("Checked by plugin");
}
Parameter Types and Value Access
| Parameter Storage Type | Read Method | Write Method |
|---|---|---|
String | AsString() | Set(string) |
Integer | AsInteger() | Set(int) |
Double | AsDouble() | Set(double) |
ElementId | AsElementId() | Set(ElementId) |
Important: AsDouble() returns values in Revit’s internal units (feet for length, radians for angles). Use UnitUtils.ConvertFromInternalUnits() to convert to display units.
double lengthFeet = wall.get_Parameter(
BuiltInParameter.CURVE_ELEM_LENGTH).AsDouble();
double lengthMeters = UnitUtils.ConvertFromInternalUnits(
lengthFeet, UnitTypeId.Meters);
Creating a Practical Plugin - Model Quality Checker
Let’s combine everything into a useful real-world plugin that checks a model for common quality issues.
[Transaction(TransactionMode.ReadOnly)]
public class ModelChecker : IExternalCommand
{
public Result Execute(
ExternalCommandData commandData,
ref string message,
ElementSet elements)
{
Document doc = commandData.Application
.ActiveUIDocument.Document;
var issues = new List<string>();
// Check 1: Walls with zero length
var zeroWalls = new FilteredElementCollector(doc)
.OfClass(typeof(Wall))
.WhereElementIsNotElementType()
.Cast<Wall>()
.Where(w =>
{
Parameter len = w.get_Parameter(
BuiltInParameter.CURVE_ELEM_LENGTH);
return len != null && len.AsDouble() < 0.01;
})
.ToList();
if (zeroWalls.Count > 0)
issues.Add(
$"Found {zeroWalls.Count} walls with near-zero length");
// Check 2: Rooms without names
var unnamedRooms = new FilteredElementCollector(doc)
.OfCategory(BuiltInCategory.OST_Rooms)
.WhereElementIsNotElementType()
.Cast<Room>()
.Where(r =>
string.IsNullOrWhiteSpace(
r.get_Parameter(BuiltInParameter.ROOM_NAME)
?.AsString()))
.ToList();
if (unnamedRooms.Count > 0)
issues.Add(
$"Found {unnamedRooms.Count} rooms without names");
// Check 3: Unplaced rooms
var unplacedRooms = new FilteredElementCollector(doc)
.OfCategory(BuiltInCategory.OST_Rooms)
.WhereElementIsNotElementType()
.Cast<Room>()
.Where(r => r.Area == 0)
.ToList();
if (unplacedRooms.Count > 0)
issues.Add(
$"Found {unplacedRooms.Count} unplaced rooms (zero area)");
// Check 4: Warnings count
IList<FailureMessage> warnings = doc.GetWarnings();
if (warnings.Count > 0)
issues.Add(
$"Model has {warnings.Count} active warnings");
// Display results
string report = issues.Count > 0
? string.Join("\n", issues)
: "No issues found. Model looks clean.";
TaskDialog td = new TaskDialog("Model Quality Report");
td.MainInstruction = issues.Count > 0
? $"Found {issues.Count} issue(s)"
: "All checks passed";
td.MainContent = report;
td.MainIcon = issues.Count > 0
? TaskDialogIcon.TaskDialogIconWarning
: TaskDialogIcon.TaskDialogIconNone;
td.Show();
return Result.Succeeded;
}
}
This kind of quality checker is something every BIM team needs. You can extend it with checks for naming conventions, parameter completeness, correct level assignments, and model extents.
Debugging Your Plugin
Debugging Revit plugins requires attaching Visual Studio to the running Revit process.
Step-by-Step Debugging Setup
- Build your project in Debug configuration
- Start Revit normally (not from Visual Studio)
- In Visual Studio, go to Debug > Attach to Process
- Find
Revit.exein the list and click Attach - Set breakpoints in your code
- Run your command from Revit - Visual Studio will break at your breakpoints
Automating the Debug Workflow
Add a post-build event in your project properties to automatically copy the DLL to the add-ins folder:
xcopy /Y "$(TargetDir)$(TargetFileName)" "C:\RevitPlugins\"
Then set the project’s Debug settings:
- Start external program:
C:\Program Files\Autodesk\Revit 2025\Revit.exe - This lets you press F5 in Visual Studio to launch Revit with the debugger already attached.
Common Debugging Pitfalls
- Revit locks the DLL while running. You must close Revit to rebuild. Use an Add-In Manager tool to hot-reload during development.
- Missing references at runtime often mean you forgot to set Copy Local to False for the Revit API DLLs.
- Transaction errors almost always mean you tried to modify the model outside of a transaction, or you forgot to call
Start(). - NullReferenceException on parameters - always check if
LookupParameter()returns null before accessing its value.
Deployment and Distribution
When your plugin is ready for your team or clients, you need a clean deployment strategy.
Manual Deployment
- Create a folder for your plugin files (e.g.,
C:\RevitPlugins\MyTools\) - Copy your DLL and any dependency DLLs into this folder
- Copy the
.addinfile to the appropriate Addins folder - Make sure the
Assemblypath in the.addinfile points to the correct DLL location
Creating an Installer
For wider distribution, build a Windows Installer (MSI) or use a tool like Inno Setup:
- Copy the DLL to a standard location (
%ProgramFiles%\YourCompany\PluginName\) - Place the
.addinfile in%ProgramData%\Autodesk\Revit\Addins\2025\ - Handle multiple Revit versions by placing
.addinfiles in each version’s folder - Include an uninstaller that removes both the DLL and
.addinfile
Version Compatibility
Revit plugins compiled against one version’s API may not work with other versions. Best practices:
- Target the oldest Revit version you need to support - forward compatibility usually works within the same major API generation
- Build separate DLLs for significantly different API versions (e.g., 2023 vs 2025)
- Use conditional compilation (
#if REVIT2025) for version-specific API calls - Test on every target version before releasing
Common Mistakes and How to Avoid Them
Even experienced developers run into these issues when starting with the Revit API.
-
Not setting Copy Local to False on Revit API references. This causes assembly loading conflicts and crashes at startup.
-
Forgetting the Transaction attribute. Every
IExternalCommandclass must have[Transaction(TransactionMode.Manual)]or[Transaction(TransactionMode.ReadOnly)]. -
Using LINQ
.Count()instead of.GetElementCount(). The LINQ method loads all elements into memory first. The collector method counts at the database level. -
Hardcoding file paths. Use
Environment.GetFolderPath()and relative paths so your plugin works on different machines. -
Not handling the “no document open” case. Always check if
commandData.Application.ActiveUIDocumentis null before accessing the Document. -
Ignoring internal units. All measurements in the Revit API use feet (lengths) and radians (angles). Always convert for display.
-
Running long operations on the main thread without a progress bar. Use
IExternalEventHandlerandExternalEventfor asynchronous operations, and show aProgressBarfor operations that take more than a few seconds. -
Not disposing of
FilteredElementCollector. While the garbage collector will eventually clean it up, disposing it promptly releases database resources.
Next Steps and Resources
Once you are comfortable with the basics covered in this guide, here are areas to explore next:
- Modeless dialogs with WPF - build full-featured UI panels that stay open while users work in Revit, using
IExternalEventHandlerfor thread-safe model access - Updaters and events - react to model changes in real-time using
IUpdateror document events likeDocumentChanged - Revit API documentation - the official Revit API Docs site provides searchable class references for all API versions
- Jeremy Tammik’s blog - The Building Coder is the most comprehensive resource for Revit API development, with over 2,000 posts covering every aspect of the API
- Dynamo integration - you can call Revit API methods from Dynamo’s Python nodes, or create custom Dynamo nodes in C# that wrap your plugin logic
If you are looking to deepen your Revit skills alongside programming, the Archgyan Academy offers structured courses on Revit workflows that complement your development knowledge with practical modeling expertise.
Conclusion
Building C# plugins for Revit opens up a powerful dimension of BIM work that goes beyond manual modeling. With the patterns covered in this guide - external commands, transactions, filtered element collectors, ribbon UI, parameter access, and deployment - you have the foundation to automate virtually any Revit workflow.
Start small with a simple utility that solves a real problem in your daily work. A wall counter or parameter checker might seem basic, but it teaches you the core API patterns that scale to complex tools. As you build more plugins, you will develop an intuition for how Revit’s database is structured and how to manipulate it efficiently.
The Revit API surface is large, but the patterns are consistent. Once you understand how transactions, collectors, and parameters work, you can tackle almost any automation challenge your firm needs.
Level up your skills
Ready to learn hands-on?
- Project-based Revit & BIM courses for architects
- Go from beginner to confident professional
- Video lessons you can follow at your own pace