If you can write a loop in C# and you know what a structural beam is, you already have enough to build a working Revit plugin. Not a demo, not a tutorial that stops before the hard part—an actual add-in that queries real structural elements, reads their parameters, and writes results back to the model. This guide gets you there in under two hours, with code you can copy, run, and break in your own project.
The Revit API is genuinely powerful for structural engineers. Automating repetitive tasks like beam schedule extraction, section verification checks, or load combination tagging can save hours per project. The barrier is usually the unfamiliar ecosystem—IExternalCommand, FilteredElementCollector, Transaction—not the difficulty of the logic itself. Once those three concepts click, most structural automation tasks become straightforward C# problems.
Before the table of contents: the core answer is this. To build a Revit plugin, you create a .NET class library targeting .NET 4.8 (for Revit 2024 and earlier), implement IExternalCommand, reference RevitAPI.dll and RevitAPIUI.dll, write an .addin manifest file, and drop both into %AppData%AutodeskRevitAddins[version]. Everything else in this article is the detail behind those five steps.
Table of Contents
▼ Collapse
- Why Structural Engineers Should Learn the Revit C# API
- Environment Setup: SDK, Visual Studio, and .NET Target
- Your First IExternalCommand: Hello, Structural Model
- The .addin Manifest File Explained
- FilteredElementCollector: Querying Structural Elements
- Reading and Writing Parameters Inside a Transaction
- Real-World Plugin: Beam Span-to-Depth Ratio Checker
- Adding a Simple UI with TaskDialog and RibbonPanel
- Revit API C# vs. Dynamo vs. pyRevit: Which Should You Use?
- Debugging Revit Plugins Without Losing Your Mind
- SDK Downloads, Books, and Tools
- Frequently Asked Questions
Why Structural Engineers Should Learn the Revit C# API
The honest answer to “why bother with C# when I can use Dynamo?” is task complexity and repeatability. Dynamo is great for parametric geometry and visual logic. It is slow, brittle on large models, and cannot reliably automate multi-document workflows or run as a background task. The Revit API through C# has no such limits.
Consider three specific problems structural engineers face every week:
- Beam schedule extraction to Excel: Manually exporting schedules, filtering by level, adding calculated columns. A 50-line C# plugin does this in seconds without touching the UI.
- Parameter validation before issuing: Checking that every structural column has a Structural Usage parameter set, that no beams are missing fire rating, that all foundations reference a soil bearing capacity. These checks take minutes in code, hours manually.
- Cross-model coordination: Comparing a structural model against an architectural model and flagging dimension mismatches. Nearly impossible in Dynamo. Manageable in C# using the Revit API’s linked document access.
⚡ Automation Impact: Manual vs Plugin (Typical Structural Project)
| Task | Manual Time | Plugin Time | Time Saved |
|---|---|---|---|
| Beam schedule export (200 beams) | 45 min | 8 sec | ~99% |
| Parameter QA check (full model) | 2 hrs | 12 sec | ~99% |
| Renaming 500 views to standard | 1.5 hrs | 6 sec | ~99% |
| Sheet numbering + PDF export | 30 min | 20 sec | ~98% |
Times based on typical mid-rise structural model. Savings scale with model complexity.
The structural engineering community on Reddit (r/AutodeskRevit and r/civilengineering) consistently reports that engineers who can write even basic Revit macros become the most-valued team members in medium-to-large firms. A highly-upvoted thread from a senior structural BIM manager put it plainly: “The moment someone on my team writes a parameter checker that works, they never do manual QA again.”
Environment Setup: SDK, Visual Studio, and .NET Target
This is where most beginner guides are vague. Here is exactly what you need, with version specifics that actually matter.
What You Need
- Revit installed (any version 2019–2025). Note your exact version number—it determines which SDK and which .NET target you use.
- Visual Studio 2022 Community (free). Download from visualstudio.microsoft.com. Install with the .NET desktop development workload.
- Revit SDK. Download from Autodesk Developer Network. The SDK installs alongside Revit 2024+ or is available as a standalone download for older versions.
RevitAPI.dll and RevitAPIUI.dll references in Visual Studio. If you leave it as True, Visual Studio copies these large DLLs into your build output. Revit already has them. Copying causes version conflicts and bloats your deploy folder.
Creating the Project
- Open Visual Studio → New Project → Class Library (.NET Framework). Do not pick “Class Library (.NET)”—that is .NET Core/5+, which will not work with Revit 2024.
- Name your project (e.g.,
StructuralBeamChecker). Set Target Framework to .NET Framework 4.8. - Right-click References → Add Reference → Browse → navigate to
C:Program FilesAutodeskRevit 2024→ selectRevitAPI.dllandRevitAPIUI.dll. - For each reference: click it in Solution Explorer → Properties → set Copy Local = False.
Getting started with the Revit API — official Autodesk Developer walkthrough
Your First IExternalCommand: Hello, Structural Model
IExternalCommand is the interface your plugin class must implement. It has exactly one method: Execute(). When a user clicks your button in the Revit ribbon, Revit calls Execute() and passes three objects you care about: ExternalCommandData (gives you the UI application and the active view), an output message string (for error messages), and an ElementSet (for highlighting elements in case of failure).
using Autodesk.Revit.Attributes; using Autodesk.Revit.DB; using Autodesk.Revit.UI; [Transaction(TransactionMode.Manual)] [Regeneration(RegenerationOption.Manual)] public class HelloStructuralCommand : IExternalCommand { public Result Execute( ExternalCommandData commandData, ref string message, ElementSet elements) { var uiApp = commandData.Application; var uiDoc = uiApp.ActiveUIDocument; var doc = uiDoc.Document; // Count structural framing elements var collector = new FilteredElementCollector(doc) .OfCategory(BuiltInCategory.OST_StructuralFraming) .WhereElementIsNotElementType(); int count = collector.GetElementCount(); TaskDialog.Show( "Structural Model Info", $"Found {count} structural framing elements in {doc.Title}" ); return Result.Succeeded; } }
Three things in that code are worth understanding before moving on:
[Transaction(TransactionMode.Manual)]— This attribute is required. Without it, Revit throws aInvalidOperationExceptionbefore your code even runs. Manual mode means you control when transactions open and close.Result.Succeeded— Return this unless something went wrong.Result.Failedtriggers an error dialog.Result.Cancelledsilently exits.FilteredElementCollector— This is how you get elements. It is the Revit API’s primary query mechanism and works on the active document by default, or you can pass a specific document or view ID.
WhereElementIsNotElementType() on your collector. Without it, you get both element instances AND their type definitions in the same collection—which means you might process the same “W10x49” family type 40 times before you process any actual beam.
The .addin Manifest File Explained
Revit finds your plugin through a plain XML file with an .addin extension. You drop this file into one of two locations:
- Per-user:
%AppData%AutodeskRevitAddins2024(replaces 2024 with your version) - All users:
C:ProgramDataAutodeskRevitAddins2024
<?xml version="1.0" encoding="utf-8" ?> <RevitAddIns> <AddIn Type="Command"> <Name>Structural Beam Checker</Name> <Assembly>C:PluginsStructuralBeamCheckerStructuralBeamChecker.dll</Assembly> <AddInId>12345678-ABCD-1234-ABCD-123456789ABC</AddInId> <FullClassName>StructuralBeamChecker.HelloStructuralCommand</FullClassName> <VendorId>YOURCO</VendorId> <VendorDescription>Your Company Name</VendorDescription> </AddIn> </RevitAddIns>
The AddInId must be a unique GUID. Generate one in Visual Studio via Tools → Create GUID or use guidgenerator.com. Two plugins with the same GUID cause one to silently fail to load—a maddening bug to diagnose.
%AppData%AutodeskRevitAddins2024 folder. Then write a post-build event that copies your .addin file there too. You can restart Revit and test without any manual file copying.
FilteredElementCollector: Querying Structural Elements
FilteredElementCollector is the most important class in the Revit API for structural engineers. Understanding it well means you can query any element in any document in any way. Here is how it works conceptually:
The collector starts with all elements in the document (or view, or element list). You apply filters to narrow it down. Filters are either quick filters (run first, check element properties stored in memory—fast) or slow filters (require the element to be fully loaded from disk). Chaining multiple filters: quick filters run before slow ones regardless of the order you write them.
🔍 FilteredElementCollector: Quick vs Slow Filters
⚡ Quick Filters (fast)
OfCategory()OfClass()WhereElementIsNotElementType()WhereElementIsElementType()WherePasses(new BoundingBoxIntersectsFilter(...))
🐌 Slow Filters (load from disk)
WherePasses(new FamilyInstanceFilter(...))WherePasses(new RoomFilter())LINQ .Where(e => e.LookupParameter(...))- Any parameter-value-based filtering
Performance tip: Always apply quick filters first, then slow filters. The Revit API applies quick filters ahead of slow ones internally, but being explicit makes your intent clear and prevents accidental full-model scans.
Common Structural Categories
// Beams and horizontal framing (W-sections, HSS, timber beams, etc.) var beams = new FilteredElementCollector(doc) .OfCategory(BuiltInCategory.OST_StructuralFraming) .WhereElementIsNotElementType() .ToElements(); // Columns (structural) var columns = new FilteredElementCollector(doc) .OfCategory(BuiltInCategory.OST_StructuralColumns) .WhereElementIsNotElementType() .ToElements(); // Structural foundations (isolated footings, mat, grade beams) var foundations = new FilteredElementCollector(doc) .OfCategory(BuiltInCategory.OST_StructuralFoundation) .WhereElementIsNotElementType() .ToElements(); // Walls (structural walls only — filter by Structural Usage parameter) var walls = new FilteredElementCollector(doc) .OfCategory(BuiltInCategory.OST_Walls) .WhereElementIsNotElementType() .Cast<Wall>() .Where(w => w.StructuralUsage == StructuralWallUsage.Bearing) .ToList();
Reading and Writing Parameters Inside a Transaction
Every change to a Revit model must happen inside an open Transaction. Reading parameters does not—you can read at any time. But writing a parameter value, changing an element’s type, or moving geometry: all of these require a transaction.
// ---- READING a parameter (no transaction needed) ---- foreach (var elem in beams) { // By BuiltInParameter (fastest - direct access, no string matching) Parameter levelParam = elem.get_Parameter( BuiltInParameter.STRUCTURAL_REFERENCE_LEVEL_OFFSET); // By parameter name (slower - string search, use only if BuiltIn unavailable) Parameter customParam = elem.LookupParameter("Fire Rating"); if (customParam != null && customParam.HasValue) { string rating = customParam.AsString(); // use rating... } } // ---- WRITING a parameter (must be inside a Transaction) ---- using (var tx = new Transaction(doc, "Set Beam Mark")) { tx.Start(); try { foreach (var elem in beams) { Parameter markParam = elem.LookupParameter("Mark"); if (markParam != null && !markParam.IsReadOnly) markParam.Set($"B-{elem.Id.IntegerValue}"); } tx.Commit(); } catch (Exception ex) { tx.RollBack(); message = ex.Message; // shown in Revit error dialog return Result.Failed; } }
AsDouble() ≈ 19.685 (feet). Convert using UnitUtils.ConvertFromInternalUnits(value, UnitTypeId.Meters) in Revit 2022+ or UnitUtils.Convert(value, DisplayUnitType.DUT_DECIMAL_FEET, DisplayUnitType.DUT_METERS) in older versions.
Revit API: Working with Parameters in C# — practical examples
Real-World Plugin: Beam Span-to-Depth Ratio Checker
Preliminary span-to-depth ratio checks are something every structural engineer does manually. For steel wide-flange beams, AISC recommends span/depth ratios typically in the range of L/12 to L/20 for initial sizing. For concrete T-beams, ACI 318 Table 9.3.1.1 gives minimum depths as L/16 to L/21. A plugin that flags beams outside these limits in a large model saves real checking time.
[Transaction(TransactionMode.ReadOnly)] public class BeamSpanDepthChecker : IExternalCommand { // Span-to-depth ratio limits (unitless) const double MAX_RATIO_STEEL = 20.0; const double MAX_RATIO_CONCRETE = 21.0; const double MIN_RATIO = 8.0; // flag if surprisingly deep public Result Execute(ExternalCommandData data, ref string message, ElementSet elements) { var doc = data.Application.ActiveUIDocument.Document; var report = new System.Text.StringBuilder(); int warnings = 0; var beams = new FilteredElementCollector(doc) .OfCategory(BuiltInCategory.OST_StructuralFraming) .WhereElementIsNotElementType() .Cast<FamilyInstance>(); foreach (var beam in beams) { // Get span (Revit internal = feet; convert to mm) Parameter lengthP = beam.get_Parameter( BuiltInParameter.INSTANCE_LENGTH_PARAM); if (lengthP == null) continue; double spanMm = UnitUtils.ConvertFromInternalUnits( lengthP.AsDouble(), UnitTypeId.Millimeters); // Get section depth from type FamilySymbol sym = doc.GetElement(beam.GetTypeId()) as FamilySymbol; if (sym == null) continue; Parameter depthP = sym.LookupParameter("b") // W-shape depth ?? sym.LookupParameter("d") // alternative naming ?? sym.LookupParameter("Depth"); if (depthP == null) continue; double depthMm = UnitUtils.ConvertFromInternalUnits( depthP.AsDouble(), UnitTypeId.Millimeters); if (depthMm < 1.0) continue; // skip zero-depth double ratio = spanMm / depthMm; string mark = beam.get_Parameter( BuiltInParameter.ALL_MODEL_MARK)?.AsString() ?? beam.Id.ToString(); bool flag = ratio > MAX_RATIO_STEEL || ratio < MIN_RATIO; if (flag) { report.AppendLine($" [{mark}] L={spanMm:F0} mm d={depthMm:F0} mm L/d={ratio:F1}"); warnings++; } } string msg = warnings == 0 ? "All beams within acceptable L/d limits." : $"{warnings} beams outside L/d limits:nn{report}"; TaskDialog.Show("Beam Span-to-Depth Check", msg); return Result.Succeeded; } }
Span-to-Depth Ratio — AISC Preliminary Sizing Rule of Thumb
=
( frac{text{Clear Span (mm)}}{text{Section Depth (mm)}} )
Target range for steel wide-flange beams: L/d ≈ 12 to 20 for typical floor loading (AISC Design Guide 3). Concrete T-beams: minimum depth per ACI 318-19 Table 9.3.1.1 equals L/16 to L/21 (one-way, non-prestressed).
Adding a Simple UI with TaskDialog and RibbonPanel
TaskDialog covers most simple output needs, but once you want a form with input fields, you use WPF. That is a full article on its own. What engineers need first is knowing how to add a proper ribbon button—so your plugin shows up in the Add-Ins tab with an icon rather than just in the external commands list.
To add a ribbon button, implement IExternalApplication instead of (or in addition to) IExternalCommand:
public class StructuralApp : IExternalApplication { public Result OnStartup(UIControlledApplication app) { RibbonPanel panel = app.CreateRibbonPanel("Structural Tools"); string dllPath = typeof(StructuralApp).Assembly.Location; var btnData = new PushButtonData( "BeamChecker", "BeamnL/d Check", dllPath, "StructuralBeamChecker.BeamSpanDepthChecker"); btnData.ToolTip = "Checks all beams for out-of-range span-to-depth ratios"; // btnData.LargeImage = new BitmapImage(new Uri(iconPath)); // 32x32 PNG panel.AddItem(btnData); return Result.Succeeded; } public Result OnShutdown(UIControlledApplication app) => Result.Succeeded; }
Update your .addin file to reference the application class (change Type="Command" to Type="Application" and update FullClassName). Your command class stays separate—the application just adds the ribbon button that calls it.
Revit API C# vs. Dynamo vs. pyRevit: Which Should You Use?
The honest recommendation for most structural engineers: start with pyRevit for quick utility scripts (no compile cycle, instant iteration), and move to C# when you need a tool that handles models with 10,000+ elements, requires a WPF form, or needs to be deployed across a team reliably. The Revit API knowledge transfers directly—you use the same classes and methods in both.
Debugging Revit Plugins Without Losing Your Mind
Debugging a Revit plugin is awkward the first time: you cannot just press F5 and step through code because Revit is the host process. Here are the approaches that actually work:
Attach Visual Studio Debugger to Revit
- Build your DLL in Debug configuration.
- Start Revit manually (do not use Visual Studio’s Start button).
- In Visual Studio: Debug → Attach to Process → Revit.exe.
- Set a breakpoint in your Execute() method.
- Trigger your command in Revit. Visual Studio pauses at the breakpoint.
🛠 Debugging Tricks That Save Hours
- RevitLookup: The single most useful tool for Revit development. Install via GitHub. It lets you click any Revit element and browse all its parameters, type data, geometry, and relationships in a tree view—essential for discovering the exact parameter names and BuiltInParameter enums you need.
- ReSharper or Rider: Both have Revit-aware IntelliSense extensions. JetBrains Rider is increasingly popular for Revit development over Visual Studio.
- Add-In Manager: From Autodesk Labs, lets you reload DLLs without restarting Revit. Download from the chuongmep RevitAddInManager GitHub.
- Write to a log file during debugging:
File.AppendAllText(@"C:revit_debug.txt", $"{DateTime.Now}: {message}n");— crude but reliable when the debugger attach cycle is too slow. - Exception filter on OperationCanceledException: Revit throws this frequently in normal operation. Add it to the Debug → Exception Settings “Never Break” list.
SDK Downloads, Books, and Tools
The Revit API documentation and tooling landscape is fragmented across GitHub, the Autodesk Knowledge Network, and community repositories. Here is a consolidated reference:
Official SDK and Documentation
Open-Source Tools and GitHub Repositories
Books and Learning Resources
Structural Engineering & BIM Services
Looking for structural analysis, BIM coordination, or Revit automation consulting? I provide structural design and Revit-based workflow services for international projects.
Building a Revit Ribbon Add-In with C# — UI setup walkthrough
Frequently Asked Questions
🔗 Related Articles on CivilMat
Building your first Revit plugin is mostly about getting past the unfamiliar boilerplate. Once you have written IExternalCommand once, added a manifest file, and seen it load—the rest is just C# and the Revit API reference. The API surface is large but very discoverable through RevitLookup, and the structural categories (OST_StructuralFraming, OST_StructuralColumns, OST_StructuralFoundation) cover the 90% case for structural automation tasks.
The beam span-to-depth checker in this article is a real tool. It runs on actual structural models and produces results an engineer can act on. That is the right starting point—not a demo, but something with structural meaning. From there, the next steps are parameter writing (marking beams that fail), exporting results to a DataGrid, and integrating with an Excel output via the DocumentFormat.OpenXml NuGet package. Those are the pieces that make a tool the whole team uses.
Related external reading: Jeremy Tammik’s FilteredElementCollector benchmark study is still the definitive reference for understanding collector performance. The Autodesk Revit API Forum has answered nearly every structural-specific API question since 2009—search it before opening a new thread.
