Blog / Creating C# Plugins for Revit: A Complete Developer Guide

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.

A
Archgyan Editor
· 16 min read

Go deeper with Archgyan Academy

Structured BIM and Revit learning paths for architects and students.

Explore Academy →

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:

AssemblyLocationPurpose
RevitAPI.dllC:\Program Files\Autodesk\Revit 2025\Core API - elements, documents, geometry, parameters
RevitAPIUI.dllC:\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.

  • 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

  1. Open Visual Studio 2022 and select Create a new project
  2. Choose Class Library (.NET Framework) - not .NET Core or .NET Standard
  3. Set the framework to .NET Framework 4.8
  4. Name the project MyFirstRevitPlugin
  5. Choose a location and click Create

Step 2 - Add Revit API References

  1. In Solution Explorer, right-click References and select Add Reference
  2. Click Browse and navigate to C:\Program Files\Autodesk\Revit 2025\
  3. Select both RevitAPI.dll and RevitAPIUI.dll
  4. Click Add, then OK
  5. For each reference, set Copy Local to False in 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:

  • IExternalCommand is the interface every Revit command plugin must implement. It requires a single Execute method.
  • [Transaction(TransactionMode.ReadOnly)] tells Revit this command only reads the model - it will not modify anything. Use TransactionMode.Manual when you need to make changes.
  • FilteredElementCollector is 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.
  • TaskDialog is Revit’s built-in dialog class. For simple messages, it works like MessageBox but 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 implements IExternalCommand.
  • AddInId - a unique GUID. Generate one in Visual Studio using Tools > Create GUID or use Guid.NewGuid() in C#. Every command needs its own unique GUID.

Where to Place the Manifest

Copy the .addin file to one of these locations:

LocationScope
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

  1. Always use TransactionMode.Manual when your command modifies the model
  2. Call tx.Start() before any modifications and tx.Commit() after
  3. Use a using block so the transaction is disposed even if an exception occurs
  4. Give each transaction a descriptive name - this becomes the undo label in Revit’s Edit menu
  5. Never nest transactions - use SubTransaction or TransactionGroup for complex multi-step operations
  6. If something goes wrong, call tx.RollBack() instead of tx.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

  1. Apply quick filters first. OfClass() and OfCategory() are “quick filters” that run at the database level before elements are loaded into memory. Always use these before slower LINQ queries.
  2. Avoid .ToElements() when you only need a count. Use .GetElementCount() instead.
  3. Scope to a view when possible. Passing a viewId to the collector limits the search to elements visible in that view, which is significantly faster for large models.
  4. Combine filters with IntersectionFilter instead 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

  1. Add a Resources folder to your Visual Studio project
  2. Add 32x32 and 16x16 PNG images for your button icons
  3. Set each image’s Build Action to Resource in the Properties panel
  4. 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 TypeRead MethodWrite Method
StringAsString()Set(string)
IntegerAsInteger()Set(int)
DoubleAsDouble()Set(double)
ElementIdAsElementId()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

  1. Build your project in Debug configuration
  2. Start Revit normally (not from Visual Studio)
  3. In Visual Studio, go to Debug > Attach to Process
  4. Find Revit.exe in the list and click Attach
  5. Set breakpoints in your code
  6. 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

  1. Create a folder for your plugin files (e.g., C:\RevitPlugins\MyTools\)
  2. Copy your DLL and any dependency DLLs into this folder
  3. Copy the .addin file to the appropriate Addins folder
  4. Make sure the Assembly path in the .addin file 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 .addin file in %ProgramData%\Autodesk\Revit\Addins\2025\
  • Handle multiple Revit versions by placing .addin files in each version’s folder
  • Include an uninstaller that removes both the DLL and .addin file

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.

  1. Not setting Copy Local to False on Revit API references. This causes assembly loading conflicts and crashes at startup.

  2. Forgetting the Transaction attribute. Every IExternalCommand class must have [Transaction(TransactionMode.Manual)] or [Transaction(TransactionMode.ReadOnly)].

  3. Using LINQ .Count() instead of .GetElementCount(). The LINQ method loads all elements into memory first. The collector method counts at the database level.

  4. Hardcoding file paths. Use Environment.GetFolderPath() and relative paths so your plugin works on different machines.

  5. Not handling the “no document open” case. Always check if commandData.Application.ActiveUIDocument is null before accessing the Document.

  6. Ignoring internal units. All measurements in the Revit API use feet (lengths) and radians (angles). Always convert for display.

  7. Running long operations on the main thread without a progress bar. Use IExternalEventHandler and ExternalEvent for asynchronous operations, and show a ProgressBar for operations that take more than a few seconds.

  8. 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 IExternalEventHandler for thread-safe model access
  • Updaters and events - react to model changes in real-time using IUpdater or document events like DocumentChanged
  • 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
Explore Archgyan Academy
← Back to Blog