diff --git a/bom/openhab-core/pom.xml b/bom/openhab-core/pom.xml
index e8f72da9320..d1f2fd3163c 100644
--- a/bom/openhab-core/pom.xml
+++ b/bom/openhab-core/pom.xml
@@ -496,6 +496,12 @@
       <version>${project.version}</version>
       <scope>compile</scope>
     </dependency>
+    <dependency>
+      <groupId>org.openhab.core.bundles</groupId>
+      <artifactId>org.openhab.core.model.yaml</artifactId>
+      <version>${project.version}</version>
+      <scope>compile</scope>
+    </dependency>
     <dependency>
       <groupId>org.openhab.core.bundles</groupId>
       <artifactId>org.openhab.core.ui</artifactId>
diff --git a/bundles/org.openhab.core.model.yaml/.classpath b/bundles/org.openhab.core.model.yaml/.classpath
new file mode 100644
index 00000000000..58cd399d639
--- /dev/null
+++ b/bundles/org.openhab.core.model.yaml/.classpath
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+	<classpathentry kind="src" output="target/classes" path="src/main/java">
+		<attributes>
+			<attribute name="optional" value="true"/>
+			<attribute name="maven.pomderived" value="true"/>
+		</attributes>
+	</classpathentry>
+	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-17">
+		<attributes>
+			<attribute name="maven.pomderived" value="true"/>
+			<attribute name="annotationpath" value="target/dependency"/>
+		</attributes>
+	</classpathentry>
+	<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
+		<attributes>
+			<attribute name="maven.pomderived" value="true"/>
+			<attribute name="annotationpath" value="target/dependency"/>
+		</attributes>
+	</classpathentry>
+	<classpathentry kind="src" output="target/test-classes" path="src/test/java">
+		<attributes>
+			<attribute name="optional" value="true"/>
+			<attribute name="maven.pomderived" value="true"/>
+			<attribute name="test" value="true"/>
+		</attributes>
+	</classpathentry>
+	<classpathentry kind="output" path="target/classes"/>
+</classpath>
diff --git a/bundles/org.openhab.core.model.yaml/.project b/bundles/org.openhab.core.model.yaml/.project
new file mode 100644
index 00000000000..1b130d3a787
--- /dev/null
+++ b/bundles/org.openhab.core.model.yaml/.project
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+	<name>org.openhab.core.model.yaml</name>
+	<comment></comment>
+	<projects>
+	</projects>
+	<buildSpec>
+		<buildCommand>
+			<name>org.eclipse.jdt.core.javabuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+		<buildCommand>
+			<name>org.eclipse.m2e.core.maven2Builder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+	</buildSpec>
+	<natures>
+		<nature>org.eclipse.jdt.core.javanature</nature>
+		<nature>org.eclipse.m2e.core.maven2Nature</nature>
+	</natures>
+</projectDescription>
diff --git a/bundles/org.openhab.core.model.yaml/NOTICE b/bundles/org.openhab.core.model.yaml/NOTICE
new file mode 100644
index 00000000000..19bf5f2cfa8
--- /dev/null
+++ b/bundles/org.openhab.core.model.yaml/NOTICE
@@ -0,0 +1,14 @@
+This content is produced and maintained by the openHAB project.
+
+* Project home: https://www.openhab.org
+
+== Declared Project Licenses
+
+This program and the accompanying materials are made available under the terms
+of the Eclipse Public License 2.0 which is available at
+https://www.eclipse.org/legal/epl-2.0/.
+
+== Source Code
+
+https://github.com/openhab/openhab-core
+
diff --git a/bundles/org.openhab.core.model.yaml/pom.xml b/bundles/org.openhab.core.model.yaml/pom.xml
new file mode 100644
index 00000000000..00941ec4b21
--- /dev/null
+++ b/bundles/org.openhab.core.model.yaml/pom.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.openhab.core.bundles</groupId>
+    <artifactId>org.openhab.core.reactor.bundles</artifactId>
+    <version>4.1.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>org.openhab.core.model.yaml</artifactId>
+
+  <name>openHAB Core :: Bundles :: Model YAML</name>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.openhab.core.bundles</groupId>
+      <artifactId>org.openhab.core</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.openhab.core.bundles</groupId>
+      <artifactId>org.openhab.core.semantics</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+  </dependencies>
+</project>
diff --git a/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/AbstractYamlFile.java b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/AbstractYamlFile.java
new file mode 100644
index 00000000000..d99c8151ca1
--- /dev/null
+++ b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/AbstractYamlFile.java
@@ -0,0 +1,65 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.model.yaml;
+
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link AbstractYamlFile} is the DTO base class used to map a YAML configuration file.
+ *
+ * A YAML configuration file consists of a version and a list of elements.
+ *
+ * @author Laurent Garnier - Initial contribution
+ */
+@NonNullByDefault
+public abstract class AbstractYamlFile implements YamlFile {
+
+    private final Logger logger = LoggerFactory.getLogger(AbstractYamlFile.class);
+
+    /**
+     * YAML file version
+     */
+    public int version;
+
+    @Override
+    public abstract List<? extends YamlElement> getElements();
+
+    @Override
+    public int getVersion() {
+        return version;
+    }
+
+    @Override
+    public boolean isValid() {
+        // Checking duplicated elements
+        List<? extends YamlElement> elts = getElements();
+        long nbDistinctIds = elts.stream().map(YamlElement::getId).distinct().count();
+        if (nbDistinctIds < elts.size()) {
+            logger.debug("Elements with same ids detected in the file");
+            return false;
+        }
+
+        // Checking each element
+        for (int i = 0; i < elts.size(); i++) {
+            if (!elts.get(i).isValid()) {
+                logger.debug("Error in element {}", i + 1);
+                return false;
+            }
+        }
+        return true;
+    }
+}
diff --git a/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/YamlElement.java b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/YamlElement.java
new file mode 100644
index 00000000000..2b1d8b16a03
--- /dev/null
+++ b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/YamlElement.java
@@ -0,0 +1,39 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.model.yaml;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link YamlElement} interface offers an identifier and a check validity method
+ * to any element defined in a YAML configuration file.
+ *
+ * @author Laurent Garnier - Initial contribution
+ */
+@NonNullByDefault
+public interface YamlElement {
+
+    /**
+     * Get the identifier of the YAML element
+     *
+     * @return the identifier as a string
+     */
+    String getId();
+
+    /**
+     * Check that the YAML element is valid
+     *
+     * @return true if all the checks are OK
+     */
+    boolean isValid();
+}
diff --git a/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/YamlFile.java b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/YamlFile.java
new file mode 100644
index 00000000000..40f5ce4c9c8
--- /dev/null
+++ b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/YamlFile.java
@@ -0,0 +1,48 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.model.yaml;
+
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link YamlFile} is the interface to manage the generic content of a YAML configuration file.
+ *
+ * @author Laurent Garnier - Initial contribution
+ */
+@NonNullByDefault
+public interface YamlFile {
+
+    /**
+     * Get the list of elements present in the YAML file.
+     *
+     * @return the list of elements
+     */
+    List<? extends YamlElement> getElements();
+
+    /**
+     * Get the version present in the YAML file.
+     *
+     * @return the version in the file
+     */
+    int getVersion();
+
+    /**
+     * Check that the file content is valid.
+     * It includes the check of duplicated elements (same identifier) and the check of each element.
+     *
+     * @return true if all the checks are OK
+     */
+    boolean isValid();
+}
diff --git a/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/YamlModelListener.java b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/YamlModelListener.java
new file mode 100644
index 00000000000..b15a8d9d639
--- /dev/null
+++ b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/YamlModelListener.java
@@ -0,0 +1,75 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.model.yaml;
+
+import java.util.Collection;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link YamlModelListener} interface is responsible for managing a particular model type
+ * with data processed from YAML configuration files.
+ *
+ * @author Laurent Garnier - Initial contribution
+ */
+@NonNullByDefault
+public interface YamlModelListener<T extends YamlElement> {
+
+    /**
+     * Method called by the model repository when elements from a model are added.
+     *
+     * @param modelName the name of the model
+     * @param elements the collection of added elements
+     */
+    void addedModel(String modelName, Collection<? extends YamlElement> elements);
+
+    /**
+     * Method called by the model repository when elements from a model are updated.
+     *
+     * @param modelName the name of the model
+     * @param elements the collection of updated elements
+     */
+    void updatedModel(String modelName, Collection<? extends YamlElement> elements);
+
+    /**
+     * Method called by the model repository when elements from a model are removed.
+     *
+     * @param modelName the name of the model
+     * @param elements the collection of removed elements
+     */
+    void removedModel(String modelName, Collection<? extends YamlElement> elements);
+
+    /**
+     * Get the root name of this model type which is also the name of the root folder
+     * containing the user files for this model type.
+     *
+     * A path is unexpected. What is expected is for example "items" or "things".
+     *
+     * @return the model root name
+     */
+    String getRootName();
+
+    /**
+     * Get the DTO class to be used for a file providing objects for this model type.
+     *
+     * @return the DTO file class
+     */
+    Class<? extends AbstractYamlFile> getFileClass();
+
+    /**
+     * Get the DTO class to be used for each object of this model type.
+     *
+     * @return the DTO element class
+     */
+    Class<T> getElementClass();
+}
diff --git a/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/YamlParseException.java b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/YamlParseException.java
new file mode 100644
index 00000000000..bae53ba78d9
--- /dev/null
+++ b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/YamlParseException.java
@@ -0,0 +1,39 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.model.yaml;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link YamlParseException} is used when an error is detected when parsing the content
+ * of a YAML configuration file.
+ *
+ * @author Laurent Garnier - Initial contribution
+ */
+@NonNullByDefault
+public class YamlParseException extends Exception {
+
+    private static final long serialVersionUID = 1L;
+
+    public YamlParseException(String message) {
+        super(message);
+    }
+
+    public YamlParseException(Throwable cause) {
+        super(cause);
+    }
+
+    public YamlParseException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git a/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/YamlModelRepository.java b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/YamlModelRepository.java
new file mode 100644
index 00000000000..a7d04138762
--- /dev/null
+++ b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/YamlModelRepository.java
@@ -0,0 +1,198 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.model.yaml.internal;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.model.yaml.AbstractYamlFile;
+import org.openhab.core.model.yaml.YamlElement;
+import org.openhab.core.model.yaml.YamlModelListener;
+import org.openhab.core.model.yaml.YamlParseException;
+import org.openhab.core.service.WatchService;
+import org.openhab.core.service.WatchService.Kind;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
+import org.osgi.service.component.annotations.ReferencePolicy;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
+
+/**
+ * The {@link YamlModelRepository} is an OSGi service, that encapsulates all YAML file processing
+ * including file monitoring to detect created, updated and removed YAML configuration files.
+ * Data processed from these files are consumed by registered OSGi services that implement {@link YamlModelListener}.
+ *
+ * @author Laurent Garnier - Initial contribution
+ */
+@NonNullByDefault
+@Component(immediate = true)
+public class YamlModelRepository implements WatchService.WatchEventListener {
+
+    private final Logger logger = LoggerFactory.getLogger(YamlModelRepository.class);
+
+    private final WatchService watchService;
+    private final Path watchPath;
+    private final ObjectMapper yamlReader;
+
+    private final Map<String, List<YamlModelListener<?>>> listeners = new ConcurrentHashMap<>();
+    private final Map<Path, List<? extends YamlElement>> objects = new ConcurrentHashMap<>();
+
+    @Activate
+    public YamlModelRepository(@Reference(target = WatchService.CONFIG_WATCHER_FILTER) WatchService watchService) {
+        this.watchService = watchService;
+        this.yamlReader = new ObjectMapper(new YAMLFactory());
+        yamlReader.findAndRegisterModules();
+
+        watchService.registerListener(this, Path.of(""));
+        watchPath = watchService.getWatchPath();
+    }
+
+    @Deactivate
+    public void deactivate() {
+        watchService.unregisterListener(this);
+    }
+
+    // The method is "synchronized" to avoid concurrent files processing
+    @Override
+    public synchronized void processWatchEvent(Kind kind, Path path) {
+        Path fullPath = watchPath.resolve(path);
+        String dirName = path.subpath(0, 1).toString();
+
+        if (Files.isDirectory(fullPath) || fullPath.toFile().isHidden() || !fullPath.toString().endsWith(".yaml")) {
+            logger.trace("Ignored {}", fullPath);
+            return;
+        }
+
+        getListeners(dirName).forEach(listener -> processWatchEvent(dirName, kind, fullPath, listener));
+    }
+
+    private void processWatchEvent(String dirName, Kind kind, Path fullPath, YamlModelListener<?> listener) {
+        logger.debug("processWatchEvent dirName={} kind={} fullPath={} listener={}", dirName, kind, fullPath,
+                listener.getClass().getSimpleName());
+        Map<String, ? extends YamlElement> oldObjects;
+        Map<String, ? extends YamlElement> newObjects;
+        if (kind == WatchService.Kind.DELETE) {
+            newObjects = Map.of();
+
+            List<? extends YamlElement> oldListObjects = objects.remove(fullPath);
+            if (oldListObjects == null) {
+                oldListObjects = List.of();
+            }
+            oldObjects = oldListObjects.stream().collect(Collectors.toMap(YamlElement::getId, obj -> obj));
+        } else {
+            AbstractYamlFile yamlData;
+            try {
+                yamlData = readYamlFile(fullPath, listener.getFileClass());
+            } catch (YamlParseException e) {
+                logger.warn("Failed to parse Yaml file {} with DTO class {}: {}", fullPath,
+                        listener.getFileClass().getName(), e.getMessage());
+                return;
+            }
+            List<? extends YamlElement> newListObjects = yamlData.getElements();
+            newObjects = newListObjects.stream().collect(Collectors.toMap(YamlElement::getId, obj -> obj));
+
+            List<? extends YamlElement> oldListObjects = objects.get(fullPath);
+            if (oldListObjects == null) {
+                oldListObjects = List.of();
+            }
+            oldObjects = oldListObjects.stream().collect(Collectors.toMap(YamlElement::getId, obj -> obj));
+
+            objects.put(fullPath, newListObjects);
+        }
+
+        String modelName = fullPath.toFile().getName();
+        modelName = modelName.substring(0, modelName.indexOf(".yaml"));
+        List<? extends YamlElement> listElements;
+        listElements = oldObjects.entrySet().stream()
+                .filter(entry -> entry.getValue().getClass().equals(listener.getElementClass())
+                        && !newObjects.containsKey(entry.getKey()))
+                .map(Map.Entry::getValue).toList();
+        if (!listElements.isEmpty()) {
+            listener.removedModel(modelName, listElements);
+        }
+
+        listElements = newObjects.entrySet().stream()
+                .filter(entry -> entry.getValue().getClass().equals(listener.getElementClass())
+                        && !oldObjects.containsKey(entry.getKey()))
+                .map(Map.Entry::getValue).toList();
+        if (!listElements.isEmpty()) {
+            listener.addedModel(modelName, listElements);
+        }
+
+        // Object is ignored if unchanged
+        listElements = newObjects.entrySet().stream()
+                .filter(entry -> entry.getValue().getClass().equals(listener.getElementClass())
+                        && oldObjects.containsKey(entry.getKey())
+                        && !entry.getValue().equals(oldObjects.get(entry.getKey())))
+                .map(Map.Entry::getValue).toList();
+        if (!listElements.isEmpty()) {
+            listener.updatedModel(modelName, listElements);
+        }
+    }
+
+    private AbstractYamlFile readYamlFile(Path path, Class<? extends AbstractYamlFile> dtoClass)
+            throws YamlParseException {
+        logger.debug("readYamlFile {} with {}", path.toFile().getAbsolutePath(), dtoClass.getName());
+        try {
+            AbstractYamlFile dto = yamlReader.readValue(path.toFile(), dtoClass);
+            if (!dto.isValid()) {
+                throw new YamlParseException("The file is not valid, some checks failed!");
+            }
+            return dto;
+        } catch (IOException e) {
+            throw new YamlParseException(e);
+        }
+    }
+
+    @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC)
+    protected void addYamlModelListener(YamlModelListener<?> listener) {
+        String dirName = listener.getRootName();
+        logger.debug("Adding model listener for {}", dirName);
+        getListeners(dirName).add(listener);
+
+        // Load all existing YAML files
+        try (Stream<Path> stream = Files.walk(watchPath.resolve(dirName))) {
+            stream.forEach(path -> {
+                if (!Files.isDirectory(path) && !path.toFile().isHidden() && path.toString().endsWith(".yaml")) {
+                    processWatchEvent(dirName, Kind.CREATE, path, listener);
+                }
+            });
+        } catch (IOException ignored) {
+        }
+    }
+
+    protected void removeYamlModelListener(YamlModelListener<?> listener) {
+        String dirName = listener.getRootName();
+        logger.debug("Removing model listener for {}", dirName);
+        getListeners(dirName).remove(listener);
+    }
+
+    private List<YamlModelListener<?>> getListeners(String dirName) {
+        return Objects.requireNonNull(listeners.computeIfAbsent(dirName, k -> new CopyOnWriteArrayList<>()));
+    }
+}
diff --git a/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/semantics/YamlSemanticTag.java b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/semantics/YamlSemanticTag.java
new file mode 100644
index 00000000000..9d8f0865abd
--- /dev/null
+++ b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/semantics/YamlSemanticTag.java
@@ -0,0 +1,66 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.model.yaml.internal.semantics;
+
+import java.util.List;
+import java.util.Objects;
+
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.model.yaml.YamlElement;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link YamlSemanticTag} is a data transfer object used to serialize a semantic tag
+ * in a YAML configuration file.
+ *
+ * @author Laurent Garnier - Initial contribution
+ */
+public class YamlSemanticTag implements YamlElement {
+
+    private final Logger logger = LoggerFactory.getLogger(YamlSemanticTag.class);
+
+    public String uid;
+    public String label;
+    public String description;
+    public List<String> synonyms;
+
+    public YamlSemanticTag() {
+    }
+
+    @Override
+    public String getId() {
+        return uid;
+    }
+
+    @Override
+    public boolean isValid() {
+        if (uid == null) {
+            logger.debug("uid missing");
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (this == obj) {
+            return true;
+        } else if (obj == null || getClass() != obj.getClass()) {
+            return false;
+        }
+        YamlSemanticTag that = (YamlSemanticTag) obj;
+        return Objects.equals(uid, that.uid) && Objects.equals(label, that.label)
+                && Objects.equals(description, that.description) && Objects.equals(synonyms, that.synonyms);
+    }
+}
diff --git a/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/semantics/YamlSemanticTagProvider.java b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/semantics/YamlSemanticTagProvider.java
new file mode 100644
index 00000000000..4f13120b9a1
--- /dev/null
+++ b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/semantics/YamlSemanticTagProvider.java
@@ -0,0 +1,126 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.model.yaml.internal.semantics;
+
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Set;
+import java.util.TreeSet;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.common.registry.AbstractProvider;
+import org.openhab.core.model.yaml.AbstractYamlFile;
+import org.openhab.core.model.yaml.YamlElement;
+import org.openhab.core.model.yaml.YamlModelListener;
+import org.openhab.core.semantics.SemanticTag;
+import org.openhab.core.semantics.SemanticTagImpl;
+import org.openhab.core.semantics.SemanticTagProvider;
+import org.openhab.core.semantics.SemanticTagRegistry;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * {@link YamlSemanticTagProvider} is an OSGi service, that allows to define semantic tags
+ * in YAML configuration files in folder conf/tags.
+ * Files can be added, updated or removed at runtime.
+ * These semantic tags are automatically exposed to the {@link SemanticTagRegistry}.
+ *
+ * @author Laurent Garnier - Initial contribution
+ */
+@NonNullByDefault
+@Component(immediate = true, service = { SemanticTagProvider.class, YamlSemanticTagProvider.class,
+        YamlModelListener.class })
+public class YamlSemanticTagProvider extends AbstractProvider<SemanticTag>
+        implements SemanticTagProvider, YamlModelListener<YamlSemanticTag> {
+
+    private final Logger logger = LoggerFactory.getLogger(YamlSemanticTagProvider.class);
+
+    private final Set<SemanticTag> tags = new TreeSet<>(Comparator.comparing(SemanticTag::getUID));
+
+    @Activate
+    public YamlSemanticTagProvider() {
+    }
+
+    @Deactivate
+    public void deactivate() {
+        tags.clear();
+    }
+
+    @Override
+    public Collection<SemanticTag> getAll() {
+        return tags;
+    }
+
+    @Override
+    public String getRootName() {
+        return "tags";
+    }
+
+    @Override
+    public Class<? extends AbstractYamlFile> getFileClass() {
+        return YamlSemanticTags.class;
+    }
+
+    @Override
+    public Class<YamlSemanticTag> getElementClass() {
+        return YamlSemanticTag.class;
+    }
+
+    @Override
+    public void addedModel(String modelName, Collection<? extends YamlElement> elements) {
+        List<SemanticTag> added = elements.stream().map(e -> mapSemanticTag((YamlSemanticTag) e))
+                .sorted(Comparator.comparing(SemanticTag::getUID)).toList();
+        tags.addAll(added);
+        added.forEach(t -> {
+            logger.debug("model {} added tag {}", modelName, t.getUID());
+            notifyListenersAboutAddedElement(t);
+        });
+    }
+
+    @Override
+    public void updatedModel(String modelName, Collection<? extends YamlElement> elements) {
+        List<SemanticTag> updated = elements.stream().map(e -> mapSemanticTag((YamlSemanticTag) e)).toList();
+        updated.forEach(t -> {
+            tags.stream().filter(tag -> tag.getUID().equals(t.getUID())).findFirst().ifPresentOrElse(oldTag -> {
+                tags.remove(oldTag);
+                tags.add(t);
+                logger.debug("model {} updated tag {}", modelName, t.getUID());
+                notifyListenersAboutUpdatedElement(oldTag, t);
+            }, () -> logger.debug("model {} tag {} not found", modelName, t.getUID()));
+        });
+    }
+
+    @Override
+    public void removedModel(String modelName, Collection<? extends YamlElement> elements) {
+        List<SemanticTag> removed = elements.stream().map(e -> mapSemanticTag((YamlSemanticTag) e))
+                .sorted(Comparator.comparing(SemanticTag::getUID).reversed()).toList();
+        removed.forEach(t -> {
+            tags.stream().filter(tag -> tag.getUID().equals(t.getUID())).findFirst().ifPresentOrElse(oldTag -> {
+                tags.remove(oldTag);
+                logger.debug("model {} removed tag {}", modelName, t.getUID());
+                notifyListenersAboutRemovedElement(oldTag);
+            }, () -> logger.debug("model {} tag {} not found", modelName, t.getUID()));
+        });
+    }
+
+    private SemanticTag mapSemanticTag(YamlSemanticTag tagDTO) {
+        if (tagDTO.uid == null) {
+            throw new IllegalArgumentException("The argument 'tagDTO.uid' must not be null.");
+        }
+        return new SemanticTagImpl(tagDTO.uid, tagDTO.label, tagDTO.description, tagDTO.synonyms);
+    }
+}
diff --git a/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/semantics/YamlSemanticTags.java b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/semantics/YamlSemanticTags.java
new file mode 100644
index 00000000000..3c2a4ee9609
--- /dev/null
+++ b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/semantics/YamlSemanticTags.java
@@ -0,0 +1,36 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.model.yaml.internal.semantics;
+
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.model.yaml.AbstractYamlFile;
+import org.openhab.core.model.yaml.YamlElement;
+
+/**
+ * The {@link YamlSemanticTags} is a data transfer object used to serialize a list of semantic tags
+ * in a YAML configuration file.
+ *
+ * @author Laurent Garnier - Initial contribution
+ */
+@NonNullByDefault
+public class YamlSemanticTags extends AbstractYamlFile {
+
+    public List<YamlSemanticTag> tags = List.of();
+
+    @Override
+    public List<? extends YamlElement> getElements() {
+        return tags;
+    }
+}
diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/internal/SemanticsMetadataProvider.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/internal/SemanticsMetadataProvider.java
index 863594d15b8..93c842f06a4 100644
--- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/internal/SemanticsMetadataProvider.java
+++ b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/internal/SemanticsMetadataProvider.java
@@ -22,6 +22,7 @@
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.openhab.core.common.registry.AbstractProvider;
+import org.openhab.core.common.registry.RegistryChangeListener;
 import org.openhab.core.items.GroupItem;
 import org.openhab.core.items.Item;
 import org.openhab.core.items.ItemRegistry;
@@ -33,6 +34,7 @@
 import org.openhab.core.semantics.Location;
 import org.openhab.core.semantics.Point;
 import org.openhab.core.semantics.Property;
+import org.openhab.core.semantics.SemanticTag;
 import org.openhab.core.semantics.SemanticTagRegistry;
 import org.openhab.core.semantics.SemanticTags;
 import org.openhab.core.semantics.Tag;
@@ -72,11 +74,16 @@ public class SemanticsMetadataProvider extends AbstractProvider<Metadata>
     private final Map<String, Metadata> semantics = new TreeMap<>(String::compareTo);
 
     private final ItemRegistry itemRegistry;
+    private final SemanticTagRegistry semanticTagRegistry;
+
+    private SemanticTagRegistryChangeListener listener;
 
     @Activate
     public SemanticsMetadataProvider(final @Reference ItemRegistry itemRegistry,
             final @Reference SemanticTagRegistry semanticTagRegistry) {
         this.itemRegistry = itemRegistry;
+        this.semanticTagRegistry = semanticTagRegistry;
+        this.listener = new SemanticTagRegistryChangeListener(this);
     }
 
     @Activate
@@ -86,10 +93,12 @@ protected void activate() {
             processItem(item);
         }
         itemRegistry.addRegistryChangeListener(this);
+        semanticTagRegistry.addRegistryChangeListener(listener);
     }
 
     @Deactivate
     protected void deactivate() {
+        semanticTagRegistry.removeRegistryChangeListener(listener);
         itemRegistry.removeRegistryChangeListener(this);
         semantics.clear();
     }
