Creating Enriched Documentation
The generator writes a Markdown document next to the NodeSet2 file. Three keys feed it:
| Key | Where | Reaches the NodeSet2? | Content |
|---|---|---|---|
description | any node | yes, as the OPC UA Description attribute | one sentence (long form: see below) |
documentation | any node | no | Markdown for the generated document |
documentation | root of the file | no | a mapping: title, header, introduction, annexes, bibliography, diagram options |
The two documentation keys look alike but are not written the same way: on a node it is
Markdown text, at the root it is a mapping. Markdown written directly under the root
documentation: is rejected with DSL-E-KEY-004.
description on a node
- Short form: a single sentence that populates the
descriptionattribute of the OPC UA node in the address space. - Long form: a multi-line Markdown text. The first line populates the OPC UA
descriptionattribute; the rest is added to the generated Markdown document.
objectTypes:
- browseName: TemperatureSensorType
methods:
- browseName: Calibrate
description: |
Initiate the calibration of the sensor. This line will go into the `description`
attribute of the OPCUA node.
The Calibrate method initiates the calibration of the **temperature sensor**
on the physical device. This method takes __two__ arguments that define
the linear interpolation `T = A*x + B`.
| key | value |
|-----|-------|
| A | B |
inputArguments:
- name: CoefficientA
dataType: ua:Double
description: Slope coefficient A for linear interpolation
- name: CoefficientB
dataType: ua:Double
description: Offset coefficient B for linear interpolation
outputArguments:
- name: PreviousA
dataType: ua:Double
- name: PreviousB
dataType: ua:Double
The long form is a shortcut: it does not expand !include. Use documentation for that.
documentation on a node
Keep description to the one sentence that goes into the address space, and put the
long text in documentation. It is Markdown, it never reaches the NodeSet2, and it is
where !include works.
objectTypes:
- browseName: FeedPumpType
description: Pump that feeds the reactor vessel.
documentation: |
The pump runs at a fixed speed while the reactor is filling and stops
when the vessel reports `Full`.
| State | Pump |
|---------|------|
| Filling | On |
| Full | Off |
Headings in a node's documentation are re-levelled to fit under the node's own heading,
so you can start them at #.
Model-level documentation
The root documentation describes the model as a whole. It opens the generated document,
before the namespace tables and the types.
documentation:
title: Reactor Line
author: [Jane Doe, John Doe]
header:
subtitle: Information model of the reactor line
namespaceUri: ${namespaceUri}
version: ${version}
options:
diagrams:
imageSuffix: "{ width=60% }"
description: |
This model describes the reactor line: the reactor vessel, its sensors
and the safety controller that supervises them.
!include(./docs/overview.md)
## Normative references
!generate(normative-references)
## Line topology
```mermaid
graph LR
Feed[Feed pump] --> Reactor[Reactor vessel]
Reactor --> Sensor[Temperature sensor]
Safety[Safety controller] -.supervises.-> Reactor
```
## Signal flow
```plantuml
digraph G {
rankdir=LR;
Sensor -> Controller -> Valve;
}
```
## Wiring

