Revit 2026, Empty Assets and Demolished Stuff

The new major release of Revit has arrived, and a bunch of API solutions for the existing ones:

What's New in Revit 2026

Autodesk has released Revit 2026, cf. What's New in Revit 2026.

For an intro to the new features, you can register for the What’s New in Revit 2026 webinar on April 10.

Revit 2026

Before going further with that, let's look at some recent solutions for programming the existing releases.

Empty Appearance Asset Element

The Revit API discussion forum thread on why is AppearanceAssetElement empty in API was solved:

Question: I am retrieving appearance assets from elements in my project and came across a door which has a material Door - Architrave. Even though it has appearance assets in the UI, RevitLookup cannot find them. It shows AssetProperties.Size = 0. This is the only material in my entire project with zero AssetProperties.Size. Is this normal? How else can I get my Appearance assets if AssetProperties.Size is zero?

I'm trying to get specific appearance asset properties from various materials, including appearance description, category and main appearance image filepath.

I tried using renderingAsset.FindByName("Description"), renderingAsset.FindByName("Category"), renderingAsset.FindByName("UnifiedBitmapSchema") and renderingAsset.FindByName("BaseSchema"). They all return null since renderingAssets is empty, i.e. AssetProperties.Size = 0.

For reference, here is my code:

private (string, string, string) GetMaterialAssets(Document doc, int materialId)
{
  string texturePath = "";
  string description = "";
  string category = "";

  // Get material element
  Material material = doc.GetElement(new ElementId(materialId)) as Material;
  if (material != null)
  {
    // Get appearance assets
    ElementId appearanceAssetId = material.AppearanceAssetId;
    AppearanceAssetElement appearanceAssetElem = doc.GetElement(appearanceAssetId) as AppearanceAssetElement;
    if (appearanceAssetElem == null) return (texturePath, description, category);

    // Get rendering asset
    Asset assetRend = appearanceAssetElem.GetRenderingAsset();

    if (assetRend != null)
    {
      if (assetRend.Size == 0)
      {
        AssetProperty baseSchema = assetRend.FindByName("BaseSchema");
        TaskDialog.Show("Base Schema", baseSchema?.ToString());
      }

      // Go through properties
      for (int assetIdx = 0; assetIdx < assetRend.Size; assetIdx++)
      {
        AssetProperty assetProperty = assetRend[assetIdx];
        Type type = assetProperty.GetType();

        if (assetProperty.Name.ToLower() == "description")
        {
          var prop = type.GetProperty("Value");
          if (prop != null && prop.GetIndexParameters().Length == 0)
          {
            description = prop.GetValue(assetProperty).ToString();
            continue;
          }
        }
        else if (assetProperty.Name.ToLower() == "category")
        {
          var prop = type.GetProperty("Value");
          if (prop != null && prop.GetIndexParameters().Length == 0)
          {
            category = prop.GetValue(assetProperty).ToString();
            continue;
          }
        }

        if (assetProperty.NumberOfConnectedProperties < 1)
          continue;

        Asset connectedAsset = assetProperty.GetConnectedProperty(0) as Asset;
        if (connectedAsset.Name == "UnifiedBitmapSchema")
        {
          if (assetProperty.Name.Contains("bump") || assetProperty.Name.Contains("pattern_map") ||
            assetProperty.Name.Contains("shader") || assetProperty.Name.Contains("opacity"))
            continue;

          // UnifiedBitmap contains file name and path
          AssetPropertyString path = connectedAsset.FindByName(UnifiedBitmap.UnifiedbitmapBitmap) as AssetPropertyString;
          if (path == null || string.IsNullOrEmpty(path.Value))
            continue;
          string pathString = path.Value;
          if (pathString.Contains("|"))
          {
            pathString = pathString.Split('|')[0];
          }
          // remove | this character
          if (Path.IsPathRooted(pathString))
          {
            texturePath = pathString;
            continue;
          }

          // return path using default texture
          string defaultTexturePath = @"C:\Program Files\Common Files\Autodesk Shared\Materials\Textures\";
          string defaultTexturePathx86 = @"C:\Program Files (x86)\Common Files\Autodesk Shared\Materials\Textures\";

          if (Directory.Exists(defaultTexturePath))
            texturePath = defaultTexturePath + pathString;
          else if (Directory.Exists(defaultTexturePathx86))
            texturePath = defaultTexturePathx86 + pathString;
        }
      }
    }
  }

  return (texturePath, description, category);
}

