Skip to main content

How to create a NodeSet2.xml file

A NodeSet2.xml file is how an OPC UA information model travels from the person who designed it to the server that exposes it. This guide explains what is in one, compares the ways to produce it, and then builds one from scratch: a pump, derived from the Device Integration companion specification, in about forty lines of YAML.

What a NodeSet2.xml is

It is an XML document whose root element is UANodeSet, in the schema the OPC Foundation defines in OPC UA Part 6, Annex F (namespace http://opcfoundation.org/UA/2011/03/UANodeSet.xsd). It lists every node of one namespace — object types, variable types, data types, reference types, objects, variables, methods — and the references between them.

Four things in it matter in practice:

PartWhat it says
NamespaceUrisThe namespaces the file talks about. Yours is index 1; every ns=1;… NodeId in the file belongs to it.
Models / RequiredModelWhich model this file defines, its version, and which other NodeSets must be loaded first.
AliasesShort names for well-known NodeIds (HasComponent for i=47), to keep the file readable.
UAObjectType, UAVariable, …The nodes themselves, each with its NodeId, BrowseName and references.

Every OPC UA companion specification — DI, Machinery, PackML, Robotics — is published as a NodeSet2.xml next to its PDF, and every mainstream OPC UA stack can load one: node-opcua, open62541, the OPC Foundation .NET Standard stack, Unified Automation's SDKs, Eclipse Milo.

Why "2"?

NodeSet2 names the current UANodeSet format. The OPC Foundation's original NodeSet.xml files used an older serialisation that is no longer used for new models. If a tool asks for "a nodeset", it means a NodeSet2.xml.

Four ways to create one

ApproachToolsGood atCosts you
Write the XML by handA text editorNothing, beyond a five-node experimentNodeIds, modelling rules and inverse references are all yours to keep consistent. One mistake and the server rejects the file, or worse, loads it.
Compile ModelDesign XMLOPC Foundation UA-ModelCompilerThe Foundation's own specifications; C# and ANSI C stubsAnother XML dialect to learn, with little documentation and no editor support.
Draw it in a GUIUaModeler, Siemens SiOME, the OPC Foundation's UA NodeSet EditorOccasional modelling; SDK or PLC integrationThe model lives in a project file or a database. Diff, review and merge happen in the tool or not at all.
Model as codeOPC UA Modeler (this site)Models that change, are reviewed, and are built in CIYou learn a small YAML language.

The rest of this guide takes the fourth route. The first section applies to all of them: whatever you use, the result has to be the file described above.

Step 1 — Write the model

Save this as pump.model.yaml:

pump.model.yaml
namespaceUri: http://acme.example/UA/Pump/
version: 1.0.0
publicationDate: "2026-01-01T00:00:00Z"

# Companion specifications this model builds on. `ua` (the OPC UA base
# namespace) is always there; `di` is Device Integration.
namespaces:
- di

objectTypes:
- browseName: PumpType
subtypeOf: di:DeviceType
description: A centrifugal pump.
components:
# DI keeps process values in the ParameterSet.
- browseName: di:ParameterSet
components:
- browseName: FlowRate
typeDefinition: ua:AnalogUnitType
dataType: ua:Double
engineeringUnits: "m³/h"
description: The measured flow.
- browseName: SetPoint
dataType: ua:Double
accessLevel: CurrentRead | CurrentWrite
description: The flow the pump is asked to deliver.

instances:
- browseName: Pump1
typeDefinition: PumpType
organizedBy: /ua:Objects
initializers:
- variable: di:Manufacturer
value: "Acme Pumps"
- variable: di:Model
value: "CP-200"
- variable: di:SerialNumber
value: "SN-0001"

Reading it top to bottom:

  • namespaceUri is the identity of your model. Use a URI you own; it does not have to resolve. It becomes NamespaceUris[1] and the ModelUri.
  • namespaces lists the companion specifications you build on, by alias. The OPC Foundation's specifications ship with the tool, so di is enough — there is no file to find or import. The aliases are listed in Namespaces.
  • objectTypes declares PumpType as a subtype of DI's DeviceType. It therefore inherits the whole DI nameplate — Manufacturer, Model, SerialNumber and the rest — without naming any of it.
  • The two process values go inside di:ParameterSet, because that is where DI says a device keeps them. Declare them directly on the type and the compiler reports DSL-W-DI-001 with the line number.
  • instances creates one pump under the server's Objects folder and gives the mandatory nameplate properties real values.

You can type this into the online editor and watch the diagnostics and the diagram update as you go, or use VS Code for the same checks offline.

Step 2 — Generate the NodeSet2.xml

With the desktop CLI:

opcua-modeler generate -i pump.model.yaml --strict

--strict turns warnings into errors, which is what you want in a build.

Or with the REST API and an API key:

curl -X POST https://api.opcua-modeler.sterfive.io/api/v1/generate \
-H "Authorization: Bearer $OPCUA_MODELER_API_KEY" \
-H "Content-Type: text/yaml" \
--data-binary @pump.model.yaml

An AI agent does the same through the MCP server's opcua_model_generate tool.

Three files come out: the NodeSet2.xml, a Markdown document describing the model with its diagrams, and a symbols CSV (see Step 4).

Step 3 — Read what came out

This is the top of the generated file, unedited apart from the shortened root element and alias list:

<UANodeSet xmlns="http://opcfoundation.org/UA/2011/03/UANodeSet.xsd" >
<NamespaceUris>
<Uri>http://acme.example/UA/Pump/</Uri>
<Uri>http://opcfoundation.org/UA/DI/</Uri>
</NamespaceUris>
<Models>
<Model ModelUri="http://acme.example/UA/Pump/" Version="1.0.0" PublicationDate="2026-01-01T00:00:00.000Z">
<RequiredModel ModelUri="http://opcfoundation.org/UA/" Version="1.05.07" PublicationDate="2026-05-01T00:00:00.000Z"/>
<RequiredModel ModelUri="http://opcfoundation.org/UA/DI/" Version="1.05.0" PublicationDate="2025-11-15T00:00:00.000Z"/>
</Model>
</Models>
<Aliases>
<Alias Alias="HasComponent">i=47</Alias>
<Alias Alias="HasSubtype">i=45</Alias>

</Aliases>
<UAObjectType NodeId="ns=1;i=1000" BrowseName="1:PumpType">
<DisplayName>PumpType</DisplayName>
<Description>A centrifugal pump.</Description>
<References>
<Reference ReferenceType="HasComponent">ns=1;i=1001</Reference>
<Reference ReferenceType="HasSubtype" IsForward="false">ns=2;i=1002</Reference>
</References>
</UAObjectType>
<UAObject NodeId="ns=1;i=1001" BrowseName="2:ParameterSet" ParentNodeId="ns=1;i=1000">

Forty lines of YAML became 186 lines of XML: 1 object type, 3 objects and 14 variables. Most of those variables you never wrote — they are the DI nameplate properties and the EURange and EngineeringUnits of the analog item, instantiated because the types you referenced make them mandatory.

Three details are worth checking in any NodeSet2.xml, whoever produced it:

  • RequiredModel names DI with a version and a publication date. A server uses this to refuse a file whose dependency is missing or older than expected.
  • HasSubtype … IsForward="false" points at ns=2;i=1002. ns=2 is the second entry of this file's NamespaceUris, DI — not namespace 2 of the server. Indexes in a NodeSet are local to the file and remapped on load.
  • BrowseName="2:ParameterSet" keeps DI's namespace even though the node itself is yours (ns=1). Getting that wrong is the most common error in hand-written NodeSets: clients looking for 2:ParameterSet would not find it.

Step 4 — Keep the NodeIds stable

ns=1;i=1000 was allocated by the generator. If the next build allocated different numbers, every client that stored a NodeId would break.

The symbols CSV is what prevents that. It maps each browse path to the NodeId it was given, and the generator reads it back on the next run. Commit it next to the model. Add a variable next month and it gets a new number; everything that already existed keeps its own.

Step 5 — Load it into a server

Dependencies first, your file last. With node-opcua:

import { OPCUAServer, nodesets } from "node-opcua";

const server = new OPCUAServer({
port: 4840,
// the base namespace, then DI, then the file generated in step 2
nodeset_filename: [nodesets.standard, nodesets.di, "./pump.model.Nodeset2.xml"]
});
await server.start();

open62541 compiles the same file with its nodeset compiler, and the .NET Standard stack imports it at start-up. The file is the standard one; nothing in it is specific to the tool that wrote it.

To look before you write any server code:

opcua-modeler generate -i pump.model.yaml --server

starts a real OPC UA server on the model, which you can browse with UaExpert or any other client. The online app does the same from its Try Server button.

Common mistakes

Loading files in the wrong order. A NodeSet that requires DI cannot be loaded before DI. Follow the RequiredModel entries.

Renumbering NodeIds between releases. Regenerating without the symbols file, or re-exporting from a GUI project that was recreated, silently changes them.

Borrowing a namespace URI. http://opcfoundation.org/UA/… belongs to the Foundation. Your types go in a namespace under a domain you control.

Editing the generated XML. The next build overwrites it. Change the model.

Process values outside ParameterSet on a DI device. It loads, and DI-aware clients do not find the values where the specification says they are.

Questions

Can I turn an existing NodeSet2.xml back into a model?

Yes. opcua-modeler reverse --input Vendor.NodeSet2.xml produces the YAML, which you can then edit and regenerate. See reverse.

How do I use a NodeSet that is not an OPC Foundation specification?

Reference the file with an imports: entry instead of a namespaces: alias. See Custom nodesets.

Does the NodeSet2.xml depend on OPC UA Modeler at run time?

No. It is a plain UANodeSet document. Nothing from this tool needs to be installed where the server runs.

Where are the official NodeSet2.xml files?

In the OPC Foundation's UA-Nodeset repository and the UA Cloud Library.

Next