| Property | Type | Effect on the generated document |
|---|---|---|
header | string or mapping | Written at the very top. A mapping becomes a --- front-matter block, one key: value line per entry, for pandoc and similar tools; a string is written as is. |
title | string | Written as the level-1 heading. |
description | Markdown | The introduction, written under the title. Supports !include, Mermaid, PlantUML and images. |
author | string or list | Accepted, but not written by the current generator. To print the authors, add an author entry to header. |
annexes | list | Annexes written after the generated part: see Annexes and bibliography. |
bibliography | Markdown | Written last, after the annexes. |
options.diagrams.imageSuffix | string | Text appended after every type diagram and PlantUML image (not Mermaid), e.g. "{ width=60% }" for pandoc. |
options.diagrams.compact | boolean | Draws type diagrams in compact form: members already implied by their TypeDefinition are left out. |
In header values, ${namespaceUri}, ${version}, ${publicationDate} and ${namespaces}
are replaced by the corresponding root properties. No other property is accepted under
the root documentation (DSL-E-KEY-001).
Annexes and bibliography
OPC Foundation companion specifications end with annexes, then a bibliography, for instance
OPC 10000-81:
"Annex A OPC UA FX Namespace and mappings (Normative)", with sub-sections A.1, A.2, …
List yours under annexes in the root documentation, after the keys above:
annexes:
- title: Reactor Line Namespace and mappings
normative: true
file: ./docs/annex-namespace.md
- title: Commissioning examples
description: |
## First start
Fill the vessel before starting the feed pump.
!include(./docs/annex-commissioning.md)
bibliography: |
- OPC 10000-1, OPC Unified Architecture - Part 1: Overview and Concepts
- OPC 10000-5, OPC Unified Architecture - Part 5: Information Model
The generator writes them after the generated part of the document:
## Annex A Reactor Line Namespace and mappings (Normative)
### A.1 Namespace and identifiers
#### A.1.1 NodeIds
## Annex B Commissioning examples (Informative)
### B.1 First start
#### B.1.1 Safety check
## Bibliography
| Annex property | Effect |
|---|---|
title | Required. Written after the annex letter. |
normative | true writes (Normative); the default is (Informative). |
file | A Markdown file holding the annex text, relative to the model file. |
description | Inline Markdown, in place of file. |
- Annexes are lettered A, B, C, … in list order: reorder the list and the letters follow.
- Write plain headings in the annex text, at any level: they are re-levelled under the annex heading and numbered A.1, A.1.1, … Headings inside fenced code blocks are left alone.
- The annex text supports
!include, Mermaid, PlantUML and images, as the otherdocumentationMarkdown does. bibliographyis Markdown too, written last under aBibliographyheading.- An annex needs a
titleand exactly one offileanddescription(DSL-E-ANNEX-001); an annex written as a bare path isDSL-E-KEY-004. Afilethat does not exist shows in the generated text as> Error: File not found, as a missing!includedoes.
!include(path): embedding external files
Keep long documentation in separate Markdown files and pull it in with !include(path),
on a line of its own, inside a node's documentation or the root documentation.description:
objectTypes:
- browseName: ReactorType
description: Main reactor vessel type.
documentation: |
!include(./docs/reactor-type.md)
objectTypes:
- browseName: SafetyControllerType
description: Safety controller.
documentation: |
The controller trips the line when a limit is exceeded.
!include(./docs/safety-notes.md)
!include(./docs/safety-maintenance.md)
- The path is relative to the directory of the
.model.yamlfile. - An included file may include other files, with paths relative to its own directory.
A cycle or a missing file does not stop the build: it shows up in the generated text
as
> Error: …. - Headings of an included file are re-levelled to sit under the heading that precedes the
!includeline. With no heading before it, they are kept as written, so in the rootdescriptionstart them at##(thetitleis the#heading). !includeis not expanded indescription, short or long form.
Recommended project layout:
your-project/
├── model.yaml
├── docs/
│ ├── annex-namespace.md
│ ├── overview.md
│ ├── reactor-type.md
│ └── safety-notes.md
└── images/
└── reactor-wiring.svg
!generate(name): blocks built from the model
Some parts of a specification document are not prose: the normative references are the
models yours depends on, and the "Namespace and mappings" annex restates the namespace
metadata. !generate(name), on a line of its own, writes such a block where you put it,
the way !include inserts a file. You keep the layout and the wording around it.
In the model-level example, the introduction has a
"Normative references" heading followed by !generate(normative-references). Annex A
takes its text from a file that ends its first section with the namespace metadata:
# Namespace and identifiers
This model uses the namespace `http://example.com/doc/enriching-documentation/`,
described by the following NamespaceMetadata.
!generate(namespace-metadata)
## NodeIds
The NodeIds of the model are listed in `enriching-documentation.model.symbols.csv`.
| Name | Block |
|---|---|
normative-references | One list item per model the NodeSet2 lists as a RequiredModel: the specification name when known, its version, its publication date and its namespace URI. |
namespace-metadata | A table of the properties of the model's NamespaceMetadata object (see namespaceMetadatas), or of its namespace URI, version and publication date when it has none. |
!generateworks in the rootdocumentation: itsdescription, its annexes and itsbibliography, including in files they!include. It is not expanded in a node'sdocumentation.- The blocks hold no headings: write the heading yourself, as in the example.
- An unknown name shows in the generated text as
> Error: unknown generated block: …. - Document numbers such as "OPC 10000-100" are not generated: write them in your own text if you want them.
Diagrams and images
documentation Markdown, on a node or at the root, may contain diagrams. The generator
turns them into SVG images embedded in the Markdown document.
Mermaid
A fenced block tagged mermaid is rendered to SVG by a headless browser (Puppeteer) on
the machine that runs the generator. See the "Line topology" block in the model-level
example above.
Where no browser can be launched (a CI runner or a slim container without Chrome), or
when Mermaid rejects a diagram, the block is kept as a ```mermaid code block and the
generator prints a warning. GitHub and GitLab render such blocks themselves, and so
does Docusaurus with its Mermaid theme enabled.
PlantUML
A fenced block tagged plantuml is rendered to SVG as well:
- a Graphviz
digraph, as in the "Signal flow" block above, is rendered locally; - any other PlantUML diagram (sequence, class, state, …) is sent to the public service at
www.plantuml.com. The generator then needs network access, and the diagram text leaves your machine: do not use it for confidential content.
options.diagrams.imageSuffix is appended to these images (it is not appended to Mermaid images).
Images (SVG, PNG, …)
Standard Markdown images work, as in the "Wiring" block above:

The link is written to the generated document unchanged: the image file is not copied and
the path is not rewritten. A relative path is therefore resolved from the generated .md
file, not from the model. Generate into the model's directory, or use an absolute URL.
📄 Full working example — enriching-documentation.model.yaml
#
# Living documentation example for: s99_enriching-documentation.md
# Validate: opcua-modeler generate --input enriching-documentation.model.yaml
namespaceUri: http://example.com/doc/enriching-documentation/
version: 1.0.0
publicationDate: "2026-10-22T00:00:00.000Z"
documentation:
title: Reactor Line
author: [Jane Doe, John Doe]
header:
subtitle: Information model of the reactor line
namespaceUri: ${namespaceUri}
version: ${version}
options:
diagrams:
imageSuffix: "{ width=60% }"
description: |
This model describes the reactor line: the reactor vessel, its sensors
and the safety controller that supervises them.
!include(./docs/overview.md)
## Normative references
!generate(normative-references)
## Line topology
```mermaid
graph LR
Feed[Feed pump] --> Reactor[Reactor vessel]
Reactor --> Sensor[Temperature sensor]
Safety[Safety controller] -.supervises.-> Reactor
```
## Signal flow
```plantuml
digraph G {
rankdir=LR;
Sensor -> Controller -> Valve;
}
```
## Wiring

annexes:
- title: Reactor Line Namespace and mappings
normative: true
file: ./docs/annex-namespace.md
- title: Commissioning examples
description: |
## First start
Fill the vessel before starting the feed pump.
!include(./docs/annex-commissioning.md)
bibliography: |
- OPC 10000-1, OPC Unified Architecture - Part 1: Overview and Concepts
- OPC 10000-5, OPC Unified Architecture - Part 5: Information Model
objectTypes:
- browseName: TemperatureSensorType
methods:
- browseName: Calibrate
description: |
Initiate the calibration of the sensor. This line will go into the `description`
attribute of the OPCUA node.
The Calibrate method initiates the calibration of the **temperature sensor**
on the physical device. This method takes __two__ arguments that define
the linear interpolation `T = A*x + B`.
| key | value |
|-----|-------|
| A | B |
inputArguments:
- name: CoefficientA
dataType: ua:Double
description: Slope coefficient A for linear interpolation
- name: CoefficientB
dataType: ua:Double
description: Offset coefficient B for linear interpolation
outputArguments:
- name: PreviousA
dataType: ua:Double
- name: PreviousB
dataType: ua:Double
- browseName: FeedPumpType
description: Pump that feeds the reactor vessel.
documentation: |
The pump runs at a fixed speed while the reactor is filling and stops
when the vessel reports `Full`.
| State | Pump |
|---------|------|
| Filling | On |
| Full | Off |
- browseName: ReactorType
description: Main reactor vessel type.
documentation: |
!include(./docs/reactor-type.md)
- browseName: SafetyControllerType
description: Safety controller.
documentation: |
The controller trips the line when a limit is exceeded.
!include(./docs/safety-notes.md)
!include(./docs/safety-maintenance.md)
📄 Generated NodeSet2.xml — enriching-documentation.model.Nodeset2.xml
<?xml version="1.0"?>
<UANodeSet xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:uax="http://opcfoundation.org/UA/2008/02/Types.xsd" xmlns="http://opcfoundation.org/UA/2011/03/UANodeSet.xsd" xmlns:ns1="http://example.com/doc/enriching-documentation/Type.xsd">
<NamespaceUris>
<Uri>http://example.com/doc/enriching-documentation/</Uri>
</NamespaceUris>
<Models>
<Model ModelUri="http://example.com/doc/enriching-documentation/" Version="1.0.0" PublicationDate="2026-10-22T00:00:00.000Z">
<RequiredModel ModelUri="http://opcfoundation.org/UA/" Version="1.05.07" PublicationDate="2026-05-01T00:00:00.000Z"/>
</Model>
</Models>
<Aliases>
<Alias Alias="Argument">i=296</Alias>
<Alias Alias="HasComponent">i=47</Alias>
<Alias Alias="HasModellingRule">i=37</Alias>
<Alias Alias="HasProperty">i=46</Alias>
<Alias Alias="HasSubtype">i=45</Alias>
<Alias Alias="HasTypeDefinition">i=40</Alias>
</Aliases>
<!--ReferenceTypes-->
<!--ObjectTypes-->
<!--ObjectType - 1:TemperatureSensorType {{{{ -->
<UAObjectType NodeId="ns=1;i=1000" BrowseName="1:TemperatureSensorType">
<DisplayName>TemperatureSensorType</DisplayName>
<References>
<Reference ReferenceType="HasSubtype" IsForward="false">i=58</Reference>
<Reference ReferenceType="HasComponent">ns=1;i=1001</Reference>
</References>
</UAObjectType>
<UAMethod NodeId="ns=1;i=1001" BrowseName="1:Calibrate" ParentNodeId="ns=1;i=1000">
<DisplayName>Calibrate</DisplayName>
<Description>Initiate the calibration of the sensor. This line will go into the `description`</Description>
<References>
<Reference ReferenceType="HasModellingRule">i=78</Reference>
<Reference ReferenceType="HasProperty">ns=1;i=1002</Reference>
<Reference ReferenceType="HasProperty">ns=1;i=1003</Reference>
</References>
</UAMethod>
<UAVariable NodeId="ns=1;i=1002" BrowseName="InputArguments" ParentNodeId="ns=1;i=1001" ValueRank="1" ArrayDimensions="2" DataType="Argument">
<DisplayName>InputArguments</DisplayName>
<Description>the definition of the input argument of method 1:TemperatureSensorType.1:Calibrate</Description>
<References>
<Reference ReferenceType="HasTypeDefinition">i=68</Reference>
<Reference ReferenceType="HasModellingRule">i=78</Reference>
</References>
<Value>
<ListOfExtensionObject xmlns="http://opcfoundation.org/UA/2008/02/Types.xsd">
<ExtensionObject>
<TypeId>
<Identifier>i=297</Identifier>
</TypeId>
<Body>
<Argument>
<Name>CoefficientA</Name>
<DataType>
<Identifier>i=11</Identifier>
</DataType>
<ValueRank>-1</ValueRank>
<ArrayDimensions/>
<Description>
<Text>Slope coefficient A for linear interpolation</Text>
</Description>
</Argument>
</Body>
</ExtensionObject>
<ExtensionObject>
<TypeId>
<Identifier>i=297</Identifier>
</TypeId>
<Body>
<Argument>
<Name>CoefficientB</Name>
<DataType>
<Identifier>i=11</Identifier>
</DataType>
<ValueRank>-1</ValueRank>
<ArrayDimensions/>
<Description>
<Text>Offset coefficient B for linear interpolation</Text>
</Description>
</Argument>
</Body>
</ExtensionObject>
</ListOfExtensionObject>
</Value>
</UAVariable>
<UAVariable NodeId="ns=1;i=1003" BrowseName="OutputArguments" ParentNodeId="ns=1;i=1001" ValueRank="1" ArrayDimensions="2" DataType="Argument">
<DisplayName>OutputArguments</DisplayName>
<Description>the definition of the output arguments of method 1:TemperatureSensorType.1:Calibrate</Description>
<References>
<Reference ReferenceType="HasTypeDefinition">i=68</Reference>
<Reference ReferenceType="HasModellingRule">i=78</Reference>
</References>
<Value>
<ListOfExtensionObject xmlns="http://opcfoundation.org/UA/2008/02/Types.xsd">
<ExtensionObject>
<TypeId>
<Identifier>i=297</Identifier>
</TypeId>
<Body>
<Argument>
<Name>PreviousA</Name>
<DataType>
<Identifier>i=11</Identifier>
</DataType>
<ValueRank>-1</ValueRank>
<ArrayDimensions/>
<Description>
<Text/>
</Description>
</Argument>
</Body>
</ExtensionObject>
<ExtensionObject>
<TypeId>
<Identifier>i=297</Identifier>
</TypeId>
<Body>
<Argument>
<Name>PreviousB</Name>
<DataType>
<Identifier>i=11</Identifier>
</DataType>
<ValueRank>-1</ValueRank>
<ArrayDimensions/>
<Description>
<Text/>
</Description>
</Argument>
</Body>
</ExtensionObject>
</ListOfExtensionObject>
</Value>
</UAVariable>
<!--ObjectType - 1:TemperatureSensorType }}}}-->
<!--ObjectType - 1:ReactorType {{{{ -->
<UAObjectType NodeId="ns=1;i=1004" BrowseName="1:ReactorType">
<DisplayName>ReactorType</DisplayName>
<Description>Main reactor vessel type.</Description>
<References>
<Reference ReferenceType="HasSubtype" IsForward="false">i=58</Reference>
</References>
</UAObjectType>
<!--ObjectType - 1:ReactorType }}}}-->
<!--ObjectType - 1:SafetyControllerType {{{{ -->
<UAObjectType NodeId="ns=1;i=1005" BrowseName="1:SafetyControllerType">
<DisplayName>SafetyControllerType</DisplayName>
<Description>Safety controller.</Description>
<References>
<Reference ReferenceType="HasSubtype" IsForward="false">i=58</Reference>
</References>
</UAObjectType>
<!--ObjectType - 1:SafetyControllerType }}}}-->
<!--ObjectType - 1:FeedPumpType {{{{ -->
<UAObjectType NodeId="ns=1;i=1006" BrowseName="1:FeedPumpType">
<DisplayName>FeedPumpType</DisplayName>
<Description>Pump that feeds the reactor vessel.</Description>
<References>
<Reference ReferenceType="HasSubtype" IsForward="false">i=58</Reference>
</References>
</UAObjectType>
<!--ObjectType - 1:FeedPumpType }}}}-->
<!--VariableTypes-->
<!--Other Nodes-->
</UANodeSet>