@@ -280,4 +289,28 @@ public void removed(Item item) {
     public void updated(Item oldItem, Item item) {
         processItem(item);
     }
+
+    private class SemanticTagRegistryChangeListener implements RegistryChangeListener<SemanticTag> {
+
+        private SemanticsMetadataProvider provider;
+
+        public SemanticTagRegistryChangeListener(SemanticsMetadataProvider provider) {
+            this.provider = provider;
+        }
+
+        @Override
+        public void added(SemanticTag element) {
+            provider.allItemsChanged(List.of());
+        }
+
+        @Override
+        public void removed(SemanticTag element) {
+            provider.allItemsChanged(List.of());
+        }
+
+        @Override
+        public void updated(SemanticTag oldElement, SemanticTag element) {
+            provider.allItemsChanged(List.of());
+        }
+    }
 }
diff --git a/bundles/pom.xml b/bundles/pom.xml
index 70000056d92..29f7257094f 100644
--- a/bundles/pom.xml
+++ b/bundles/pom.xml
@@ -99,6 +99,7 @@
     <module>org.openhab.core.model.thing</module>
     <module>org.openhab.core.model.thing.ide</module>
     <module>org.openhab.core.model.thing.runtime</module>
+    <module>org.openhab.core.model.yaml</module>
     <module>org.openhab.core.storage.json</module>
     <module>org.openhab.core.test</module>
     <module>org.openhab.core.test.magic</module>