Answer: I think it's because the AppearanceAsset doesn't really exist. I had this when editing materials in a Rvt created by a IFC conversion. Could also be the material original also was created/duplicated by API and no asset was attached, or from a material library. Or even a (very) old Revit style material

When opening the material in the material editor, Revit assigns the AppearanceAsset, but it's not saved to the material on closing the editor if no changes are made to the AppearanceAsset. Change, for instance, the description field of the asset and save it. Now the AppearanceAsset will also be found by the API

In my case, it always was a Generic class material (IFC conversion) and not a Wood Class; maybe that's why an AppearanceAsset is created by Revit with wood settings; likely a copy of the first existing Wood Class Asset?? (or some default).

I solved it in my case by assigning my own AppearanceAsset (copy of a template AppearanceAsset with correct settings), editing some properties and assign it to the material.

Response: It was indeed as you described. By changing a property of my Appearance asset and saving the material re-applies the appearance assets in the API.

No solution in my case, other then warning the user to this issue, since I can't programmatically re-assign the properties without reading the assigned values first, which I can't since renderingAssets is empty. My materials do not come from IFC conversion.

I've been told that the family I'm using with the material with the empty Appearance Assets (Door - Architrave) is actually one of the default families that comes with Revit 2024 installation. So assuming this is a very old family, recycled between Revit versions, and it might be a very old material as well.

Answer: I just checked the 2024 revit project template "Default-Multi-discipline". It seems to have 22 materials without an appearance asset. Like "Analytical Spaces", "Metal - Stainless Steel" etc..(others do have an appearance asset.)

Duplicating such material (duplicate incl. asset, shared asset not tried) will result in a material with an appearance asset (size > 0), The original will still lack it, as nothing changed to that one.

Duplicating such material in the API will result in a material with no appearance asset, because the UI version adds it internally. So that's what then also should be done in the API in you're addin. How the Revit UI creates the asset is another topic; I think it depends on the material class and what maybe already exists in the project/family and/or some internal default assets.

I haven't tried this via the API yet but, after checking the Appearance Asset Element and finding it to have a size of 0, try duplicating it and see if Revit creates a properly defined version of it, if so then you could assign the newly created Appearance Asset to the material (along with searching for and redefining any other materials that might be using it as well).

