diff --git a/CODEOWNERS b/CODEOWNERS
index 0e80ca1e07..33b01fe933 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -473,6 +473,7 @@
/bundles/org.openhab.io.metrics/ @pravussum
/bundles/org.openhab.io.neeo/ @morph166955
/bundles/org.openhab.io.openhabcloud/ @kaikreuzer
+/bundles/org.openhab.io.yamlcomposer/ @jimtng
/bundles/org.openhab.persistence.dynamodb/ @ssalonen
/bundles/org.openhab.persistence.influxdb/ @lujop
/bundles/org.openhab.persistence.inmemory/ @J-N-K
diff --git a/bundles/org.openhab.io.yamlcomposer/NOTICE b/bundles/org.openhab.io.yamlcomposer/NOTICE
new file mode 100644
index 0000000000..38d625e349
--- /dev/null
+++ b/bundles/org.openhab.io.yamlcomposer/NOTICE
@@ -0,0 +1,13 @@
+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-addons
diff --git a/bundles/org.openhab.io.yamlcomposer/README.md b/bundles/org.openhab.io.yamlcomposer/README.md
new file mode 100644
index 0000000000..ff075c030f
--- /dev/null
+++ b/bundles/org.openhab.io.yamlcomposer/README.md
@@ -0,0 +1,329 @@
+# YAML Composer
+
+YAML Composer introduces extended YAML features that make openHAB configuration more modular, reusable, and maintainable. These features let you structure configuration as composable building blocks rather than large, repetitive files.
+
+The add-on loads enhanced-syntax YAML files from `OPENHAB_CONF/yamlcomposer/` and compiles them into fully resolved plain YAML written to `OPENHAB_CONF/yaml/composed/`.
+
+[[toc]]
+
+## Feature Summary
+
+YAML Composer adds several enhancements on top of standard YAML.
+Each feature addresses a different kind of reuse, composition, or abstraction to help you build cleaner and more maintainable YAML.
+
+| Feature | Purpose | Typical Use |
+|--------------------------------------------|-----------------------------------------------------------|----------------------------------------------------------------------------------------------------------------|
+| **Variables and Substitution (`${..}`)** | Insert dynamic values or evaluate expressions | Build labels, topics, IDs, or computed values |
+| **Conditionals (`!if`)** | Conditionally include or exclude YAML blocks | Enable or disable features when using packages or template flags |
+| **Include (`!include`)** | Insert the contents of another file | Reuse YAML across files; parameterize reusable blocks |
+| **Templates (`!insert`)** | Reuse YAML defined within the same file | Local parameterized blocks; reusable channel or item fragments |
+| **Packages** | Bundle multiple top-level sections into one reusable unit | Define reusable device structures containing things, items, metadata; sourced from external files or templates |
+| **Anchors and Aliases (`&name`, `*name`)** | Define small, reusable YAML fragments | Static defaults, shared fields |
+| **Merge Keys (`<<:`)** | Combine mappings from multiple sources | Layer defaults, override fields, compose structures |
+
+Each feature has a dedicated documentation page:
+
+- [Variables and Substitution](doc/variables.md)
+- [Conditionals](doc/conditionals.md)
+- [Include](doc/include.md)
+- [Templates](doc/templates.md)
+- [Packages](doc/packages.md)
+- [Anchors and Aliases](doc/anchors.md)
+- [Merge Keys](doc/merge-keys.md)
+
+These features can be used independently, but they become especially powerful when combined.
+
+For a general introduction to YAML, see [YAML Basics](doc/basics.md).
+
+## Packaging Example
+
+**CONF/yamlcomposer/LivingRoom.yaml:**
+
+```yaml
+version: 1
+
+variables:
+ # Captures "LivingRoom" from the filename automatically
+ location: ${__FILE_NAME__} # => LivingRoom
+
+packages:
+ Light1: !include
+ file: $pkg/zigbee_light.inc.yaml
+ vars: &LIGHT_VARS
+ color_temperature: {} # Include color temperature feature
+ power: # Customize the power item
+ groups:
+ - gInsideLights
+ - gSmartLights
+
+ Light2: !include
+ file: $pkg/zigbee_light.inc.yaml
+ vars:
+ <<: *LIGHT_VARS
+```
+
+**CONF/yamlcomposer/pkg/zigbee_light.inc.yaml:**
+
+```yaml
+# This is the package file, i.e. the template for a zigbee light
+variables:
+ id: ${location}_${package_id}
+ thingid: ${id | lower | replace("_", "-")}
+ equipment: ${id}_Equipment
+ label: ${id | label}
+
+ # Set defaults
+ power: &DEFAULTS
+ groups: []
+ dimmer:
+ <<: *DEFAULTS
+
+things:
+ mqtt:topic:${thingid}:
+ bridge: mqtt:broker:mosquitto
+ channels:
+ power:
+ type: switch
+ config:
+ stateTopic: zigbee2mqtt/${thingid}/state
+ commandTopic: zigbee2mqtt/${thingid}/set/state
+
+ dimmer:
+ type: dimmer
+ config:
+ stateTopic: zigbee2mqtt/${thingid}/brightness
+ commandTopic: zigbee2mqtt/${thingid}/set/brightness
+
+ <<: !if
+ if: VARS.containsKey('color_temperature')
+ then:
+ color-temperature: !sub
+ type: dimmer
+ config:
+ stateTopic: zigbee2mqtt/${thingid}/color_temp
+ commandTopic: zigbee2mqtt/${thingid}/set/color_temp
+
+items:
+ ${equipment}:
+ type: Group
+ label: ${label} Equipment
+ tags: [Lightbulb]
+ groups: ${[location]}
+
+ ${id}:
+ type: Switch
+ label: ${label}
+ tags: [Control, Light]
+ groups: ${[equipment] + power.groups}
+ channel: mqtt:topic:${thingid}:power
+
+ ${id}_Dimmer:
+ type: Dimmer
+ label: ${label} Brightness
+ tags: [Control, Level]
+ groups: ${[equipment] + dimmer.groups}
+ channel: mqtt:topic:${thingid}:dimmer
+
+ <<: !if
+ if: VARS.containsKey('color_temperature')
+ then:
+ ${id}_CT:
+ type: Dimmer
+ label: ${label} Color Temperature
+ tags: [Control, ColorTemperature]
+ groups: ${[equipment] + color_temperature.groups}
+ channel: mqtt:topic:${thingid}:color-temperature
+```
+
+
+Output in CONF/yaml/composed/LivingRoom.yaml:
+
+```yaml
+version: 1
+
+things:
+ mqtt:topic:livingroom-light1:
+ bridge: mqtt:broker:mosquitto
+ channels:
+ power:
+ type: switch
+ config:
+ stateTopic: zigbee2mqtt/livingroom-light1/state
+ commandTopic: zigbee2mqtt/livingroom-light1/set/state
+ dimmer:
+ type: dimmer
+ config:
+ stateTopic: zigbee2mqtt/livingroom-light1/brightness
+ commandTopic: zigbee2mqtt/livingroom-light1/set/brightness
+ color-temperature:
+ type: dimmer
+ config:
+ stateTopic: zigbee2mqtt/livingroom-light1/color_temp
+ commandTopic: zigbee2mqtt/livingroom-light1/set/color_temp
+
+ mqtt:topic:livingroom-light2:
+ bridge: mqtt:broker:mosquitto
+ channels:
+ power:
+ type: switch
+ config:
+ stateTopic: zigbee2mqtt/livingroom-light2/state
+ commandTopic: zigbee2mqtt/livingroom-light2/set/state
+ dimmer:
+ type: dimmer
+ config:
+ stateTopic: zigbee2mqtt/livingroom-light2/brightness
+ commandTopic: zigbee2mqtt/livingroom-light2/set/brightness
+ color-temperature:
+ type: dimmer
+ config:
+ stateTopic: zigbee2mqtt/livingroom-light2/color_temp
+ commandTopic: zigbee2mqtt/livingroom-light2/set/color_temp
+
+items:
+ LivingRoom_Light1_Equipment:
+ type: Group
+ label: Living Room Light 1 Equipment
+ tags:
+ - Lightbulb
+ groups:
+ - LivingRoom
+
+ LivingRoom_Light1:
+ type: Switch
+ label: Living Room Light 1
+ tags:
+ - Control
+ - Light
+ groups:
+ - LivingRoom_Light1_Equipment
+ - gInsideLights
+ - gSmartLights
+ channel: mqtt:topic:livingroom-light1:power
+
+ LivingRoom_Light1_Dimmer:
+ type: Dimmer
+ label: Living Room Light 1 Brightness
+ tags:
+ - Control
+ - Level
+ groups:
+ - LivingRoom_Light1_Equipment
+ channel: mqtt:topic:livingroom-light1:dimmer
+
+ LivingRoom_Light1_CT:
+ type: Dimmer
+ label: Living Room Light 1 Color Temperature
+ tags:
+ - Control
+ - ColorTemperature
+ groups:
+ - LivingRoom_Light1_Equipment
+ channel: mqtt:topic:livingroom-light1:color-temperature
+
+ LivingRoom_Light2_Equipment:
+ type: Group
+ label: Living Room Light 2 Equipment
+ tags:
+ - Lightbulb
+ groups:
+ - LivingRoom
+
+ LivingRoom_Light2:
+ type: Switch
+ label: Living Room Light 2
+ tags:
+ - Control
+ - Light
+ groups:
+ - LivingRoom_Light2_Equipment
+ - gInsideLights
+ - gSmartLights
+ channel: mqtt:topic:livingroom-light2:power
+
+ LivingRoom_Light2_Dimmer:
+ type: Dimmer
+ label: Living Room Light 2 Brightness
+ tags:
+ - Control
+ - Level
+ groups:
+ - LivingRoom_Light2_Equipment
+ channel: mqtt:topic:livingroom-light2:dimmer
+
+ LivingRoom_Light2_CT:
+ type: Dimmer
+ label: Living Room Light 2 Color Temperature
+ tags:
+ - Control
+ - ColorTemperature
+ groups:
+ - LivingRoom_Light2_Equipment
+ channel: mqtt:topic:livingroom-light2:color-temperature
+```
+
+
+
+## Processing Overview
+
+YAML Composer reads enhanced-syntax YAML files and performs a compilation pass that expands all extended features into a single, fully resolved YAML document that openHAB can load.
+
+During compilation, YAML Composer performs the following steps:
+
+1. **YAML Parsing**: The source file is parsed into an internal structure.
+1. **Variable Substitution (`${..}`)**: Expressions are evaluated and injected.
+1. **Conditionals (`!if`)**: Conditional logic determines which blocks remain.
+1. **Template and Include Expansion**: `!insert` and `!include` bring in referenced content, often using resolved variables.
+1. **Package Expansion**: External or local packages are loaded and expanded into their component sections.
+1. **Recursive Merging**: Merge keys and package structures are combined into the main document.
+1. **Hidden Key Removal**: Keys beginning with `.` are removed from the final output.
+
+The resulting YAML contains:
+
+- all variables resolved
+- all conditionals evaluated
+- all templates and includes expanded
+- all anchors and merges applied
+- all packages integrated
+- all hidden keys removed
+
+This final compiled YAML is written to `OPENHAB_CONF/yaml/composed/`, where openHAB loads it as Things, Items, Metadata, and other configuration elements as defined by the Core YAML Configuration structure.
+
+## Hidden Keys
+
+Keys beginning with a dot (`.`) are treated as hidden. They:
+
+- exist only during compilation
+- are ideal for storing anchors, templates, or shared structures
+- keep visible configuration clean
+- are removed from the final output
+
+**Example:**
+
+```yaml
+.base-switch: &BASE_SWITCH
+ type: Switch
+ autoupdate: false
+
+items:
+ Light1:
+ <<: *BASE_SWITCH
+ label: Light One
+```
+
+## File Structure and Conventions
+
+YAML files can be organized freely, but the following conventions improve clarity and maintainability:
+
+- Place `variables:` and `templates:` near the top of the file.
+- Group reusable structures under hidden keys.
+- Use anchors for static fragments and includes for parameterized ones.
+- Keep packages in separate files when they represent reusable device or feature definitions.
+
+These conventions are optional but help keep complex configurations predictable and easy to navigate.
+
+### File naming convention
+
+- Use `*.yaml` or `*.yml` for main source files that produce composed output.
+- Use `*.inc.yaml` or `*.inc.yml` for include fragments referenced by `!include`.
+- Mixed-role files (both main and include) are not supported.
diff --git a/bundles/org.openhab.io.yamlcomposer/doc/anchors.md b/bundles/org.openhab.io.yamlcomposer/doc/anchors.md
new file mode 100644
index 0000000000..0bbd93373e
--- /dev/null
+++ b/bundles/org.openhab.io.yamlcomposer/doc/anchors.md
@@ -0,0 +1,63 @@
+# Anchors & Aliases
+
+Anchors and aliases provide efficient, in‑file content reuse to reduce repetition without the overhead of external include files or packages.
+
+[[toc]]
+
+## Core Concepts
+
+YAML provides two primary symbols for defining reusable content:
+
+- **Anchor (`&name`)** — Marks a YAML node (a map, list, or scalar) so it can be reused later.
+- **Alias (`*name`)** — References a previously anchored node and injects its content at the current location.
+
+::: tip Scope Note
+Anchors and aliases are **file‑local**.
+An anchor defined in an included file cannot be referenced from the main file, and vice versa.
+:::
+
+### Using Hidden Keys for Anchors
+
+Hidden keys are a convenient place to store anchored nodes without exposing them in the final configuration.
+
+For a full explanation of hidden keys, see [Hidden Keys](../#hidden-keys).
+
+Anchors are often combined with the YAML **merge key** (`<<:`), which inserts the contents of an anchored map into the current map.
+For full merge‑key semantics, see [Merge Keys](merge-keys.md).
+
+```yaml
+.base-switch: &BASE_SWITCH
+ type: Switch
+ autoupdate: false
+
+items:
+ Light1:
+ <<: *BASE_SWITCH
+ label: Light One
+```
+
+Anchors can also be applied to scalar values.
+This works similarly to using a variable:
+
+```yaml
+.bar: &BAR value
+
+foo: *BAR
+```
+
+## When to Use Anchors
+
+Anchors and aliases are most useful when reusing structural blocks **within the same file**.
+They work well for:
+
+- repeating common fields across many entries
+- sharing channel or configuration structures
+- keeping small reusable fragments close to where they are used
+
+Anchors are a YAML‑native mechanism.
+They participate in variable substitution, but they do not support cross‑file reuse or template‑style evaluation.
+
+## Best Practices
+
+- **Naming** — Use `UPPER_CASE` for anchors to distinguish them from standard keys.
+- **Standardization** — Use anchors to enforce consistent structure across repeated YAML blocks within a file.
diff --git a/bundles/org.openhab.io.yamlcomposer/doc/basics.md b/bundles/org.openhab.io.yamlcomposer/doc/basics.md
new file mode 100644
index 0000000000..122adeff71
--- /dev/null
+++ b/bundles/org.openhab.io.yamlcomposer/doc/basics.md
@@ -0,0 +1,329 @@
+# Standard YAML Syntax
+
+This page introduces the standard YAML syntax used throughout this documentation.
+It is not a full YAML tutorial.
+It provides the foundational concepts needed to understand and work with the examples in the YAML Configuration section.
+
+The YAML Composer builds on top of standard YAML 1.2 and adds its own processing features such as custom tags and preprocessing behavior.
+This page focuses on the core YAML syntax that those features extend.
+
+For more detailed information on the YAML 1.2 syntax supported in openHAB configurations, see the [YAML 1.2 specification](https://yaml.org/spec/1.2/).
+
+[[toc]]
+
+## Indentation and Structure
+
+YAML uses indentation to represent structure.
+
+- Indentation defines the nesting level.
+- Only spaces are allowed.
+- Tabs are not permitted for indentation.
+- All items at the same level must use the same indentation.
+
+openHAB recommends using **two spaces** per indentation level for consistency.
+
+```yaml
+thing:
+ id: myThing
+ label: "Example Thing"
+```
+
+Misaligned indentation is one of the most common YAML errors.
+
+## Block Style vs Flow Style
+
+YAML supports two ways of expressing mappings and lists.
+
+### Block Style (Recommended)
+
+Block style is the most readable form.
+
+```yaml
+item:
+ type: Switch
+ label: "Lamp"
+ tags:
+ - Light
+ - LivingRoom
+```
+
+### Flow Style
+
+Flow style is more compact and resembles JSON.
+It is functionally identical to block style.
+
+```yaml
+item: { type: Switch, label: "Lamp", tags: [Light, LivingRoom] }
+```
+
+YAML allows block and flow styles to be mixed freely.
+
+## Comments
+
+- Comments begin with `#`.
+- Comments continue to the end of the line.
+- Comments may appear on their own line or after a value.
+
+```yaml
+# This is a comment describing the thing
+thing:
+ id: myThing # Inline comment
+ label: "Example Thing"
+```
+
+## Data Types
+
+YAML defines three fundamental data types: **scalars**, **lists** (sequences), and **mappings**.
+
+### Scalars
+
+Scalars represent simple values such as strings, numbers, booleans, and nulls.
+
+#### Booleans
+
+In openHAB YAML files, only the unquoted literals `true`, `True`, `TRUE`, and `false`, `False`, `FALSE` are recognized as boolean values.
+`ON`, `OFF`, `Yes`, `No`, `enable`, and `disable` are parsed as plain strings.
+To specify `true` or `false` as a string, they must be enclosed in single or double quotes.
+
+#### Strings
+
+Strings may be written with or without quotes.
+
+Unquoted strings are allowed only when the value **cannot** be interpreted as another YAML scalar type such as a number, boolean, or null.
+In practice, this means unquoted strings should contain letters or simple punctuation, but **not** values that look like numbers or booleans.
+
+```yaml
+name: KitchenLight
+label: Kitchen Light
+role: admin
+```
+
+If a value could be parsed as a number, boolean, or null, it must be quoted to ensure it is treated as a string.
+
+```yaml
+string_number: "123"
+string_boolean: "true"
+string_null: "null"
+```
+
+Quotes are required when:
+
+- The string contains characters YAML may interpret as syntax.
+- The string contains leading or trailing spaces.
+- The string contains a colon followed by a space, which YAML interprets as a key/value separator.
+
+```yaml
+label: "Kitchen: Main Light"
+note: " This string preserves leading spaces"
+```
+
+Single‑quoted and double‑quoted strings behave differently.
+
+- Single‑quoted strings treat the content literally.
+- Single‑quoted strings do not process escape sequences.
+- Single‑quoted strings are useful when you want the text to appear exactly as written.
+
+```yaml
+description: 'This string contains \n and it will not be interpreted as a newline'
+```
+
+- Double‑quoted strings support escape sequences.
+- Double‑quoted strings allow characters such as `\n`, `\t`, and `\"` to be interpreted.
+
+```yaml
+message: "Line one\nLine two"
+```
+
+- Use single quotes when you want literal text or when the string contains double quotes.
+- Use double quotes when you need escape sequences or when the string contains single quotes and requires escaping.
+
+#### Multiline Strings
+
+Multiline strings are used for longer descriptions or scripts.
+
+Use `|` to preserve line breaks exactly as written.
+
+```yaml
+description: |
+ This is a multi-line
+ description.
+ Line breaks are preserved.
+```
+
+Use `>` to fold lines into a single paragraph.
+
+```yaml
+notes: >
+ This text will be folded
+ into a single line,
+ with spaces instead of line breaks.
+```
+
+- `|` preserves line breaks.
+- `>` folds single line breaks into spaces.
+- Blank lines are preserved when using `>`.
+- Indentation under `|` or `>` must be consistent and becomes part of the string.
+
+#### Numbers
+
+YAML supports numeric scalar values without requiring quotes.
+A number is interpreted as numeric when it contains only digits, with an optional sign, decimal point, or exponent.
+
+##### Valid Examples
+
+```yaml
+positive_integer: 42
+negative_integer: -7
+decimal_number: 3.1415
+negative_decimal: -0.25
+scientific_notation: 1.2e6
+```
+
+##### When to Use Quotes
+
+Numbers must be quoted if you want them treated as **strings**:
+
+```yaml
+string_number: "42"
+leading_zero_string: "007"
+```
+
+#### Null
+
+YAML supports a null value using any of the following forms:
+
+```yaml
+value1: null
+value2: Null
+value3: NULL
+value4: ~
+value5:
+```
+
+All of these are interpreted as `null`.
+
+### Lists (Sequences)
+
+Lists are introduced with a dash (`-`) at the start of each item.
+
+```yaml
+tags:
+ - Light
+ - LivingRoom
+```
+
+Flow style is also supported:
+
+```yaml
+tags: [Light, LivingRoom]
+```
+
+### Mappings (Key/Value Pairs)
+
+Mappings define key/value structures.
+
+```yaml
+stateDescription:
+ config:
+ pattern: "%.1f °C"
+```
+
+Flow style is also supported:
+
+```yaml
+stateDescription: { config: { pattern: "%.1f °C" } }
+```
+
+#### How YAML Identifies a Mapping Entry
+
+A mapping entry uses the pattern:
+
+```yaml
+key: value
+```
+
+The space after the colon is required.
+
+```yaml
+text: "Alice: Hello" # Must be quoted because the colon is followed by a space
+url: http://example.com # Safe unquoted because the colon is not followed by a space
+```
+
+## Anchors and Aliases
+
+Anchors define reusable blocks.
+Aliases reference those blocks.
+
+```yaml
+defaults: &defaultConfig
+ refresh: 60
+ geolocation: 1.23456, 7.890123
+
+thing:
+ label: "Lamp"
+ config: *defaultConfig
+```
+
+For more details, see [Anchors and Aliases](anchors.md).
+
+## Merge Keys
+
+YAML merge keys use the `<<:` syntax to merge mappings.
+
+> **Note:** Although merge keys (`<<:`) are not defined in YAML 1.2, the YAML Composer includes full support for them.
+> They are a foundational mechanism used throughout the preprocessing system to combine mappings, apply defaults, and build modular configuration structures.
+
+```yaml
+base: &base
+ type: Switch
+ category: Light
+
+item:
+ <<: *base
+ label: "Desk Lamp"
+```
+
+Effective structure:
+
+```yaml
+item:
+ type: Switch
+ category: Light
+ label: "Desk Lamp"
+```
+
+For more details, see [Merge Keys](merge-keys.md).
+
+## YAML Tags (Brief Introduction)
+
+YAML supports tags, which change how a value is interpreted.
+Some tags are built in, and YAML Composer adds several **custom tags** such as `!include`, `!if`, and `!remove` to enable enhanced features.
+
+A tag can be applied to a simple scalar:
+
+```yaml
+value: !include file.inc.yaml
+```
+
+A tag can also be applied to a mapping.
+In that form, it applies to everything inside the mapping:
+
+```yaml
+thing: !if
+ if: ${thing_enabled}
+ then:
+ label: Room Light
+```
+
+You will see these custom tags throughout the enhanced YAML configuration.
+
+## Common Pitfalls
+
+- Using tab characters instead of spaces for indentation.
+- Using inconsistent indentation.
+- Misaligning list items.
+- Omitting quotes around strings containing special characters.
+- Incorrectly nesting mappings.
+- Incorrect indentation under `|` or `>` for multiline strings.
+
+If a YAML file fails to load, indentation, quoting, or multiline formatting issues are often the cause.
diff --git a/bundles/org.openhab.io.yamlcomposer/doc/conditionals.md b/bundles/org.openhab.io.yamlcomposer/doc/conditionals.md
new file mode 100644
index 0000000000..6369a1729a
--- /dev/null
+++ b/bundles/org.openhab.io.yamlcomposer/doc/conditionals.md
@@ -0,0 +1,164 @@
+# Conditionals (!if)
+
+The `!if` tag performs logical branching during the **preprocessing phase**.
+
+> **Note:** Conditions are evaluated **once** when the YAML file is loaded.
+> These are not runtime rules.
+> They do not react to live state changes in openHAB.
+
+[[toc]]
+
+## When to Use `!if`
+
+Use the `!if` tag to adapt your configuration based on the **Resolution Context** (variables defined in the file, injected via `!include` or `!insert`, or [environment globals](variables.md#env-to-access-environment-variables)).
+
+- **Conditional Snippets**: choose between alternative configuration blocks or values.
+- **Optional Properties**: conditionally merge in additional settings using [merge keys (<<)](merge-keys.md).
+
+## Basic Syntax
+
+The `!if` tag supports two forms: a **Mapping Form** for simple logic and a **Sequence Form** for multiple branches.
+
+The `if:` and `elseif:` keys are treated as **implicit expressions**.
+You do not need to wrap the expression in `${...}`.
+
+### Mapping Form (Simple)
+
+Use this for simple if/else decisions.
+
+```yaml
+example: !if
+ if: env == 'prod'
+ then: "secure-server-url"
+ else: "localhost"
+```
+
+| Key | Description | Required |
+|:-------|:------------------------------------------------------------------------------------------------------------------------|:---------|
+| `if` | The expression to evaluate. | Yes |
+| `then` | The value to return if **truthy** ([see rules below](#truthiness-rules)). Can be a scalar, map, list, or any valid tag. | Yes |
+| `else` | The value to return if **falsy**. | No |
+
+### Sequence Form (Multiple Branches)
+
+Use this for multiple ordered conditions.
+
+Evaluates conditions in order and stops at the first **truthy** match.
+If no condition matches and no `else` is provided, the tag resolves to `null`.
+
+```yaml
+environment_type: !if
+ - if: hardware_version >= 2
+ then: "high-power-mode"
+ - elseif: battery_powered
+ then: "eco-mode"
+ - else: "standard-mode"
+```
+
+## Expression Evaluation
+
+The `if:` key follows a specific order of operations.
+
+### 1. Bare Expressions (Recommended)
+
+The string is evaluated directly as an expression against the available variables.
+
+```yaml
+if: count > 10 and status == 'ALARM'
+```
+
+::: tip
+The expression can be quoted when it contains characters that YAML would otherwise misinterpret, such as `:` or `#`.
+:::
+
+### 2. Using substitution pattern (Advanced — Double Evaluation)
+
+If you use a substitution pattern inside an `if:` key, the substitution engine runs **first** to resolve `${...}` patterns.
+The resulting string is then evaluated as an expression.
+This is useful for building logic strings from variables.
+
+```yaml
+variables:
+ operator: ">"
+
+test: !if
+ if: 75 ${operator} 50
+ # Step 1: ${operator} resolves to ">"
+ # Step 2: The if expression resolves to "75 > 50"
+ # Step 3: expression evaluates to true
+ then: "High"
+```
+
+## Truthiness Rules
+
+When a value is used in a conditional, it is first evaluated and then interpreted as either **truthy** or **falsy**.
+
+The following values are considered **falsy**:
+
+- `false`
+- `null`
+- `0` or `0.0`
+- empty strings (`""`)
+- empty lists (`[]`)
+- empty maps (`{}`)
+
+All other values are **truthy**.
+This includes any non‑empty string, any non‑zero number, and any non‑empty collection.
+
+### Short-Circuiting (Lazy Evaluation)
+
+Only the active branch is processed.
+Tags such as `!include` inside inactive branches are ignored.
+An `!include` in an inactive branch is never loaded and will not cause errors if the file does not exist.
+
+## Advanced Integration
+
+### Nesting and Composition
+
+The `!if` tag is fully recursive.
+You can nest `!if` tags within the `then` or `else` blocks to create complex decision trees.
+
+```yaml
+status: !if
+ if: device_online
+ then: !if
+ if: battery_level < 20
+ then: "online-low-battery"
+ else: "online-healthy"
+ else: "offline"
+```
+
+### Conditional Merging (Mixins)
+
+Use `!if` with the YAML merge key (`<<`) to conditionally mix in sets of properties.
+
+```yaml
+server_config:
+ port: 8080
+ <<: !if
+ if: is_prod
+ then:
+ ssl_enabled: true
+ strict_security: true
+```
+
+### Using !include and !insert
+
+You can return entire files or templates by using `!include` or `!insert` inside a branch.
+Only the tag in the active branch is processed.
+
+```yaml
+network_settings: !if
+ if: wifi_enabled
+ then: !include wifi-config.inc.yaml
+ else: !insert ethernet-template
+```
+
+## Common Pitfalls
+
+1. **Expression vs String Literal**: `if: production` checks for a variable named `production`.
+ To check for the literal string, quote it: `if: env == 'production'`.
+1. **Omitting `else`**: If no condition matches and there is no `else`, the result is `null`.
+1. **Invalid YAML**: Even inactive branches must be syntactically valid YAML.
+
+See [Expression Syntax](variables.md#expression-syntax) for more details.
diff --git a/bundles/org.openhab.io.yamlcomposer/doc/include.md b/bundles/org.openhab.io.yamlcomposer/doc/include.md
new file mode 100644
index 0000000000..1cc249c5f8
--- /dev/null
+++ b/bundles/org.openhab.io.yamlcomposer/doc/include.md
@@ -0,0 +1,291 @@
+# Including Other Files
+
+`!include` inserts the referenced file or structure exactly at the position where the include appears.
+
+YAML Composer supports including external YAML files to facilitate modular, reusable, and maintainable configurations.
+This is especially useful for modular reuse, creating device [packages](packages.md), or separating concerns across multiple files.
+
+[[toc]]
+
+## Syntax Options
+
+### Short Form
+
+```yaml
+keyname: !include filename.inc.yaml
+```
+
+```yaml
+keyname: !include filename.inc.yaml?arg1=value1&flag
+```
+
+The short form allows you to reference another file directly after the `!include` tag.
+Optional arguments may be appended using URL‑style query syntax:
+
+- `arg1=value1` assigns a specific value
+- `flag` (a value‑less argument) is interpreted as `true`
+- Multiple arguments are separated with `&`
+- The file name, argument names, and argument values are URL‑decoded.
+ If any of them contain characters such as `+`, `%`, or other reserved symbols, make sure to URL‑encode them.
+
+This form is ideal when you only need to include a file and pass a small number of simple parameters.
+
+### Long Form (supports variables)
+
+```yaml
+# Block style (multi-line)
+keyname: !include
+ file: filename.inc.yaml
+ vars:
+ var1: value1
+ var2: value2
+```
+
+```yaml
+# Flow style (single-line)
+keyname: !include { file: filename.inc.yaml, vars: { var1: value1, var2: value2 } }
+```
+
+The `vars:` mapping is layered on top of the file’s current variables to form the evaluation context.
+In the long form, the `vars:` section is optional.
+
+::: tip Passing Existing Variables to Included Files
+
+The `vars:` section of an `!include` directive can contain literal values or references to **existing variables**.
+
+Example:
+
+```yaml
+keyname: !include
+ file: xx.inc.yaml
+ vars:
+ var1: ${mainvar}
+```
+
+The include file can now refer to `var1` without relying on the variable names used in the main file.
+This is especially useful when the same include file is shared across multiple configurations that may use different variable names.
+
+:::
+
+The contents of the include file are inserted as the value for the given key.
+This means that the top-level keys in the include file become sub-keys of the key in the main file.
+
+## Variable Resolution Order
+
+Understanding how variables interact across files is important when using includes, especially when include files define their own defaults.
+
+When include files are involved, variables can originate from multiple sources.
+Their values are resolved according to the following order, from highest priority to lowest priority.
+
+1. Inline `vars` in `!include` directives
+1. Global `variables` defined in the main file
+1. Local `variables` defined inside the include file
+
+Variables provided in the `vars` section of an `!include` directive are visible only within the included file, but they override both global and local variables.
+
+Local variables defined inside an include file can act as default values when they are not provided in the `!include` directive.
+This allows include files to be reused with different parameters while keeping sensible defaults inside the file itself.
+
+**Example:**
+
+`main.yaml`:
+
+```yaml
+variables:
+ broker: mqtt:broker:main
+
+things:
+ mqtt:topic:livingroom-window: !include
+ file: mqtt_contact.inc.yaml
+ vars:
+ label: Living Room Window
+ id: livingroom-window
+
+ mqtt:topic:bedroom-window: !include
+ file: mqtt_contact.inc.yaml
+ vars:
+ label: Bedroom Window
+ id: bedroom-window
+ broker: mqtt:broker:external # override the global broker variable
+```
+
+`mqtt_contact.inc.yaml`:
+
+```yaml
+bridge: ${broker}
+label: ${label}
+config:
+ availabilityTopic: ${id}/availability
+ payloadAvailable: online
+ payloadNotAvailable: offline
+```
+
+Resulting configuration:
+
+```yaml
+things:
+ mqtt:topic:livingroom-window:
+ bridge: mqtt:broker:main
+ label: Living Room Window
+ config:
+ availabilityTopic: livingroom-window/availability
+ payloadAvailable: online
+ payloadNotAvailable: offline
+
+ mqtt:topic:bedroom-window:
+ bridge: mqtt:broker:external
+ label: Bedroom Window
+ config:
+ availabilityTopic: bedroom-window/availability
+ payloadAvailable: online
+ payloadNotAvailable: offline
+```
+
+## File Naming & Reload Behavior
+
+### Include File Naming
+
+Include files should use a dedicated extension, either `.inc.yaml` or `.inc.yml`.
+Files with a normal `.yaml` extension are treated as **main configuration files** and must contain the full [top‑level structure]({{base}}/configuration/yaml/) required by the YAML configuration schema.
+
+In contrast, `.inc.yaml` files are recognized as include fragments and are only processed when referenced through `!include`.
+
+::: tip Note
+Include fragments and main files are intentionally treated as different roles.
+Using the same file as both a compiled main file and an included fragment is not supported.
+:::
+
+### Path Resolution
+
+Include file paths may be written as absolute paths or as paths relative to the current file.
+YAML Composer also supports two shorthand prefixes that simplify referencing files inside the configuration directory.
+
+#### Shorthand Prefixes
+
+| Shorthand | Resolves To | Meaning |
+|---------------------|-------------------------------------|----------------------------------------------------------------------------------------------------------------------------|
+| `@/path` `@path` | `${OPENHAB_CONF}/path` | The openHAB configuration root. Use when referencing files anywhere under `OPENHAB_CONF`. The slash after `@` is optional. |
+| `$/path` `$path` | `${OPENHAB_CONF}/yamlcomposer/path` | The source directory for your extended-syntax YAML files. The slash after `$` is optional. |
+
+##### `@/path` → `${OPENHAB_CONF}/path`
+
+A leading `@` resolves directly to the openHAB configuration root (`OPENHAB_CONF`).
+
+```yaml
+key: !include "@/yaml/includes/device.inc.yaml"
+# Resolves to: ${OPENHAB_CONF}/yaml/includes/device.inc.yaml
+```
+
+**Notes:**
+
+- Paths beginning with `@` **must be quoted** (e.g., `"@/path"`), because YAML plain scalars cannot begin with `@`.
+- The slash after `@` is **optional**: `"@yaml/includes/device.inc.yaml"` works the same.
+- Use `@` when you want to reference files anywhere under the configuration root without writing long absolute paths.
+
+##### `$/path` → `${OPENHAB_CONF}/yamlcomposer/path`
+
+A leading `$` resolves to `OPENHAB_CONF/yamlcomposer`.
+
+**Example directory layout:**
+
+```sh
+OPENHAB_CONF/
+ yamlcomposer/
+ shared.inc.yaml
+ lights/
+ kitchen/
+ main.yaml
+```
+
+If `main.yaml` contains:
+
+```yaml
+key: !include "$/shared.inc.yaml"
+```
+
+Then `$` resolves to:
+
+```sh
+${OPENHAB_CONF}/yamlcomposer
+```
+
+So the final resolved path becomes:
+
+```sh
+${OPENHAB_CONF}/yamlcomposer/shared.inc.yaml
+```
+
+**Notes:**
+
+- The slash after `$` is **optional**: `"$shared.inc.yaml"` works the same.
+- It avoids brittle paths like `../../../shared.inc.yaml`.
+
+#### Relative Paths
+
+If the path does not begin with `/`, `@`, or `$` it is interpreted as a path **relative to the directory of the including file**.
+You may use `.` and `..` to refer to the current and parent directory.
+
+**Example directory layout:**
+
+```sh
+yamlcomposer/
+ parent.inc.yaml
+ main/
+ main.yaml
+ shared.inc.yaml
+ common/
+ defaults.inc.yaml
+```
+
+**Same directory:**
+
+```yaml
+key: !include "shared.inc.yaml"
+# Resolves to: yamlcomposer/main/shared.inc.yaml
+```
+
+**Navigate downward (into a subdirectory):**
+
+```yaml
+key: !include "common/defaults.inc.yaml"
+# Resolves to: yamlcomposer/main/common/defaults.inc.yaml
+```
+
+**Navigate upward:**
+
+```yaml
+key: !include "../parent.inc.yaml"
+# Resolves to: yamlcomposer/parent.inc.yaml
+```
+
+Relative paths always resolve from the directory containing the including file.
+
+#### Using Variable Substitutions
+
+You can use variable substitution patterns in the file name to construct paths dynamically.
+This includes normal variables, [predefined variables](variables.md#predefined-variables), and environment variables exposed through `ENV`.
+
+**Example:**
+
+```yaml
+keyname: !include
+ file: ${OPENHAB_CONF}/packages/hue-light.pkg.yaml
+```
+
+### File Organization
+
+It may be helpful to store include files in a dedicated subdirectory.
+These files can be referenced using relative paths, the `@` or `$` shorthands, or full absolute paths.
+Choose whichever style best matches your preference.
+
+### Nested Includes
+
+Include files may themselves contain `!include` directives.
+This allows configurations to be composed from multiple layers:
+
+main.yaml → a.inc.yaml → b.inc.yaml → …
+
+### Reload Behavior
+
+When an include file changes, YAML Composer reloads the main files that reference it rather than attempting to load the include file directly.
+Reloads occur only if the include file is located within `${OPENHAB_CONF}/yamlcomposer/`.
diff --git a/bundles/org.openhab.io.yamlcomposer/doc/merge-keys.md b/bundles/org.openhab.io.yamlcomposer/doc/merge-keys.md
new file mode 100644
index 0000000000..41dc6959b6
--- /dev/null
+++ b/bundles/org.openhab.io.yamlcomposer/doc/merge-keys.md
@@ -0,0 +1,205 @@
+# Merge Keys
+
+Merge keys (`<<`) let you combine mappings defined directly in a mapping with other mappings defined elsewhere, such as:
+
+- [Anchors](anchors.md)
+- [!include](include.md) (External files)
+- [!insert](templates.md) (In-file Templates)
+- [!if](conditionals.md) (Conditionals)
+- [Variables](variables.md)
+
+They promote reusability and avoid repetition by letting you define common mappings once and update them in a single place.
+
+[[toc]]
+
+## What Merge Keys Do
+
+A merge key takes one or more source mappings and merges their key–value pairs into the current mapping.
+The source mappings may be written inline, referenced through an anchor, or loaded from another file.
+The merged values behave exactly as if they were written inline.
+Local fields may add to or override the merged content.
+
+## Merge Rules
+
+- Only mappings can be merged.
+- Merge keys may appear multiple times or as a list.
+- Merge order matters because keys from earlier mappings override keys from later ones.
+- Local keys always override merged keys, even when defined after a merge key.
+- Merges are shallow, and nested mappings are replaced rather than combined.
+
+### Example: Multiple Merges and Precedence
+
+```yaml
+.defaults: &DEFAULTS
+ icon: default
+ autoupdate: false
+
+.metadata: &LIGHTS
+ tags: [Light]
+ icon: light
+
+items:
+ Lamp:
+ <<: [*LIGHTS, *DEFAULTS]
+ label: Desk Lamp
+```
+
+Result:
+
+```yaml
+items:
+ Lamp:
+ label: Desk Lamp
+ icon: light # from LIGHTS (earlier mapping wins)
+ autoupdate: false
+ tags: [Light]
+```
+
+## Limitations
+
+- Merge keys only merge mappings, not lists.
+- Merge keys cannot modify scalar values.
+- Merge keys cannot be used inside sequences unless the sequence elements are mappings.
+
+## Integrating with Other Features
+
+### Anchors
+
+[Anchors](anchors.md) define reusable structures whose content can be inserted into the current mapping via an alias.
+Merge keys then combine that anchored content with any local fields in the mapping.
+Anchors are a standard YAML feature for sharing data within a single file.
+Variables or templates are often preferred for more complex needs.
+
+```yaml
+items:
+ Light1: &LIGHT_BASE
+ type: Switch
+ icon: light
+ autoupdate: false
+ label: Light One
+
+ Light2:
+ <<: *LIGHT_BASE
+ label: Light Two
+```
+
+See also: [Using Hidden Keys for Anchors](anchors.md#using-hidden-keys-for-anchors).
+
+### `!include`
+
+The [!include](include.md) tag loads the contents of another file so it can be inserted into the current mapping.
+Merge keys then combine the included content with any local fields in the mapping.
+
+**Benefits over anchors:**
+
+- Included files can be parameterized, allowing the same structure to be reused with different values.
+- Included files can be shared across many different YAML files, whereas anchors are limited to a single file.
+
+```yaml
+# light-common.inc.yaml
+power:
+ type: switch
+ stateTopic: ${id}/power
+availability:
+ type: contact
+ stateTopic: ${id}/availability
+```
+
+```yaml
+# light-color.inc.yaml
+color:
+ type: color
+ stateTopic: ${id}/color
+```
+
+```yaml
+# main.yaml
+things:
+ mqtt:topic:living-room-light:
+ channels:
+ <<: !include { file: light-common.inc.yaml, vars: { id: living-room-light } }
+ <<: !include { file: light-color.inc.yaml, vars: { id: living-room-light } }
+```
+
+### Templates (`!insert`)
+
+The [!insert](templates.md) tag inserts the contents of an in-file template into the current mapping.
+Merge keys then combine the template’s fields with any local fields in that mapping.
+
+Templates can be parameterized when used in a merge key.
+
+```yaml
+templates:
+ common_channels:
+ power:
+ type: switch
+ stateTopic: ${id}/power
+ availability:
+ type: contact
+ stateTopic: ${id}/availability
+
+ color_channel:
+ color:
+ type: color
+ stateTopic: ${id}/color
+
+things:
+ mqtt:topic:living-room-light:
+ channels:
+ <<: !insert { template: common_channels, vars: { id: living-room-light } }
+ <<: !insert { template: color_channel, vars: { id: living-room-light } }
+```
+
+### Conditionals (`!if`)
+
+The [!if](conditionals.md) tag allows you to choose between different mappings or return `null` to skip the merge entirely.
+The engine resolves tags before the merge, so the merge key receives the final mapping.
+
+```yaml
+# Merge extra settings only for production
+server:
+ port: 8080
+ <<: !if
+ if: is_prod
+ then: { ssl: true, cache: true }
+```
+
+### Substitution
+
+Merge keys can combine content produced dynamically from an expression.
+This enables inline conditional merging using Python-style expressions.
+
+```yaml
+variables:
+ has_color: true
+ color_channel:
+ color:
+ type: color
+
+things:
+ mqtt:topic:light:
+ channels:
+ power:
+ type: switch
+ # Returns color_channel map if true
+ <<: ${color_channel if has_color}
+```
+
+::: tip Hints
+
+- Merge keys only merge mappings, so the expression must resolve to a map.
+- Avoid compound patterns such as `${foo}${bar}` or `x${foo}` because they are interpreted as literal strings and cannot be merged.
+- Merging `null` or an empty map is a no-op, effectively omitting the merge key.
+
+:::
+
+## Related Topics
+
+For details on how variables are resolved and how substitution interacts with YAML structures, see [Variables & Substitution — Return Types & Coercion](variables.md#return-types--coercion).
+
+## Best Practices
+
+- Use merge keys to define reusable base templates.
+- Keep anchors inside hidden keys to avoid clutter.
+- Use includes or packages to centralize shared structures.
+- Prefer simple, predictable structures for reusable blocks.
diff --git a/bundles/org.openhab.io.yamlcomposer/doc/packages.md b/bundles/org.openhab.io.yamlcomposer/doc/packages.md
new file mode 100644
index 0000000000..307ae00989
--- /dev/null
+++ b/bundles/org.openhab.io.yamlcomposer/doc/packages.md
@@ -0,0 +1,450 @@
+# Packages
+
+Packages provide a way to bundle multiple related YAML sections into a reusable, parameterized unit.
+Unlike fragment-level insertion, a package expands into full top‑level sections such as `things:` or `items:`.
+These sections may come from an external file or a same‑file template.
+They are then merged into the current configuration.
+
+[[toc]]
+
+## Purposes
+
+- **Logical Grouping:**
+ Packages allow a **Thing** and its related **Items**, channels, and metadata to be defined together in one file, representing a complete, self‑contained device definition.
+
+- **Reuse with Different Parameters:**
+ Through variable substitution, a single package can be instantiated multiple times with different values.
+ This makes it easy to define many similar devices such as sensors, lights, or switches from one shared template.
+
+## Package Syntax and Structure
+
+Packages are declared in the main YAML file under the top‑level `packages:` section.
+Each entry defines a package ID and the source from which the package content is obtained.
+
+```yaml
+packages:
+ :
+ :
+ ...
+```
+
+### Key Components
+
+- **Package ID:**
+ A package ID is a unique identifier for the package.
+ It may include spaces.
+ The special variable `${package_id}` resolves to this key inside the package content.
+
+- **Package Source:**
+ A package can be created from either of the following sources:
+
+ - **`!include` (external file):**
+ Loads a separate YAML file and applies the package’s variable context to it.
+ See the [!include syntax options](include.md#syntax-options).
+
+ - **`!insert` (same‑file template):**
+ Expands a template defined under the main file’s `templates:` section.
+ See the [!insert syntax options](templates.md#syntax-options).
+
+ Both forms support parameterization through `vars:` and participate fully in package merging.
+
+## Package Source Contents
+
+- **Top‑Level Sections:**
+ Package sources contain any combination of top‑level keys such as `things:` and `items:`.
+
+- **Uniqueness:**
+ Because package sources can be referenced multiple times, use variable substitutions such as `${package_id}` and unique `vars:` variables for entity UIDs in each invocation to avoid collisions.
+
+- **Deep Nesting:**
+ A package source may itself include other packages.
+ When a package references another package via `packages:` inside an included file, it creates an inheritance chain.
+ Properties merge downward sequentially from the deepest core file out to the final main file.
+
+## Package Example
+
+`main.yaml`:
+
+```yaml
+variables:
+ broker: mqtt:broker:main
+
+packages:
+ livingroom-light: !include
+ file: package/mqtt-light.inc.yaml
+ vars:
+ name: Living_Room_Light
+ label: Living Room Light
+
+ bedroom-light: !include
+ file: package/mqtt-light.inc.yaml
+ vars:
+ name: Bed_Room_Light
+ label: Bed Room Light
+```
+
+`package/mqtt-light.inc.yaml`:
+
+```yaml
+things:
+ mqtt:topic:${package_id}:
+ bridge: ${broker}
+ channels:
+ power:
+ type: switch
+ config:
+ stateTopic: ${package_id}/state
+ commandTopic: ${package_id}/set/state
+ # ... other channels (brightness, color)
+
+items:
+ ${name}_Power:
+ type: Switch
+ label: ${label} Power
+ channel: mqtt:topic:${package_id}:power
+ # ... more items for the light, e.g. brightness, color, etc.
+```
+
+Resulting YAML structure:
+
+```yaml
+things:
+ mqtt:topic:livingroom-light:
+ bridge: mqtt:broker:main
+ channels:
+ power:
+ type: switch
+ config:
+ stateTopic: livingroom-light/state
+ commandTopic: livingroom-light/set/state
+ mqtt:topic:bedroom-light:
+ bridge: mqtt:broker:main
+ channels:
+ power:
+ type: switch
+ config:
+ stateTopic: bedroom-light/state
+ commandTopic: bedroom-light/set/state
+
+items:
+ Living_Room_Light_Power:
+ type: Switch
+ label: Living Room Light Power
+ channel: mqtt:topic:livingroom-light:power
+ Bed_Room_Light_Power:
+ type: Switch
+ label: Bed Room Light Power
+ channel: mqtt:topic:bedroom-light:power
+```
+
+## Merge Behavior
+
+### Final Top-Level Sections
+
+A final top‑level section is the fully expanded `things:`, `items:`, or other top‑level section of the configuration that openHAB receives after all packages, templates, includes, and merges have been applied.
+Entries defined directly under these sections can merge with package‑generated entries when their identifiers match.
+Entries remain independent when their identifiers do not match.
+
+::: tip Note
+The following example uses `!insert`, but the same merge rules apply to packages sourced from `!include`.
+:::
+
+When a package is expanded, its contents are merged into the main YAML structure.
+You may customize the resulting structure by overriding, adding, or removing elements defined in the package.
+This is done by redefining the elements you want to customize in the main file.
+These redefinitions appear in the final top‑level section.
+
+### Default Merge Behavior
+
+Source YAML File:
+
+```yaml
+templates:
+ number_item:
+ items:
+ ${package_id}_Item:
+ type: Number
+ label: Package Label
+ tags: [Measurement]
+ metadata:
+ stateDescription:
+ config:
+ min: 1
+ pattern: '%.3f'
+ widget:
+ value: oh-card
+
+packages:
+ Number: !insert number_item
+
+# This is the final top‑level `items:` section of the configuration
+# The packages will merge into this section
+items:
+ Number_Item:
+ label: Power Draw
+ dimension: Power
+ tags: [Power]
+ metadata:
+ stateDescription:
+ config:
+ max: 10
+```
+
+Result:
+
+```yaml
+items:
+ Number_Item:
+ type: Number
+ label: Power Draw
+ dimension: Power
+ tags:
+ - Measurement
+ - Power
+ metadata:
+ stateDescription:
+ config:
+ min: 1
+ max: 10
+ pattern: '%.3f'
+ widget:
+ value: oh-card
+```
+
+The way keys interact depends on their data type.
+
+| Data Type | Behavior | Description |
+|-----------|-----------|----------------------------------------------------------------------------------------------------------|
+| Scalar | Overwrite | A scalar in the final top‑level section replaces the scalar defined at the same path inside the package. |
+| Map | Merge | Maps are merged key by key, recursively. |
+| List | Merge | Lists are concatenated (package values first) and de-duplicated. |
+
+### Automatic Removal of Empty Values
+
+During merging, empty structures are automatically stripped from the final configuration.
+Empty maps (`{}`) and lists (`[]`) as well as map keys whose value is `null` or an empty string are removed.
+This keeps the resulting configuration clean and allows packages to define catch‑all defaults.
+
+**Example:**
+
+```yaml
+variables:
+ icon: null # default to avoid unknown‑variable warnings
+
+icon: ${icon}
+```
+
+Because `icon` evaluates to `null`, the entire `icon:` key is removed from the merged output unless the including file overrides it.
+
+### How Package Merging Differs from YAML Merge Keys
+
+Mappings from packages are merged recursively with the corresponding mappings in the final top‑level section.
+This differs from standard YAML merge keys, which perform only shallow merges.
+
+**Merge Key (shallow merge):**
+
+```yaml
+# merge key:
+targetkey:
+ foo:
+ bar:
+ boo: baz
+ <<: # merge `foo` into `targetkey`
+ foo:
+ bar:
+ boo: waldo
+ goo: fy
+ qux: quux
+```
+
+```yaml
+# result — the merge key's foo mapping
+# is ignored because foo already exists in main
+targetkey:
+ foo:
+ bar:
+ boo: baz
+```
+
+**Package Merging (recursive merge):**
+
+```yaml
+# main file
+targetkey:
+ foo:
+ bar:
+ boo: baz
+
+packages:
+ anyid: !include packagefile.inc.yaml
+```
+
+```yaml
+# packagefile.inc.yaml
+targetkey:
+ foo:
+ bar:
+ boo: waldo
+ goo: fy
+ qux: quux
+```
+
+```yaml
+# result:
+targetkey:
+ foo:
+ bar:
+ boo: baz # main file overrides matching keys
+ goo: fy # but includes additional keys...
+ qux: quux # from the package
+```
+
+recursive merging allows customization at any depth in the mapping.
+
+### Controlling Package Merge Behavior with Tags
+
+Use these special YAML tags to explicitly override the default recursive merge behavior.
+These directives can be declared at any point in the configuration pipeline—either within the **main YAML file** or deeply embedded inside an intermediate **include file** acting as a middle-tier package.
+
+#### 1. The `!replace` Tag
+
+The `!replace` tag forces an absolute replacement for maps or lists that would otherwise merge.
+This acts as a destructive splice, completely discarding any existing inherited data structure beneath that key from upstream sources and starting fresh with the new layout provided.
+
+#### 2. The `!remove` Tag
+
+The `!remove` tag cleanly deletes the corresponding key and its entire sub-tree from the configuration hierarchy.
+It is ideal for pruning specific components, metadata keys, or configurations exposed by a baseline package that do not apply to the current downstream context.
+
+---
+
+### Example: Nested Package Overrides
+
+When packages include other packages, a middle-tier file can surgically modify or strip elements declared by the core baseline configuration before it ever reaches the main composition file.
+
+#### Core Configuration (`nested-items.inc.yaml`)
+
+Provides a core item layout with deep metadata fields:
+
+```yaml
+items:
+ target_item:
+ label: base_label
+ tags:
+ - from_nested
+ metadata:
+ stateDescription:
+ value: from_nested
+ category:
+ value: from_nested
+ untouched_item:
+ label: untouched_from_nested
+```
+
+#### Middle-Tier Override Package (`pkg-with-overrides.inc.yaml`)
+
+This file consumes the core configuration via `!include`, but applies `!replace` and `!remove` tags to selectively adjust properties:
+
+```yaml
+packages:
+ nested: !include nested-items.inc.yaml
+items:
+ target_item:
+ tags: !replace # 1. Overwrite the list, discarding 'from_nested'
+ - from_outer
+ metadata:
+ stateDescription: !remove # 2. Entirely purge this sub-tree key
+ category: !replace # 3. Swap out the map structure with a fresh one
+ value: from_outer
+ config:
+ origin: nested_package_override
+```
+
+#### Root Configuration (`main.yaml`)
+
+Loads the middle-tier package:
+
+```yaml
+packages:
+ p1: !include pkg-with-overrides.inc.yaml
+```
+
+#### Final Merged Output
+
+When evaluated by the preprocessor, the downstream overrides successfully neutralize the deep properties inherited from the original package file.
+The `stateDescription` block is erased, lists and maps are replaced cleanly, and unrelated siblings are passed through untouched:
+
+```yaml
+items:
+ target_item:
+ label: base_label
+ tags:
+ - from_outer
+ metadata:
+ category:
+ value: from_outer
+ config:
+ origin: nested_package_override
+ untouched_item:
+ label: untouched_from_nested
+```
+
+::: tip Usage Notes
+
+- `!replace` and `!remove` are evaluated locally within each package layer before that package's content is returned and merged into the caller.
+ This allows intermediate include files to cleanly intercept, prune, or completely reset configurations inherited from deeper core packages.
+- They can be declared in the root configuration file or anywhere down an inheritance chain of nested package includes.
+- `!remove` target references apply to entire map keys; individual array elements within a list cannot be targeted for independent removal.
+- Use `!replace` on an array to drop inherited elements and start an array list with entirely new contents.
+
+:::
+
+## Strategic Use of Package IDs
+
+Choose a **Package ID** that can also serve as a Thing UID fragment, Item name, or similar identifier.
+This avoids defining extra variables in your package source and lets you derive related identifiers directly from `${package_id}`.
+
+You can override `${package_id}` in the `vars:` block if needed.
+
+**Example:**
+
+```yaml
+# main file
+packages:
+ Living_Room_Light: !include light.inc.yaml
+ Kitchen_Light: !include light.inc.yaml
+```
+
+```yaml
+# light.inc.yaml package source
+variables:
+ id: ${package_id|lower|replace('_', '-')}
+ thing_uid: "mqtt:topic:${id}"
+ item_name: ${package_id}
+ label: ${package_id|replace('_', ' ')}
+```
+
+**Resulting variables:**
+
+| Variable | Living_Room_Light | Kitchen_Light |
+|-----------------|--------------------------------|----------------------------|
+| `${package_id}` | `Living_Room_Light` | `Kitchen_Light` |
+| `${id}` | `living-room-light` | `kitchen-light` |
+| `${thing_uid}` | `mqtt:topic:living-room-light` | `mqtt:topic:kitchen-light` |
+| `${item_name}` | `Living_Room_Light` | `Kitchen_Light` |
+| `${label}` | `Living Room Light` | `Kitchen Light` |
+
+## Limitation: Top-Level Merge Keys in `packages:`
+
+Top-level YAML merge keys (for example `<<:`) inside the `packages:` map are not supported.
+Each package must be declared explicitly as a direct key under `packages:`.
+
+The following pattern is **not supported**:
+
+```yaml
+packages:
+ <<: !include common-packages.yaml
+```
+
+In this form, the merge key tries to inject package declarations into `packages:` itself.
+The composer does not expand package declarations through top-level merge keys.
diff --git a/bundles/org.openhab.io.yamlcomposer/doc/templates.md b/bundles/org.openhab.io.yamlcomposer/doc/templates.md
new file mode 100644
index 0000000000..458effa052
--- /dev/null
+++ b/bundles/org.openhab.io.yamlcomposer/doc/templates.md
@@ -0,0 +1,188 @@
+# Templates
+
+Templates let you define reusable blocks of YAML directly within the same file.
+They are stored under the top‑level `templates:` section and can be expanded using the `!insert` tag.
+
+Templates behave similarly to `!include` in how they are evaluated — including variable scoping and per‑invocation isolation — but they are defined locally rather than in external files.
+
+[[toc]]
+
+## What Templates Are
+
+A template is a named YAML node stored under the top‑level `templates:` section.
+A template may contain **any YAML value**:
+
+- a mapping
+- a sequence
+- a scalar
+
+The keys under `templates:` (`simple_greeting`, `greeting`, `channel_block`, `color_list`) are **template names**.
+They identify each template and are not part of the template’s content.
+
+Example:
+
+```yaml
+templates:
+ simple_greeting: Hello!
+ greeting: Hello, ${name}!
+ channel_block:
+ power:
+ type: switch
+ stateTopic: ${id}/power
+ color_list:
+ - red
+ - green
+ - blue
+```
+
+::: tip Important!
+
+Templates are not evaluated when defined.
+They are evaluated **each time** they are expanded with `!insert`.
+Therefore, a template may contain references to variables that do not yet exist; those variables are supplied later during the `!insert` invocation.
+
+:::
+
+## Syntax Options
+
+`!insert` expands a template at the point where it appears.
+It supports two forms:
+
+### Short Form
+
+```yaml
+message: !insert simple_greeting
+```
+
+```yaml
+message: !insert simple_greeting?arg1=value1&flag
+```
+
+The short form allows you to reference a template directly after the `!insert` tag.
+Optional arguments may be appended using URL‑style query syntax:
+
+- `arg1=value1` assigns a specific value
+- `flag` (a value‑less argument) is interpreted as `true`
+- Multiple arguments are separated with `&`
+- The template name, argument names, and argument values are URL‑decoded.
+ If any of them contain characters such as `+`, `%`, or other reserved symbols, make sure to URL‑encode them.
+
+Use the no‑argument form when you simply want to insert a template as‑is.
+Use the query‑parameter form when you want to pass a small number of simple parameters.
+
+### Long Form (supports variables)
+
+```yaml
+# Block style (multi-line)
+message: !insert
+ template: greeting
+ vars:
+ name: "Jimmy"
+
+# Or flow style (single-line)
+message: !insert { template: greeting, vars: { name: "Jimmy" } }
+```
+
+The `vars:` mapping is layered on top of the file’s current variables to form the evaluation context.
+In the long form, the `vars:` section is optional.
+
+## How Template Evaluation Works
+
+Each `!insert` call is evaluated in its **own isolated context**:
+
+- It begins with all variables defined in the current file.
+- If this file was loaded via `!include`, it also inherits variables from parent files.
+- Variables provided through `vars:` override or extend this context.
+- The template body is evaluated using this combined set of variables.
+- The resulting value — scalar, list, or mapping — replaces the `!insert` node.
+
+### Example: Scalar Template
+
+```yaml
+templates:
+ greeting: Hello, ${name}!
+
+message: !insert { template: greeting, vars: { name: "Alice" } }
+```
+
+Result:
+
+```yaml
+message: Hello, Alice!
+```
+
+### Example: Template Producing a Mapping
+
+```yaml
+templates:
+ channel_block:
+ power:
+ type: switch
+ stateTopic: ${id}/power
+
+thing:
+ channels: !insert { template: channel_block, vars: { id: living-room-light } }
+```
+
+### Example: Template as a List Item
+
+```yaml
+templates:
+ color_entry: ${color}
+
+colors:
+ - !insert { template: color_entry, vars: { color: red } }
+ - !insert { template: color_entry, vars: { color: green } }
+ - !insert { template: color_entry, vars: { color: blue } }
+```
+
+### Example: Template Producing a Sequence
+
+```yaml
+templates:
+ rgb:
+ - red
+ - green
+ - blue
+
+palette: !insert rgb
+```
+
+## Similarities with `!include`
+
+Templates and includes share the same evaluation model:
+
+- Each invocation is evaluated in its own isolated context.
+- The context includes:
+ - variables defined in the current file
+ - variables inherited from parent files (if this file was loaded via `!include`)
+ - variables provided through `vars:` for this specific invocation
+- Variable resolution inside the template uses this combined context.
+
+This section is intentionally mechanical — it explains _how_ templates behave, not _when_ to use them.
+
+## Template Scope and Variable Resolution
+
+Inside a template:
+
+- Interpolation happens only when the template is inserted.
+- `${var}` resolves using the combined variable context described above.
+- Each `!insert` call is isolated — variables from one invocation do not leak into another.
+
+## Limitations
+
+- Templates must be defined under the top‑level `templates:` section.
+- Templates cannot be shared across files (use `!include` for that).
+- Template names must be unique within the file.
+- A template may produce any YAML value, but the resulting value must be valid for the location where it is inserted (scalar, list item, or mapping field).
+
+## Best Practices
+
+- Keep template names descriptive and consistent.
+- Prefer the block style when passing multiple variables for readability.
+- Use the composed output in `OPENHAB_CONF/yaml/composed/` to verify template expansion.
+
+---
+
+In‑file templates provide a flexible, parameterizable way to structure reusable YAML within a single file.
+They complement `!include` and anchors, offering a clean mechanism for building consistent configurations.
diff --git a/bundles/org.openhab.io.yamlcomposer/doc/variables.md b/bundles/org.openhab.io.yamlcomposer/doc/variables.md
new file mode 100644
index 0000000000..19a76e6723
--- /dev/null
+++ b/bundles/org.openhab.io.yamlcomposer/doc/variables.md
@@ -0,0 +1,281 @@
+# Variables & Substitution
+
+Variables allow you to define reusable values and substitute them throughout your YAML configuration.
+They provide the flexibility needed for complex templates and reduce hard-coded values.
+These variables work consistently across both the current file and any included packages.
+
+[[toc]]
+
+## Variable Definition
+
+Variables are defined within a top-level `variables:` section.
+It is recommended to place this section at the beginning of the file for better organization.
+
+**Example:**
+
+```yaml
+variables:
+ # Scalar variables
+ expire: 5m
+ label: Living Room
+
+ # Map variable
+ mqtt:
+ broker: mqtt:broker:mybroker
+
+ # List variable
+ rooms:
+ - Kitchen
+ - Bedroom
+```
+
+## Variable Substitution
+
+### Default Substitution Behavior
+
+Substitution enables dynamic value construction instead of static hard-coding.
+Whenever a scalar contains a `${...}` pattern, the preprocessor evaluates the expression and replaces the pattern with the result.
+This makes it easy to construct labels, identifiers, and paths from defined variables.
+Substitution also applies to YAML keys to allow identifiers to be built dynamically.
+
+**Example:**
+
+```yaml
+variables:
+ room: Kitchen
+ light_id: Kitchen_Light
+
+items:
+ ${light_id}:
+ label: ${room} Light
+```
+
+**Resulting Output:**
+
+```yaml
+items:
+ Kitchen_Light:
+ label: Kitchen Light
+```
+
+### Return Types & Coercion
+
+The output type depends on how the substitution is structured within the YAML scalar.
+
+#### Type Preservation (Single Expression)
+
+If a YAML value consists **entirely** of a single substitution pattern, the resulting object preserves the original Java type returned by the expression.
+This allows you to inject complex structures like booleans, lists, or maps directly.
+
+```yaml
+is_active: ${status == 'ON'} # Becomes a real Boolean
+target_rooms: ${rooms} # Becomes a real List
+connection: "${mqtt_config_map}" # Becomes a real Map
+```
+
+#### String Coercion (Mixed Content)
+
+If the substitution pattern is combined with any other text, or if multiple patterns are used together, the entire value is coerced into a **String**.
+
+```yaml
+description: "Status is ${status}" # String: "Status is ON"
+concatenated: "${10}${20}" # String: "1020"
+room_name: "${room} " # String: "Kitchen " (includes space)
+```
+
+| Syntax Pattern | Resulting Type | Example Output |
+|:---------------|:---------------|:------------------------|
+| `${expr}` | **Preserved** | `[Item1, Item2]` (List) |
+| `"${expr}"` | **Preserved** | `true` (Boolean) |
+| `Text ${expr}` | **String** | `"Count: 5"` (String) |
+| `${ex1}${ex2}` | **String** | `"1020"` (String) |
+
+### The `!literal` Tag and `!sub` Escape Hatch
+
+The `!literal` tag disables substitution recursively for a specific YAML node.
+This is useful when you need to preserve the `${...}` syntax as literal text.
+Inside a `!literal` section, you can use the `!sub` tag to re-enable substitution for a specific child node.
+When these tags overlap, the innermost tag always controls the final behavior.
+
+**Example:**
+
+```yaml
+top: !literal
+ foo: ${LITERAL}
+ bar:
+ baz: ${LITERAL}
+ quux: !sub ${substituted}
+ grault: ${LITERAL}
+```
+
+## Expression Syntax
+
+The Expression syntax is based on the [Jinja expression](https://jinja.palletsprojects.com/en/stable/templates/#expressions) language.
+Only expressions inside `${...}` are supported; template blocks like `{% if %}` or `{% for %}` are not available.
+
+### Variable References
+
+1. `label`: Refers to a scalar variable.
+1. `rooms[0]`: Refers to the first element of a list.
+1. `mqtt.broker` or `mqtt['broker']`: Refers to a map subkey.
+1. `mqtt[key]`: Resolves the key dynamically using the value of the `key` variable.
+
+### Operations & Concatenation
+
+An expression can include string, arithmetic, and boolean operations.
+
+1. **String Concatenation**: Use the `~` operator (e.g., `"Room " ~ index`).
+1. **Coercion**: The `~` operator is the preferred way to join values because it automatically converts non-string operands into strings.
+1. **Automatic Joining**: Adjacent literal text and substitution patterns are automatically joined without operators (e.g., `value: "Hello ${username}"`).
+
+> **Note:** Referencing an undefined variable resolves to `null`, logs a warning, and results in an empty string if used in a string context.
+
+### List Concatenation with `+`
+
+Jinja’s `+` operator supports list concatenation.
+If one side is a list and the other is a scalar, the scalar is automatically wrapped into a single-element list.
+
+**Example:**
+
+```yaml
+variables:
+ groups: [Group1, Group2]
+ location: SemanticLocationGroup
+
+effective_groups: ${ groups + location }
+# Result: [Group1, Group2, SemanticLocationGroup]
+```
+
+### Built-in Filters
+
+Filters are applied using the `variable|filter` syntax and can be chained.
+
+#### Text Transformation
+
+| Filter | Description |
+|:-------------|:------------------------------------------------------------|
+| `capitalize` | Capitalize a value. |
+| `title` | Return a titlecased version. |
+| `lower` | Convert a value to lowercase. |
+| `upper` | Convert a value to uppercase. |
+| `replace` | Replace a substring. |
+| `trim` | Strip leading and trailing characters (default whitespace). |
+
+#### Formatting
+
+| Filter | Description |
+|:---------|:----------------------------------------------|
+| `format` | Apply values to a printf-style format string. |
+| `round` | Round a number to an optional precision. |
+| `int` | Convert a value into an integer. |
+
+#### Collection Helpers
+
+| Filter | Description |
+|:---------|:---------------------------------------|
+| `first` | Return the first item of a list. |
+| `length` | Return the length of a list or string. |
+
+#### Fallbacks
+
+| Filter | Description |
+|:----------|:--------------------------------------------------------------|
+| `default` | Return a default value if the variable is empty or undefined. |
+
+**Default Example:**
+
+```yaml
+label: ${room_label | default('Kitchen')}
+```
+
+### Custom Filters
+
+| Filter | Description |
+|:--------|:------------------------------------------------------------------------------------|
+| `label` | Converts identifiers (camelCase, snake_case) into human-friendly titles. |
+| `dig` | Safely navigates deep maps; returns `null` instead of an error if a key is missing. |
+
+**`dig` Example:**
+
+```yaml
+# Dot notation and mixed access supported
+user: ${ infrastructure | dig('config.login.user') }
+host: ${ VARS | dig('config', 'servers', 1, 'host') | default('localhost') }
+```
+
+## Advanced Usage
+
+### Predefined Variables
+
+The Composer injects environmental and file-system context automatically.
+These variables can be interpolated just like regular ones and are helpful when constructing paths for directives.
+
+| Variable | Description |
+|:-------------------|:------------------------------------------------------------------------|
+| `OPENHAB_CONF` | Absolute path to openHAB's main configuration directory. |
+| `OPENHAB_USERDATA` | Absolute path to openHAB's userdata directory. |
+| `__FILE__` | Absolute path to the current file. |
+| `__FILE_NAME__` | Filename portion without the extension or leading path. |
+| `__FILE_EXT__` | File extension portion of the current file name. |
+| `__DIRECTORY__` | Directory portion of the current file. |
+| `__DIR__` | Alias for `__DIRECTORY__`. |
+| `package_id` | Automatically resolved to the Package ID within included package files. |
+
+### Handling Reserved Keywords
+
+If a variable name is a Jinja keyword (like `and`, `or`, `if`), access it via the `VARS` dictionary.
+This also works for variable names containing characters like hyphens that are invalid in direct references.
+
+```yaml
+foo: ${VARS['and']}
+```
+
+### ENV to Access Environment Variables
+
+A special variable `ENV` exposes a map of system environment variables.
+This is especially useful for configurations running within Docker containers.
+
+```yaml
+mode: ${ENV.OPENHAB_MODE} # Resolves to the environment value
+```
+
+### Calling Java Methods
+
+Variables retain their Java types, allowing you to call standard methods directly.
+
+Common types you may encounter include:
+
+- [String](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/String.html)
+- [Integer](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Integer.html)
+- [Double](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Double.html)
+- [Boolean](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Boolean.html)
+- [Map](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/Map.html)
+- [List](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/List.html)
+
+This is useful for logic beyond built-in filters, such as complex regex replacements.
+
+```yaml
+# Use Java String methods for complex logic
+is_sensor: ${ device_id.startsWith("sensor_") }
+clean_id: ${ device_id.replaceAll("ABC", "XYZ") }
+```
+
+### Custom Pattern Delimiters
+
+If your content conflicts with `${...}`, you can define custom delimiters using a named pattern.
+This allows the same pattern to be used consistently across all included files.
+
+```yaml
+variables:
+ jinja: "{{..}}"
+
+foo: !sub:jinja "Hello {{ username }}!"
+```
+
+## Common Pitfalls
+
+1. **Unquoted Operators**: Expressions with operators should be quoted so YAML doesn't misinterpret characters.
+1. **Reserved Names**: Avoid naming variables using keywords like `true`, `false`, `null`, `in`, or `if`.
+1. **`+` vs `~`**: Use `~` for strings to avoid type mismatch errors and use `+` for numbers or lists.
+1. **Jinja Blocks**: Template blocks such as `{% for %}` are not supported; use inline `if` expressions instead.
+1. **Whitespace Sensitivity**: Spaces outside of quotes inside `${ ... }` are ignored, but spaces in quoted strings are preserved.
diff --git a/bundles/org.openhab.io.yamlcomposer/pom.xml b/bundles/org.openhab.io.yamlcomposer/pom.xml
new file mode 100644
index 0000000000..e47b84c528
--- /dev/null
+++ b/bundles/org.openhab.io.yamlcomposer/pom.xml
@@ -0,0 +1,100 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.addons.bundles
+ org.openhab.addons.reactor.bundles
+ 5.2.0-SNAPSHOT
+
+
+ org.openhab.io.yamlcomposer
+
+ openHAB Add-ons :: Bundles :: IO :: YAML Composer
+
+
+ com.sun.jdi;resolution:=optional,com.sun.jdi.connect;resolution:=optional,com.sun.jdi.event;resolution:=optional,com.sun.jdi.request;resolution:=optional,com.sun.tools.attach;resolution:=optional,jdk.internal.module;resolution:=optional,sun.misc;resolution:=optional,*
+
+
+
+
+ org.snakeyaml
+ snakeyaml-engine
+ 3.0.1
+
+
+ org.openhab.osgiify
+ com.hubspot.jinjava.jinjava
+ 2.7.4
+ compile
+
+
+ com.google.re2j
+ re2j
+ 1.2
+ compile
+
+
+ com.google.guava
+ guava
+ 33.5.0-jre
+ test
+
+
+ org.openhab.osgiify
+ com.hubspot.immutables.immutables-exceptions
+ 1.9
+ compile
+
+
+ org.apache.commons
+ commons-lang3
+ 3.20.0
+ compile
+
+
+ org.jsoup
+ jsoup
+ 1.15.3
+ compile
+
+
+ commons-net
+ commons-net
+ ${commons.net.version}
+ compile
+
+
+ ch.obermuhlner
+ big-math
+ 2.3.2
+ compile
+
+
+ com.googlecode.java-ipv6
+ java-ipv6
+ 0.17
+ compile
+
+
+ com.google.code.findbugs
+ jsr305
+ 3.0.2
+ compile
+
+
+ org.javassist
+ javassist
+ 3.30.2-GA
+ compile
+
+
+ com.fasterxml.jackson.datatype
+ jackson-datatype-jdk8
+ ${jackson.version}
+ compile
+
+
+
diff --git a/bundles/org.openhab.io.yamlcomposer/src/main/feature/feature.xml b/bundles/org.openhab.io.yamlcomposer/src/main/feature/feature.xml
new file mode 100644
index 0000000000..cd68cd9cfe
--- /dev/null
+++ b/bundles/org.openhab.io.yamlcomposer/src/main/feature/feature.xml
@@ -0,0 +1,15 @@
+
+
+ mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features
+
+
+ openhab-runtime-base
+ openhab.tp-commons-net
+ mvn:org.snakeyaml/snakeyaml-engine/3.0.1
+ mvn:org.openhab.osgiify/com.hubspot.jinjava.jinjava/2.7.4
+ mvn:org.openhab.osgiify/com.google.re2j.re2j/1.2
+ mvn:ch.obermuhlner/big-math/2.3.2
+ mvn:org.openhab.osgiify/com.hubspot.immutables.immutables-exceptions/1.9
+ mvn:org.openhab.addons.bundles/org.openhab.io.yamlcomposer/${project.version}
+
+
diff --git a/bundles/org.openhab.io.yamlcomposer/src/main/java/org/openhab/io/yamlcomposer/internal/BufferedLogger.java b/bundles/org.openhab.io.yamlcomposer/src/main/java/org/openhab/io/yamlcomposer/internal/BufferedLogger.java
new file mode 100644
index 0000000000..5743df3193
--- /dev/null
+++ b/bundles/org.openhab.io.yamlcomposer/src/main/java/org/openhab/io/yamlcomposer/internal/BufferedLogger.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (c) 2010-2026 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.io.yamlcomposer.internal;
+
+import java.util.Objects;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.slf4j.Logger;
+import org.slf4j.helpers.MessageFormatter;
+
+/**
+ * A wrapper/decorator for SLF4J loggers that intercepts warnings for
+ * consolidation through {@link LogSession} while passing other
+ * log levels through immediately.
+ *
+ * @author Jimmy Tanagra - Initial contribution
+ */
+@NonNullByDefault
+public class BufferedLogger {
+ private final Logger delegate;
+ private final LogSession session;
+
+ public BufferedLogger(Logger delegate, LogSession session) {
+ this.delegate = delegate;
+ this.session = session;
+ }
+
+ public LogSession getLogSession() {
+ return session;
+ }
+
+ /**
+ * Intercepts warnings and sends them to the session for count consolidation.
+ */
+ public void warn(String template, Object... args) {
+ String resolvedMessage = Objects.requireNonNull(MessageFormatter.arrayFormat(template, args).getMessage());
+ session.trackWarning(delegate, resolvedMessage);
+ }
+
+ // Pass-through methods for immediate logging
+ public void info(String format, Object... args) {
+ delegate.info(format, args);
+ }
+
+ public void debug(String format, Object... args) {
+ delegate.debug(format, args);
+ }
+
+ public void trace(String format, Object... args) {
+ delegate.trace(format, args);
+ }
+
+ public void error(String format, Object... args) {
+ delegate.error(format, args);
+ }
+
+ // Simple string overloads
+ public void info(String msg) {
+ delegate.info(msg);
+ }
+
+ public void debug(String msg) {
+ delegate.debug(msg);
+ }
+
+ public void trace(String msg) {
+ delegate.trace(msg);
+ }
+
+ public void error(String msg) {
+ delegate.error(msg);
+ }
+}
diff --git a/bundles/org.openhab.io.yamlcomposer/src/main/java/org/openhab/io/yamlcomposer/internal/ComposerConfig.java b/bundles/org.openhab.io.yamlcomposer/src/main/java/org/openhab/io/yamlcomposer/internal/ComposerConfig.java
new file mode 100644
index 0000000000..86ea24ba02
--- /dev/null
+++ b/bundles/org.openhab.io.yamlcomposer/src/main/java/org/openhab/io/yamlcomposer/internal/ComposerConfig.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (c) 2010-2026 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.io.yamlcomposer.internal;
+
+import java.nio.file.Path;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.OpenHAB;
+
+/**
+ * Static configuration constants and system path resolution for the YAML Composer.
+ *
+ * @author Jimmy Tanagra - Initial contribution
+ */
+@NonNullByDefault
+public final class ComposerConfig {
+
+ private static final Path DEFAULT_CONFIG_ROOT = normalizeAbsolute(Path.of(OpenHAB.getConfigFolder()));
+ private static final Path DEFAULT_USERDATA_ROOT = normalizeAbsolute(Path.of(OpenHAB.getUserDataFolder()));
+
+ private static volatile Path configRootOverride = DEFAULT_CONFIG_ROOT;
+ private static volatile Path userDataRootOverride = DEFAULT_USERDATA_ROOT;
+
+ public static final Path SOURCE_ROOT_DIRECTORY = Path.of("yamlcomposer");
+ public static final Path OUTPUT_ROOT_DIRECTORY = Path.of("yaml", "composed");
+
+ // Special section keys
+ public static final String TEMPLATES_KEY = "templates";
+ public static final String VARIABLES_KEY = "variables";
+ public static final String PACKAGES_KEY = "packages";
+
+ // Preprocessing limits
+ public static final int MAX_INCLUDE_DEPTH = 100;
+
+ private ComposerConfig() {
+ // Static utility class
+ }
+
+ public static Path configRoot() {
+ return configRootOverride;
+ }
+
+ public static Path userDataRoot() {
+ return userDataRootOverride;
+ }
+
+ public static Path sourceRoot() {
+ return configRoot().resolve(SOURCE_ROOT_DIRECTORY);
+ }
+
+ public static Path outputRoot() {
+ return configRoot().resolve(OUTPUT_ROOT_DIRECTORY);
+ }
+
+ static void setRootsForTesting(Path configRoot, Path userDataRoot) {
+ configRootOverride = normalizeAbsolute(configRoot);
+ userDataRootOverride = normalizeAbsolute(userDataRoot);
+ }
+
+ static void resetRootsForTesting() {
+ configRootOverride = DEFAULT_CONFIG_ROOT;
+ userDataRootOverride = DEFAULT_USERDATA_ROOT;
+ }
+
+ private static Path normalizeAbsolute(Path path) {
+ return path.toAbsolutePath().normalize();
+ }
+
+ /**
+ * Resolves the output path for a generated YAML file.
+ *
+ * @param sourcePath source file's absolute path
+ * @return the path where the generated output should be written
+ */
+ public static Path resolveOutputPath(Path sourcePath) {
+ Path relativeToSourceRoot = sourceRoot().relativize(sourcePath);
+ return outputRoot().resolve(relativeToSourceRoot);
+ }
+}
diff --git a/bundles/org.openhab.io.yamlcomposer/src/main/java/org/openhab/io/yamlcomposer/internal/ComposerUtils.java b/bundles/org.openhab.io.yamlcomposer/src/main/java/org/openhab/io/yamlcomposer/internal/ComposerUtils.java
new file mode 100644
index 0000000000..50a0757802
--- /dev/null
+++ b/bundles/org.openhab.io.yamlcomposer/src/main/java/org/openhab/io/yamlcomposer/internal/ComposerUtils.java
@@ -0,0 +1,280 @@
+/*
+ * Copyright (c) 2010-2026 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.io.yamlcomposer.internal;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.ZonedDateTime;
+import java.util.ArrayList;
+import java.util.IdentityHashMap;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.OpenHAB;
+import org.openhab.io.yamlcomposer.internal.constructors.ModelConstructor;
+import org.snakeyaml.engine.v2.api.Dump;
+import org.snakeyaml.engine.v2.api.DumpSettings;
+import org.snakeyaml.engine.v2.api.Load;
+import org.snakeyaml.engine.v2.api.LoadSettings;
+import org.snakeyaml.engine.v2.common.FlowStyle;
+import org.snakeyaml.engine.v2.resolver.ScalarResolver;
+import org.snakeyaml.engine.v2.schema.CoreSchema;
+import org.snakeyaml.engine.v2.schema.Schema;
+
+/**
+ * Pure utility functions for YAML preprocessing.
+ *
+ * @author Jimmy Tanagra - Initial contribution
+ */
+@NonNullByDefault
+final class ComposerUtils {
+ private static final String GENERATED_HEADER = """
+ # ==============================================================================
+ # Generated by openHAB %s (%s) YAML Composer, DO NOT EDIT
+ # Source: %s
+ # Generated: %s
+ # ==============================================================================
+ """;
+
+ private ComposerUtils() {
+ // Static utility class
+ }
+
+ /**
+ * Removes top-level keys that start with a dot from the given map.
+ *
+ * @param dataMap the map to process
+ */
+ static void removeHiddenKeys(Map, ?> dataMap) {
+ dataMap.keySet().removeIf(key -> key instanceof String keyStr && keyStr.startsWith("."));
+ }
+
+ /**
+ * Writes the fully processed YAML representation as it would be seen by openHAB after preprocessing
+ * to a compiled output file.
+ *
+ * @param dataMap the resulting data map to dump
+ * @param sourcePath the absolute path of the source file being processed
+ * @throws IOException if writing to the file fails
+ */
+ static void writeCompiledOutput(Object dataMap, Path sourcePath, Path outputPath) throws IOException {
+
+ Path outputDir = outputPath.getParent();
+ if (outputDir != null) {
+ Files.createDirectories(outputDir);
+ }
+
+ DumpSettings dumpSettings = DumpSettings.builder() //
+ .setDefaultFlowStyle(FlowStyle.BLOCK) //
+ .setIndentWithIndicator(true) //
+ .setIndicatorIndent(2) //
+ .setMultiLineFlow(true) //
+ .build();
+ Dump yaml = new Dump(dumpSettings);
+ Object unaliasedData = breakAliases(dataMap);
+ String compiledYaml = yaml.dumpToString(unaliasedData);
+ compiledYaml = formatYamlSpacing(compiledYaml);
+
+ Path sourcePathRelative = ComposerConfig.configRoot().relativize(sourcePath);
+ compiledYaml = GENERATED_HEADER.formatted(OpenHAB.getVersion(), OpenHAB.buildString(), sourcePathRelative,
+ ZonedDateTime.now()) + "\n" + compiledYaml;
+
+ Files.writeString(outputPath, compiledYaml, StandardCharsets.UTF_8);
+ }
+
+ /**
+ * Creates a new SnakeYAML Engine load instance with the custom composer constructor and resolver.
+ *
+ * @param sourcePath the source path of the YAML file for error reporting in the constructor
+ * @return a new Load instance
+ */
+ private static Load createYamlLoader(String sourcePath) {
+ Schema schema = new CoreSchema() {
+ @Override
+ public ScalarResolver getScalarResolver() {
+ return new ModelResolver();
+ }
+ };
+
+ LoadSettings loadSettings = LoadSettings.builder() //
+ .setLabel(sourcePath) //
+ .setAllowDuplicateKeys(false) //
+ .setUseMarks(true) //
+ .setSchema(schema) //
+ .build();
+ return new Load(loadSettings, new ModelConstructor(loadSettings, sourcePath));
+ }
+
+ /**
+ * Loads YAML content from the given byte array using a Yaml instance configured with the custom constructor.
+ *
+ * @param fileBytes the YAML content as a byte array
+ * @param sourcePath the source path of the YAML file for error reporting in the constructor
+ * @return the loaded YAML object
+ * @throws IOException if an I/O error occurs
+ */
+ static @Nullable Object loadYaml(byte[] fileBytes, Path sourcePath) throws IOException {
+ Load loader = createYamlLoader(sourcePath.toString());
+ return loader.loadFromInputStream(new ByteArrayInputStream(fileBytes));
+ }
+
+ /**
+ * Produces a deep-copy of the common YAML structure objects (maps, lists, sets)
+ * such that no container instance is shared more than once in the resulting
+ * graph. This removes object identity sharing that SnakeYAML would otherwise
+ * emit as anchors/aliases when dumping.
+ *
+ * @param root the loaded YAML object (may be Map, List, Set, scalar, or custom)
+ * @return a deep-copied object graph with sharing broken
+ */
+ private static @Nullable Object breakAliases(@Nullable Object root) {
+ if (root == null) {
+ return null;
+ }
+ return cloneNode(root, new IdentityHashMap<>());
+ }
+
+ /**
+ * Post-processes a YAML string to inject empty lines for improved readability.
+ *
+ *
+ * This method applies two specific spacing rules:
+ *
+ *
Level 0: Inserts a newline before every top-level key (except the first line).
+ *
Level 2: Inserts a newline between sibling nodes indented by exactly two spaces.
+ *
+ *
+ *
+ * The logic ensures that the first child of a section remains "glued" to its parent header,
+ * and it safely handles both Map keys (e.g., {@code Key:}) and List items (e.g., {@code - Item}).
+ *
+ * @param yaml The raw YAML string to be formatted.
+ * @return A formatted YAML string with injected "breathing room."
+ */
+ private static @Nullable String formatYamlSpacing(@Nullable String yaml) {
+ if (yaml == null || yaml.isEmpty()) {
+ return yaml;
+ }
+
+ String[] lines = yaml.split("\n");
+ StringBuilder sb = new StringBuilder();
+
+ boolean hasSeenLevel0 = false;
+ boolean hasSeenLevel2 = false;
+
+ for (String line : lines) {
+ // --- Level 0 Logic (Sections) ---
+ // Matches lines starting with a character (not space, not comment)
+ if (!line.startsWith(" ") && !line.startsWith("#")) {
+ if (hasSeenLevel0) {
+ sb.append("\n");
+ }
+ hasSeenLevel0 = true;
+ hasSeenLevel2 = false; // Reset level 2 tracker for the new section
+ }
+
+ // --- Level 2 Logic (Items or List Elements) ---
+ // Matches exactly 2 spaces followed by a non-whitespace character.
+ // This targets siblings within a top-level section.
+ else if (line.matches("^ {2}[^ \\t].*")) {
+ if (hasSeenLevel2) {
+ sb.append("\n");
+ }
+ hasSeenLevel2 = true;
+ }
+
+ sb.append(line).append("\n");
+ }
+
+ return sb.toString().trim();
+ }
+
+ /**
+ * Recursively clones a YAML node, breaking object identity to prevent SnakeYAML from emitting anchors/aliases.
+ *
+ * @param node The YAML node to clone.
+ * @param stack The recursion stack used to handle cycles.
+ * @return A deep-cloned YAML node with object identity broken.
+ */
+ private static @Nullable Object cloneNode(@Nullable Object node, IdentityHashMap