diff --git a/features/karaf/openhab-core/src/main/feature/feature.xml b/features/karaf/openhab-core/src/main/feature/feature.xml
index 5cdcf866a1e..c8d8c0b2dec 100644
--- a/features/karaf/openhab-core/src/main/feature/feature.xml
+++ b/features/karaf/openhab-core/src/main/feature/feature.xml
@@ -367,6 +367,13 @@
 		<bundle>mvn:org.openhab.core.bundles/org.openhab.core.model.lsp/${project.version}</bundle>
 	</feature>
 
+	<feature name="openhab-core-model-yaml" version="${project.version}">
+		<feature>openhab-core-base</feature>
+		<bundle>mvn:org.openhab.core.bundles/org.openhab.core.model.yaml/${project.version}</bundle>
+		<requirement>openhab.tp;filter:="(feature=jackson)"</requirement>
+		<feature dependency="true">openhab.tp-jackson</feature>
+	</feature>
+
 	<feature name="openhab-core-storage-json" version="${project.version}">
 		<feature>openhab-core-base</feature>
 
@@ -411,6 +418,7 @@
 		<feature>openhab-core-model-script</feature>
 		<feature>openhab-core-model-sitemap</feature>
 		<feature>openhab-core-model-thing</feature>
+		<feature>openhab-core-model-yaml</feature>
 		<feature>openhab-core-ui-icon</feature>
 		<feature>openhab-core-storage-json</feature>
 		<feature>openhab-runtime-certificate</feature>