I went ahead and tried this suggestion. It does indeed work. Here's the code in VB (where RDB = Revit.DB and RDV = Revit.DB.Visual):

  'Forum Issue resolution
  Public Sub ReplaceEmptyAppearanceAssets(thisDoc As RDB.Document)
    Dim printStr As String = "Invalid Assets: " & vbCrLf

    'collect document materials
    Dim matCollector As RDB.FilteredElementCollector = New RDB.FilteredElementCollector(thisDoc)
    Dim allMats As IList(Of RDB.Material) = matCollector.OfClass(GetType(RDB.Material)).OfType(Of RDB.Material).ToList
    'collect document appearance Asset Elements
    Dim assetCollector As New RDB.FilteredElementCollector(thisDoc)
    Dim allAppearanceAssets As IList(Of RDB.AppearanceAssetElement) = assetCollector.OfClass(GetType(RDB.AppearanceAssetElement)) _
      .OfType(Of RDB.AppearanceAssetElement).ToList
    'create a list of asset names in use
    Dim currentNames As New List(Of String)
    For Each appAsset As RDB.AppearanceAssetElement In allAppearanceAssets
      If currentNames.Contains(appAsset.Name) = False Then
        currentNames.Add(appAsset.Name)
      End If
    Next

    'prep for creating Asset if required
    'this takes a while on first run to expand the default library,
    'could be moved to seperate function and called if neede
    Dim assetList As List(Of RDV.Asset) = thisDoc.Application.GetAssets(RDV.AssetType.Appearance)
    Dim genericAsset As RDV.Asset = Nothing

    'really shouldn't start a transaction unless modification is needed...
    Using thisTrans As New RDB.Transaction(thisDoc, "Create new material")
      thisTrans.Start()

      Dim nameStr As String = ""

      'parse materials looking for invalid Appearance Assets
      Dim thisAssetElem As RDB.AppearanceAssetElement
      For Each thisMat As RDB.Material In allMats
        If thisMat.AppearanceAssetId <> RDB.ElementId.InvalidElementId Then
          Dim renderAssetElem As RDB.AppearanceAssetElement = TryCast(thisDoc.GetElement(thisMat.AppearanceAssetId), RDB.AppearanceAssetElement)

          If renderAssetElem IsNot Nothing Then
            'Check to see if it's fully defined
            'from API help
            'AppearanceAssetElement.GetRenderingAsset
            'The retrieved Asset may be empty if it is loaded from material library without any modification.
            'In this case, you can use Application.GetAssets(AssetType.Appearance) to load all preset appearance assets,
            'and retrieve the asset by its name.

            Dim thisAsset As RDV.Asset = renderAssetElem.GetRenderingAsset()
            If thisAsset.Size = 0 Then
              printStr += "Invalid Asset Size in Material: " & thisMat.Name & " - Asset: " & renderAssetElem.Name & vbCrLf
              genericAsset = assetList.FirstOrDefault(Function(eachAsset) eachAsset.Name = thisAsset.Name)
              'We could read the default properties directly from this genericAsset or
              'create new as duplicate

              'the following would be a seperate function due to replication
              nameStr = renderAssetElem.Name

              Dim numb As Integer = 1
              Dim testStr As String = nameStr

              Do While currentNames.Contains(testStr) = True
                testStr = nameStr & "_" & numb.ToString()
                numb = numb + 1
              Loop
              nameStr = testStr

              If genericAsset IsNot Nothing Then
                printStr += "  : Duplicating Asset :" & nameStr & vbCrLf
                Dim newAssetElem As RDB.AppearanceAssetElement = RDB.AppearanceAssetElement.Create(thisDoc, nameStr, genericAsset)
                thisMat.AppearanceAssetId = newAssetElem.Id
                currentNames.Add(nameStr)
              Else
                printStr += "  !! Could not aquire Asset !!" & vbCrLf
              End If
            End If
          Else
            printStr += "Cannot aquire Asset from Material: " & thisMat.Name & vbCrLf
          End If
        Else
          'from API help
          'Material.AppearanceAssetId
          'The id of the AppearanceAssetElement, or InvalidElementId if the material does not have an associated appearance asset.
          'This is the id to the element that contains visual material information used for rendering.
          'In some cases where the material is created without setting up custom render appearance properties
          '(for example, when the material is created via an import, or when it is created by the API),
          'this property will be InvalidElementId. In that situation the standard material properties
          'such as Color and Transparency will dictate the appearance of the material during rendering.

          printStr += "Invalid Asset ID in Material: " & thisMat.Name & vbCrLf
          'create new from existing generic

          'the following would be a seperate function due to replication
          nameStr = thisMat.Name
          Dim numb As Integer = 1
          Dim testStr As String = nameStr

          Do While currentNames.Contains(testStr) = True
            testStr = nameStr & "_" & numb.ToString()
            numb = numb + 1
          Loop
          nameStr = testStr

          genericAsset = assetList.FirstOrDefault(Function(eachAsset) eachAsset.FindByName(RDV.Generic.GenericDiffuse) IsNot Nothing)
          If genericAsset IsNot Nothing Then
            printStr += "  : Creating Asset :" & nameStr & vbCrLf
            Dim newAssetElem As RDB.AppearanceAssetElement = RDB.AppearanceAssetElement.Create(thisDoc, nameStr, genericAsset)
            thisMat.AppearanceAssetId = newAssetElem.Id
            currentNames.Add(nameStr)
          Else
            printStr += "  !! Could not aquire Asset from App !!" & vbCrLf
          End If

        End If
      Next

      thisTrans.Commit()
    End Using

    RUI.TaskDialog.Show("Material Info", printStr)

  End Sub

