diff --git a/gui/src/metainfo.json b/gui/src/metainfo.json
index 4a4aab724272bc529b62de5dbe857e3b7534b0a0..5168d8421a44e770320e559c41d71b243212c9d6 100644
--- a/gui/src/metainfo.json
+++ b/gui/src/metainfo.json
@@ -12874,7 +12874,9 @@
               },
               "shape": [
                 "0..*"
-              ]
+              ],
+              "default": [],
+              "virtual": true
             }
           ],
           "sub_sections": [
diff --git a/nomad/datamodel/metainfo/eln/perovskite_solar_cell_database/__init__.py b/nomad/datamodel/metainfo/eln/perovskite_solar_cell_database/__init__.py
index 8cbc09b3acba85bcfd03b65c550e2f309bbed3f3..24177cf64aad08d4e9d9c040271c67ff6ecf8eea 100644
--- a/nomad/datamodel/metainfo/eln/perovskite_solar_cell_database/__init__.py
+++ b/nomad/datamodel/metainfo/eln/perovskite_solar_cell_database/__init__.py
@@ -37,11 +37,6 @@ def add_band_gap(archive, band_gap):
         band_structure = BandStructureElectronic(band_gap=[band_gap])
         electronic = ElectronicProperties(band_structure_electronic=[band_structure])
         archive.results.properties.electronic = electronic
-        props = archive.results.properties.available_properties
-        if not props:
-            props = []
-        props.append('electronic.band_structure_electronic.band_gap')
-        archive.results.properties.available_properties = props
 
 
 def add_solar_cell(archive):
@@ -54,12 +49,6 @@ def add_solar_cell(archive):
         archive.results.properties.optoelectronic = OptoelectronicProperties()
     if not archive.results.properties.optoelectronic.solar_cell:
         archive.results.properties.optoelectronic.solar_cell = SolarCell()
-    props = archive.results.properties.available_properties
-    if not props:
-        props = []
-    if 'solar_cell' not in props:
-        props.append('solar_cell')
-    archive.results.properties.available_properties = props
 
 
 class Ref(MSection):
diff --git a/nomad/datamodel/results.py b/nomad/datamodel/results.py
index 2fe228ced265f7343d252098fad5bcaeb6c81622..a38792b18e35dcc3d1ead01c0dd1b762fe514b59 100644
--- a/nomad/datamodel/results.py
+++ b/nomad/datamodel/results.py
@@ -16,12 +16,14 @@
 # limitations under the License.
 #
 
+from typing import List
 import numpy as np
 from elasticsearch_dsl import Text
 
 from ase.data import chemical_symbols
 
 from nomad import config
+from nomad.utils import traverse_reversed
 from nomad.atomutils import Formula
 from nomad.datamodel.metainfo.measurements import Spectrum
 from nomad.datamodel.metainfo.simulation.system import Atoms
@@ -190,6 +192,43 @@ def get_formula_iupac(formula: str) -> str:
     return None if formula is None else Formula(formula).format('iupac')
 
 
+def available_properties(root: MSection) -> List[str]:
+    '''Returns a list of property names that are available in results.properties.
+
+    Args:
+        root: The metainfo section containing the properties
+
+    Returns:
+        List of property names that are present
+    '''
+    available_property_names = {
+        'electronic.band_structure_electronic.band_gap': 'electronic.band_structure_electronic.band_gap',
+        'electronic.band_structure_electronic': 'band_structure_electronic',
+        'electronic.dos_electronic': 'dos_electronic',
+        'electronic.greens_functions_electronic': 'greens_functions_electronic',
+        'vibrational.dos_phonon': 'dos_phonon',
+        'vibrational.band_structure_phonon': 'band_structure_phonon',
+        'vibrational.energy_free_helmholtz': 'energy_free_helmholtz',
+        'vibrational.heat_capacity_constant_volume': 'heat_capacity_constant_volume',
+        'thermodynamic.trajectory': 'trajectory',
+        'structural.radial_distribution_function': 'radial_distribution_function',
+        'dynamical.mean_squared_displacement': 'mean_squared_displacement',
+        'structural.radius_of_gyration': 'radius_of_gyration',
+        'geometry_optimization': 'geometry_optimization',
+        'mechanical.bulk_modulus': 'bulk_modulus',
+        'mechanical.shear_modulus': 'shear_modulus',
+        'mechanical.energy_volume_curve': 'energy_volume_curve',
+        'spectroscopy.eels': 'eels',
+        'optoelectronic.solar_cell': 'solar_cell',
+    }
+    available_properties: List[str] = []
+    for path, shortcut in available_property_names.items():
+        for _ in traverse_reversed(root, path.split('.')):
+            available_properties.append(shortcut)
+            break
+    return sorted(available_properties)
+
+
 tokenizer_formula = get_tokenizer(r'[A-Z][a-z]?\d*')
 
 
@@ -2512,6 +2551,8 @@ class Properties(MSection):
     )
     available_properties = Quantity(
         type=str,
+        default=[],
+        derived=lambda a: available_properties(a),
         shape=['0..*'],
         description='Subset of the property names that are present in this entry.',
         a_elasticsearch=Elasticsearch(material_entry_type),
diff --git a/nomad/normalizing/results.py b/nomad/normalizing/results.py
index 22d1a93073c326203b468180c1997e46c7c0c8e2..7c785bac6bbc3d9990183403eac6e5952a2372e8 100644
--- a/nomad/normalizing/results.py
+++ b/nomad/normalizing/results.py
@@ -26,6 +26,7 @@ import matid.geometry  # pylint: disable=import-error
 
 from nomad import config
 from nomad import atomutils
+from nomad.utils import traverse_reversed
 from nomad.atomutils import Formula
 from nomad.normalizing.normalizer import Normalizer
 from nomad.normalizing.method import MethodNormalizer
@@ -114,34 +115,6 @@ class ResultsNormalizer(Normalizer):
         for measurement in self.entry_archive.measurement:
             self.normalize_measurement(measurement)
 
-        # Add the list of available_properties: it is a selected subset of the
-        # stored properties.
-        available_property_names = {
-            "results.properties.electronic.band_structure_electronic.band_gap": "electronic.band_structure_electronic.band_gap",
-            "results.properties.electronic.band_structure_electronic": "band_structure_electronic",
-            "results.properties.electronic.dos_electronic": "dos_electronic",
-            "results.properties.electronic.greens_functions_electronic": "greens_functions_electronic",
-            "results.properties.vibrational.dos_phonon": "dos_phonon",
-            "results.properties.vibrational.band_structure_phonon": "band_structure_phonon",
-            "results.properties.vibrational.energy_free_helmholtz": "energy_free_helmholtz",
-            "results.properties.vibrational.heat_capacity_constant_volume": "heat_capacity_constant_volume",
-            "results.properties.thermodynamic.trajectory": "trajectory",
-            "results.properties.structural.radial_distribution_function": "radial_distribution_function",
-            "results.properties.dynamical.mean_squared_displacement": "mean_squared_displacement",
-            "results.properties.structural.radius_of_gyration": "radius_of_gyration",
-            "results.properties.geometry_optimization": "geometry_optimization",
-            "results.properties.mechanical.bulk_modulus": "bulk_modulus",
-            "results.properties.mechanical.shear_modulus": "shear_modulus",
-            "results.properties.mechanical.energy_volume_curve": "energy_volume_curve",
-            "results.properties.spectroscopy.eels": "eels",
-        }
-        available_properties: List[str] = []
-        for path, shortcut in available_property_names.items():
-            for _ in self.traverse_reversed(path.split('.')):
-                available_properties.append(shortcut)
-                break
-        results.properties.available_properties = sorted(available_properties)
-
     def normalize_sample(self, sample) -> None:
         results = self.entry_archive.results
 
@@ -260,7 +233,7 @@ class ResultsNormalizer(Normalizer):
           - There is a non-empty array of energies.
         """
         def resolve_band_structure(path):
-            for bs in self.traverse_reversed(path):
+            for bs in traverse_reversed(self.entry_archive, path):
                 if not bs.segment:
                     continue
                 valid = True
@@ -319,7 +292,7 @@ class ResultsNormalizer(Normalizer):
         """
 
         def resolve_dos(path):
-            for dos in self.traverse_reversed(path):
+            for dos in traverse_reversed(self.entry_archive, path):
                 energies = dos.energies
                 values = np.array([d.value.magnitude for d in dos.total])
                 if valid_array(energies) and valid_array(values):
@@ -366,7 +339,7 @@ class ResultsNormalizer(Normalizer):
         """
 
         def resolve_greens_functions(path):
-            for gfs in self.traverse_reversed(path):
+            for gfs in traverse_reversed(self.entry_archive, path):
                 tau = gfs.tau
                 iw = gfs.matsubara_freq
                 values_gtau = np.array([np.absolute(gtau) for gtau in gfs.greens_function_tau.real])
@@ -403,7 +376,7 @@ class ResultsNormalizer(Normalizer):
           - There is a non-empty array of energies.
         """
         path = ["run", "calculation", "band_structure_phonon"]
-        for bs in self.traverse_reversed(path):
+        for bs in traverse_reversed(self.entry_archive, path):
             if not bs.segment:
                 continue
             valid = True
@@ -430,7 +403,7 @@ class ResultsNormalizer(Normalizer):
           - There is a non-empty array of energies.
         """
         path = ["run", "calculation", "dos_phonon"]
-        for dos in self.traverse_reversed(path):
+        for dos in traverse_reversed(self.entry_archive, path):
             energies = dos.energies
             values = np.array([d.value.magnitude for d in dos.total])
             if valid_array(energies) and valid_array(values):
@@ -450,7 +423,7 @@ class ResultsNormalizer(Normalizer):
           - There is a non-empty array of energies.
         """
         path = ["workflow", "thermodynamics"]
-        for thermo_prop in self.traverse_reversed(path):
+        for thermo_prop in traverse_reversed(self.entry_archive, path):
             temperatures = thermo_prop.temperature
             energies = thermo_prop.vibrational_free_energy_at_constant_volume
             if valid_array(temperatures) and valid_array(energies):
@@ -470,7 +443,7 @@ class ResultsNormalizer(Normalizer):
           - There is a non-empty array of energies.
         """
         path = ["workflow", "thermodynamics"]
-        for thermo_prop in self.traverse_reversed(path):
+        for thermo_prop in traverse_reversed(self.entry_archive, path):
             temperatures = thermo_prop.temperature
             heat_capacities = thermo_prop.heat_capacity_c_v
             if valid_array(temperatures) and valid_array(heat_capacities):
@@ -486,7 +459,7 @@ class ResultsNormalizer(Normalizer):
         properties based on the first found geometry optimization workflow.
         """
         path = ["workflow"]
-        for workflow in self.traverse_reversed(path):
+        for workflow in traverse_reversed(self.entry_archive, path):
             # Check validity
             if workflow.type == "geometry_optimization" and workflow.calculations_ref:
 
@@ -529,7 +502,7 @@ class ResultsNormalizer(Normalizer):
         """
         path = ["workflow"]
         trajs = []
-        for workflow in self.traverse_reversed(path):
+        for workflow in traverse_reversed(self.entry_archive, path):
             # Check validity
             if workflow.type == "molecular_dynamics":
                 traj = Trajectory()
@@ -590,7 +563,7 @@ class ResultsNormalizer(Normalizer):
         """
         path = ["workflow", "molecular_dynamics", "results", "radial_distribution_functions"]
         rdfs = []
-        for rdf_workflow in self.traverse_reversed(path):
+        for rdf_workflow in traverse_reversed(self.entry_archive, path):
             rdf_values = rdf_workflow.radial_distribution_function_values
             if rdf_values is not None:
                 for rdf_value in rdf_values or []:
@@ -620,7 +593,7 @@ class ResultsNormalizer(Normalizer):
         """
         path_workflow = ["workflow"]
         rgs: List[RadiusOfGyration] = []
-        for workflow in self.traverse_reversed(path_workflow):
+        for workflow in traverse_reversed(self.entry_archive, path_workflow):
 
             # Check validity
             if workflow.type == "molecular_dynamics":
@@ -654,7 +627,7 @@ class ResultsNormalizer(Normalizer):
         """
         path = ["workflow", "molecular_dynamics", "results", "mean_squared_displacements"]
         msds = []
-        for msd_workflow in self.traverse_reversed(path):
+        for msd_workflow in traverse_reversed(self.entry_archive, path):
             msd_values = msd_workflow.mean_squared_displacement_values
             if msd_values is not None:
                 for msd_value in msd_values or []:
@@ -1011,28 +984,3 @@ class ResultsNormalizer(Normalizer):
                     ))
 
         return shear_modulus
