Skip to main content

Creating Enriched Documentation

The generator writes a Markdown document next to the NodeSet2 file. Three keys feed it:

KeyWhereReaches the NodeSet2?Content
descriptionany nodeyes, as the OPC UA Description attributeone sentence (long form: see below)
documentationany nodenoMarkdown for the generated document
documentationroot of the filenoa 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 description attribute of the OPC UA node in the address space.
  • Long form: a multi-line Markdown text. The first line populates the OPC UA description attribute; the rest is added to the generated Markdown document.
Rich description on a method
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.

description + documentation
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.

Model-level documentation
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

![Wiring of the reactor line](./images/reactor-wiring.svg)
PropertyTypeEffect on the generated document
headerstring or mappingWritten 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.
titlestringWritten as the level-1 heading.
descriptionMarkdownThe introduction, written under the title. Supports !include, Mermaid, PlantUML and images.
authorstring or listAccepted, but not written by the current generator. To print the authors, add an author entry to header.
annexeslistAnnexes written after the generated part: see Annexes and bibliography.
bibliographyMarkdownWritten last, after the annexes.
options.diagrams.imageSuffixstringText appended after every type diagram and PlantUML image (not Mermaid), e.g. "{ width=60% }" for pandoc.
options.diagrams.compactbooleanDraws 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 and bibliography (inside documentation:)
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 propertyEffect
titleRequired. Written after the annex letter.
normativetrue writes (Normative); the default is (Informative).
fileA Markdown file holding the annex text, relative to the model file.
descriptionInline 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 other documentation Markdown does.
  • bibliography is Markdown too, written last under a Bibliography heading.
  • An annex needs a title and exactly one of file and description (DSL-E-ANNEX-001); an annex written as a bare path is DSL-E-KEY-004. A file that does not exist shows in the generated text as > Error: File not found, as a missing !include does.

!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:

!include directive
objectTypes:
- browseName: ReactorType
description: Main reactor vessel type.
documentation: |
!include(./docs/reactor-type.md)
Several !include directives
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.yaml file.
  • 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 !include line. With no heading before it, they are kept as written, so in the root description start them at ## (the title is the # heading).
  • !include is not expanded in description, 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:

docs/annex-namespace.md
# 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`.
NameBlock
normative-referencesOne 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-metadataA 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.
  • !generate works in the root documentation: its description, its annexes and its bibliography, including in files they !include. It is not expanded in a node's documentation.
  • 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:

![Wiring of the reactor line](./images/reactor-wiring.svg)

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
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

![Wiring of the reactor line](./images/reactor-wiring.svg)
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
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>