You will also notice a couple of comments taken from the API help that directly answer your original question of "Why is... empty..." and further to those that do not have a valid AppearanceAssetId.

Running this code on your materials with no appearance asset element or assets with size=0 creates valid assets in both cases. This was a good exercise, thanks for the question and I hope this helps you in some way.

Response: Thank you for your answer and sample code! I tried as you suggested and it does indeed work wonderfully! Thanks everyone for the suggestions and input!

Answer 2: I haven't tried this via the API yet but, after checking the Appearance Asset Element and finding it to have a size of 0, try duplicating it and see if Revit creates a properly defined version of it, if so then you could assign the newly created Appearance Asset to the material (along with searching for and redefining any other materials that might be using it as well). It could have been better of course, and it really should have two other additions to it:

Retrieving Demolished Room Data

My colleague Naveen Kumar shared a solution to retrieve room data for demolished family instances:

In Revit projects, it is important to track room data for family instances, especially when they are marked as 'Existing' and later demolished during the 'New Construction' phase.

The Problem: Missing Room Data for Demolished Family Instances The issue comes from how the Revit API works. The FamilyInstance.Room property gives room data based on the final phase of the project. If a family instance has been demolished and no longer exists in the final phase, the API might return incorrect or null values. This can be problematic when accurate room data is needed from earlier phases, before the family instance was demolished.

The Solution: Getting Room Data by Phase To fix this, the Revit API provides the overload method FamilyInstance.get_Room(Phase). This allows you to retrieve room information for the specific phase you need, even if the family instance was demolished in a later phase. It ensures you’re getting the right data for each phase, just like in Revit schedules.

Another option is to use Document.GetRoomAtPoint(XYZ, Phase), which retrieves the room based on the family instance’s exact location during a specific phase. This method is useful for more complex cases, ensuring that you get accurate room data regardless of what happens to the family instances in later phases.

// Get the active document and UIDocument from the commandData
Document doc = commandData.Application.ActiveUIDocument.Document;
UIDocument uidoc = commandData.Application.ActiveUIDocument;

// Retrieve all project phases (assuming 3 phases: "Existing", "New Construction", "Final Phase")
FilteredElementCollector phaseCollector = new FilteredElementCollector(doc)
  .OfCategory(BuiltInCategory.OST_Phases)
  .WhereElementIsNotElementType();

// Find the specific phases by name
Phase existingPhase=phaseCollector.FirstOrDefault(phase=>phase.Name=="Existing") as Phase;
Phase newConstructionPhase=phaseCollector.FirstOrDefault(phase=>phase.Name=="New Construction") as Phase;
Phase finalPhase=phaseCollector.FirstOrDefault(phase=>phase.Name=="Final Phase") as Phase;

// Let the user pick an element (family instance) from the Revit model
Autodesk.Revit.DB.Reference pickedReference = uidoc.Selection.PickObject(ObjectType.Element);
Element pickedElement = doc.GetElement(pickedReference);
FamilyInstance familyInstance = pickedElement as FamilyInstance;

if (familyInstance != null)
{
  // Room in which the family instance is located during the final phase of the project
  Room currentRoom = familyInstance.Room;

  // Access the room the family instance is located in, based on a specific phase
  // Modify this to set the desired phase: existingPhase, newConstructionPhase, or finalPhase
  Phase targetPhase = existingPhase;
  Room roomInSpecificPhase = familyInstance.get_Room(targetPhase);

  // Workaround: Get the family instance's location point
  //and find the corresponding room in the specified phase.
  LocationPoint familyLocation = familyInstance.Location as LocationPoint;
  if (familyLocation != null)
  {
    XYZ locationPoint = familyLocation.Point;
    Room roomAtPoint = doc.GetRoomAtPoint(locationPoint, targetPhase);
  }
}

