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:
| Part | What it says |
|---|---|
NamespaceUris | The namespaces the file talks about. Yours is index 1; every ns=1;… NodeId in the file belongs to it. |
Models / RequiredModel | Which model this file defines, its version, and which other NodeSets must be loaded first. |
Aliases | Short 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.
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
| Approach | Tools | Good at | Costs you |
|---|---|---|---|
| Write the XML by hand | A text editor | Nothing, beyond a five-node experiment | NodeIds, 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 XML | OPC Foundation UA-ModelCompiler | The Foundation's own specifications; C# and ANSI C stubs | Another XML dialect to learn, with little documentation and no editor support. |
| Draw it in a GUI | UaModeler, Siemens SiOME, the OPC Foundation's UA NodeSet Editor | Occasional modelling; SDK or PLC integration | The model lives in a project file or a database. Diff, review and merge happen in the tool or not at all. |
| Model as code | OPC UA Modeler (this site) | Models that change, are reviewed, and are built in CI | You 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:
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:
namespaceUriis the identity of your model. Use a URI you own; it does not have to resolve. It becomesNamespaceUris[1]and theModelUri.namespaceslists the companion specifications you build on, by alias. The OPC Foundation's specifications ship with the tool, sodiis enough — there is no file to find or import. The aliases are listed in Namespaces.objectTypesdeclaresPumpTypeas a subtype of DI'sDeviceType. It therefore inherits the whole DI nameplate —Manufacturer,Model,SerialNumberand 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 reportsDSL-W-DI-001with the line number. instancescreates one pump under the server'sObjectsfolder 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:
RequiredModelnames 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 atns=2;i=1002.ns=2is the second entry of this file'sNamespaceUris, 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 for2:ParameterSetwould 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
- The YAML DSL reference
- Temperature sensor, in full — adds methods and documentation
- The build pipeline — what happens between the YAML and the XML