-
-    def traverse_reversed(self, path: List[str]) -> Any:
-        """Traverses the given metainfo path in reverse order. Useful in
-        finding the latest reported section or value.
-        """
-        def traverse(root, path, i):
-            if not root:
-                return
-            sections = getattr(root, path[i])
-            if isinstance(sections, list):
-                for section in reversed(sections):
-                    if i == len(path) - 1:
-                        yield section
-                    else:
-                        for s in traverse(section, path, i + 1):
-                            yield s
-            else:
-                if i == len(path) - 1:
-                    yield sections
-                else:
-                    for s in traverse(sections, path, i + 1):
-                        yield s
-        for t in traverse(self.entry_archive, path, 0):
-            if t is not None:
-                yield t
diff --git a/nomad/utils/__init__.py b/nomad/utils/__init__.py
index d23b6bb0ef323aaa9c49f524951caa4c48befdea..0c793cb797ddec14d020a031393ab9bd5aba1018 100644
--- a/nomad/utils/__init__.py
+++ b/nomad/utils/__init__.py
@@ -589,3 +589,37 @@ def query_list_to_dict(path_list: List[Union[str, int]], value: Any) -> Dict[str
         current = current[key]
         i += 1
     return returned
+
+
+def traverse_reversed(archive: Any, path: List[str]) -> Any:
+    '''Traverses the given metainfo path in reverse order. Useful in finding the
+    latest reported section or value.
+
+    Args:
+        archive: The root section to traverse
+        path: List of path names to traverse
+
+    Returns:
+        Returns the last metainfo section or quantity in the given path or None
+        if not found.
+    '''
+    def traverse(root, path, i):
+        if not root:
+            return
+        sections = getattr(root, path[i])
+        if isinstance(sections, list):
+            for section in reversed(sections):
+                if i == len(path) - 1:
+                    yield section
+                else:
+                    for s in traverse(section, path, i + 1):
+                        yield s
+        else:
+            if i == len(path) - 1:
+                yield sections
+            else:
+                for s in traverse(sections, path, i + 1):
+                    yield s
+    for t in traverse(archive, path, 0):
+        if t is not None:
+            yield t