Automating Phase-Based View Schedule Creation for Revit Projects: You can also use the Revit API to automate the creation of view schedules for different project phases. Instead of manually creating schedules for each phase, the API can automate this process, linking each schedule to the correct phase and organizing the data properly. This saves time and ensures accuracy across all phases of the project.

// Create a filtered collector to gather all Phase elements in the document
FilteredElementCollector collector = new FilteredElementCollector(doc)
  .OfCategory(BuiltInCategory.OST_Phases)
  .WhereElementIsNotElementType();

using (Transaction actrans = new Transaction(doc, "Create View Schedules"))
{
  actrans.Start();
  foreach (Element e in collector)
  {
    Phase phase = e as Phase;
    if (phase != null)
    {
      // Create a view schedule for the each phase
       CreateViewSchedule(doc ,phase);
    }
  }
  actrans.Commit();
}

private void CreateViewSchedule(Document doc , Phase phase)
{
  // Create a new view schedule in the document
  //with an InvalidElementId for a multi-category schedule
  ViewSchedule viewSchedule = ViewSchedule.CreateSchedule(doc, ElementId.InvalidElementId);

  // Set the name of the schedule
  viewSchedule.Name = "API-" + phase.Name;

  // Set the phase parameter of the view schedule to the required phase
  viewSchedule.get_Parameter(BuiltInParameter.VIEW_PHASE).Set(phase.Id);

  ScheduleDefinition definition = viewSchedule.Definition;

  // Loop through all schedulable fields and add them to the schedule definition
  foreach (SchedulableField sf in definition.GetSchedulableFields())
  {
    ScheduleField field = definition.AddField(sf);
  }
}

Many thanks to Naveen for pointing this out.

RST Results Package Creation

A Revit Structural solution for RST results package creation with API:

Look at:

The examples are pretty old, so the code needs some tweaks, e.g. for the ForgeTypeID.

In my case the example works (not perfectly) for linear results, but for surface results nothing comes up. Here is a summary and further information that others may find helpful:

is now

AnalyticalPanel analyticalModel = (doc.GetElement(elementId) as AnalyticalPanel);
IList<Curve> curves = analyticalModel.GetOuterContour().ToList();

From my understanding, the ResultManager actually uses the AVF so it is up to you to decide whether it is more suited for your case to go for the ResultsBuilder and Manager or directly for the AVF. Here are some pros and cons.

Thus, I think that if you want just to show some results in Revit that are typical in the structural discipline go with ResultsBuilder. On the other hand, if you want to present more complex data or need more control on the data you are better off implementing your own data structure and use AVF directly.

RST ResultsBuilder SDK Sample

Waldemar @okapawal Okapa, Revit Structural Sr Product Owner, answered the related question on the structural analysis toolkit ResultsBuilder and reviewing stored results in Revit:

When calling the AddLinearResult function, the result package is set as type of ResultsPackageTypes.All.

The ResultsPackageTypes.Static type package should be used here because the static analysis results are saved in this function

RST ResultsBuilder

Inside, the function AddArbitraryResults is calling the method with a parameter saying that results are not dependent on load cases.

RST ResultsBuilder

For saving the reinforcement results, it is preferable to create a new package that will contain the reinforcement results.


/// <summary>
/// Creates an empty results package to store reinforcement results
/// </summary>
/// <param name="doc">Revit document</param>
/// <returns>Reference to the newly created package</returns>
private ResultsPackageBuilder createReinforcementResultsPackageBuilder( Document doc)
{
  ResultsAccess resultsAccess = ResultsAccess.CreateResultsAccess(doc);
  ResultsPackageBuilder resultsPackageBuilder = resultsAccess.CreateResultsPackage(reinfPackageGuid, "REINFORCEMENT_ResultsInRevit", UnitsSystem.Metric, ResultsPackageTypes.RequiredReinforcement);

  resultsPackageBuilder.SetAnalysisName("Reinforcement__ResultsInRevit_Analysis");
  resultsPackageBuilder.SetModelName("ResultsInRevit_Model");
  resultsPackageBuilder.SetDescription("Sample results");
  resultsPackageBuilder.SetVendorDescription("Autodesk");
  resultsPackageBuilder.SetVendorId("ADSK");

  return resultsPackageBuilder;
}

/// <summary>
/// Adds reinforcement results to a linear element
/// </summary>
/// <param name="resultsPackageBuilder">Reference to the results package</param>
/// <param name="elementId">Id of the linear element to which results are to be added</param>
private void AddReinforcementLinearResults(ResultsPackageBuilder resultsPackageBuilder, ElementId elementId)
{
  // Create list of some results
  // Each list element contains result type and a list of result values
  List<Tuple<LinearResultType, List<double>>> valuesForRnf = new List<Tuple<LinearResultType, List<double>>>()
  {
    new Tuple<LinearResultType,List<double>> ( LinearResultType.AsBottom, new List<double>() {  45.00,  30.00,  15.00,  60.00,  0.00, 0.00, 10.00, 0.00, 0.00, 90.00 }),
    new Tuple<LinearResultType,List<double>> ( LinearResultType.AsTop,    new List<double>() {  45.00,  30.00,  15.00,  60.00,  0.00, 0.00, 10.00, 0.00, 0.00, 90.00 }),
    new Tuple<LinearResultType,List<double>> ( LinearResultType.AsLeft,   new List<double>() {  45.00,  30.00,  15.00,  60.00,  0.00, 0.00, 10.00, 0.00, 0.00, 90.00 }),
    new Tuple<LinearResultType,List<double>> ( LinearResultType.AsRight,  new List<double>() {  45.00,  30.00,  15.00,  60.00,  0.00, 0.00, 10.00, 0.00, 0.00, 90.00 }),
  };

  // Add result domain for load independent results
  List<double> xCoordinateValues = new List<double>() { 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0 };
  resultsPackageBuilder.SetBarResult(elementId, null, DomainResultType.X, xCoordinateValues);
  // Add result values
  foreach (var valueForRnf in valuesForRnf)
  {
    resultsPackageBuilder.SetBarResult(elementId, null, valueForRnf.Item1, valueForRnf.Item2);
  }
}

/// <summary>
/// Auxiliary method. Generates a list of sample reinforcement results for points on surface element contour
/// </summary>
/// <param name="points">List of points for which arbitrary results are to be generated</param>
/// <returns>A list containing a number of records with arbitrary surface result type and corresponding result values</returns>
private List<Tuple<SurfaceResultType, List<double>>> GenerateSampleReinforcementSurfaceResultsForContour(List<XYZ> points)
{
  // Create an array of arbitrary result types.
  SurfaceResultType[] surfaceResultTypes = { SurfaceResultType.AxxBottom, SurfaceResultType.AyyBottom, SurfaceResultType.AxxTop, SurfaceResultType.AyyTop };

  // Create list
  var sampleResults = new List<Tuple<SurfaceResultType, List<double>>>();
  double coeff = 1.0e-4;
  // Iterate over types, create a value for each point and add a record to the list
  foreach (SurfaceResultType surfaceResultType in surfaceResultTypes)
  {
    coeff *= 1.5;
    List<double> results = points.Select(s => (s.X * coeff + s.Y * coeff + s.Z * coeff)).ToList();
    sampleResults.Add(new Tuple<SurfaceResultType, List<double>>(surfaceResultType, results));
  }
  return sampleResults;
}

I am attaching new source code here in StoreResults.zip. Use that, you will see this in Revit:

RST ResultsBuilder