TL;DR — OPC UA Modelling Best Practices
Full spec: reference.opcfoundation.org/Model-Best/v103/docs Source folder: opcua-reference/model-best
What Is It?
A non-normative whitepaper (i.e., recommendations, not requirements) on how to design OPC UA Information Models — primarily aimed at Companion Specification authors but useful for any vendor designing a UANodeSet. It is a living document published at higher cadence than the formal specs.
It covers naming, versioning rules, when to use which modelling concept, how to extend or deprecate, and how to combine ConformanceUnits with Profiles for testing.
At a Glance — The 15 Chapters
| # | Topic | One-liner |
|---|---|---|
| 1 | Scope | Non-normative best-practice guide; not every server uses every concept. |
| 2 | Naming Conventions | PascalCase everywhere; specific suffix rules per NodeClass. |
| 3 | Backward Compatibility | What you may and may not change while keeping the same NamespaceUri. |
| 4 | StatusCodes | Don't invent your own — request additions to the base spec; use DiagnosticInfo. |
| 5 | App-specific Method statuses | Add a trailing Status Int32 output arg + return Uncertain StatusCode. |
| 6 | Creating a Companion Specification | Use OPCF templates, NodeSet-Validator, define test cases early. |
| 7 | Using Modelling Concepts | The big chapter: ObjectTypes, VariableTypes, Methods, DataTypes, Events, References. |
| 8 | Configuration changes to AddressSpace | Prefer Methods over NodeManagement Services or writing Variables. |
| 9 | Dictionary References | Ship a separate UANodeSet of dictionary entries; servers merge them. |
| 10 | Using existing Companion Specs | Prefer composition over inheritance to survive future subtype additions. |
| 11 | Extending an existing Companion Spec | Add optional only; for breaking changes use a new …/V2/ NamespaceUri. |
| 12 | ConformanceUnits & Profiles | Smallest testable unit = ConformanceUnit; Profiles bundle them. |
| 13 | BrowseNames in text | Use 0:Name in spec prose; omit NamespaceIndex inside UANodeSet Description text. |
| 14 | Deprecation | IsDeprecated ReferenceType; for Companion Specs, prefer a new NamespaceUri instead. |
| 15 | Audit events | Reuse base AuditEventTypes; subtype for filtering, don't add fields. |
1. Scope
Whitepaper of recommendations — highly recommended but not required. Living document, extended over time.
2. Naming Conventions (the rules you'll forget)
All BrowseNames are PascalCase, including acronyms (PortMacAddress, NodeId, UInt32). Letters/digits/underscore only, except for the special <…> placeholder syntax. If special chars are needed, define a SymbolicName for code generation.
Suffix table
| NodeClass | Suffix | Examples |
|---|---|---|
| ObjectType | Type | ServerType, BaseEventType |
| VariableType | Type (or VariableType if ambiguous) | PropertyType, BaseDataVariableType |
| Structured DataType | DataType | RedundantServerDataType |
| Enumeration DataType | none (or Enum) | NodeClass, ServerState |
| Built-in / Simple | none | Int32, UtcTime |
| ReferenceType | none — verb-based | HasComponent, Organizes; inverse e.g. ComponentOf |
| Object/Variable/Method/View | none — no specific rule | — |
Other rules:
- Structure fields, Enum values, and Method Arguments →
PascalCase(the UANodeSet uses PascalCase even when the spec text shows lowerCamelCase). - Don't rely on case-sensitivity (
IdvsIDis illegal — useNetworkId/DeviceId). - BrowseNames of TypeDefinitions must be unique within their Namespace; avoid clashing with names from the base spec.
- UANodeSet filenames:
Opc.Ua.<ShortName>.NodeSet2.xmlfor Companion Specs,*NodeSet2.xmlfor vendors. - NamespaceUri template:
http://opcfoundation.org/UA/<short-name>/. No %-escaping.
3. Backward Compatibility — the Hard Rules
The reusable mantra: you may add optional things; you may add subtypes; almost everything else is breaking.
Allowed without breaking the NamespaceUri
- Add
Optional/OptionalPlaceholderInstanceDeclarations. - Add new Interfaces to ObjectTypes (no new mandatory members).
- Add subtypes of Objects/Variables/DataTypes/References.
- Add metadata to existing Method arguments (e.g.
EngineeringUnits). - Edit Description, DisplayName, InverseName if the meaning is unchanged (typo fixes).
- Promote
IsAbstractfromtrue→false(never the reverse).
Forbidden without a new Namespace
- Adding
Mandatory/MandatoryPlaceholderInstanceDeclarations. - Adding/removing Enumeration values, Structure or Union fields, OptionSet bits.
- Changing a Method signature (even adding optional args ⇒ define
MethodName2). - Changing a Variable's
DataType,ValueRank, orArrayDimensions(even refining to a subtype). - Changing TypeDefinition of an InstanceDeclaration (subtype only OK if it adds nothing mandatory).
- Inserting types inside an existing hierarchy (especially under
BaseEventType). - Removing any published Node — ever.
Strategies when you'd otherwise break things
- Subtype + new ConformanceUnit/Profile that requires it.
- Add optional members + ConformanceUnit/Profile making them mandatory.
- If neither works → new NamespaceUri (
…/V2/) or new parallel types (DeviceType2).
Tip: if you anticipate future enum extensions, don't use an Enumeration DataType — use
MultiStateValueDiscreteType(see §7.12).
4. StatusCodes
Do not define new StatusCodes in a Companion Spec or vendor model. Use DiagnosticInfo for richer detail. If you really need a new one, contact the base OPC UA WG.
5. App-specific Method Status Pattern
Add a trailing Status Int32 output argument with documented enum values. On error, return StatusCode Uncertain and put the application code in Status.
SomeMethod(
[in] String SomeInput,
[out] UInt32 SomeOutput,
[out] Int32 Status // 0 = OK, -1 = E_FirstError, -2 = E_SecondError, …
)
Use a HasArgumentDescription reference to attach an EnumValues Property to the Status argument so clients can decode it.
6. Creating a Companion Specification
A Companion Spec = a prose document (the Information Model definition) + a UANodeSet XML file.
- Use OPCF templates (Word, Visio shapes), NodeSet-Validator before publishing.
- Keep the legacy
DataTypeDictionaryNodes in your UANodeSet for compatibility with old apps even though the modern way isDataTypeDefinition. - Start writing test cases as soon as the model stabilises — defining tests routinely uncovers modelling bugs.
- Contact:
Compliance@opcfoundation.org(CMP working group).
7. Using OPC UA Modelling Concepts (the fat chapter)
7.2 ObjectTypes
- Reuse before reinvent — search reference.opcfoundation.org first.
- Single inheritance only. Use composition (Interfaces, AddIns) for orthogonal aspects.
- Use a grouping Object once a type has > ~30 subcomponents, or when subcomponents need a
PlaceholderModellingRule. - Interface vs AddIn:
- Interface — small/simple, can be deployed directly on a type (no grouping object).
- AddIn — implies a grouping Object with a standardized BrowseName; better for richer features.
- Mandatory vs Optional: prefer
Mandatorywith a sensible default if the value is at least writeable; useOptionalif the server simply can't provide it. Avoid mandatory-but-returns-Bad pattern. - Placeholder combinations (grouping Object × placeholder rule):
Grouping Placeholder Use when Mandatory MandatoryPlaceholder At least one child of this type required. Mandatory OptionalPlaceholder Group has its own functionality (e.g. an AddMethod).Optional MandatoryPlaceholder Group only appears when at least one child exists. Optional OptionalPlaceholder Future subtypes may upgrade the group's role.
7.3 VariableTypes
- Define a new VariableType only if it'll be used in many places — base specs (esp.
AnalogItemType) usually suffice. - Property vs DataVariable:
- Property = describes the Node, leaf-only, always
PropertyType, can't have its own Properties. - DataVariable = anything substantive; required if you need to attach Properties.
- Property = describes the Node, leaf-only, always
- Don't invent Properties that duplicate built-in Attributes (e.g.
Description).
7.4 Methods
- ModellingRule
Optional/Mandatory→ instance Method on each instance. OptionalPlaceholder/MandatoryPlaceholder→ instances pick their own signature (only when signature can't be standardized).- No ModellingRule → class Method called on the ObjectType itself (e.g. a
Createfactory).
7.5 Granularity of structured data — five choices
For something like an IP configuration:
| Approach | Transactional | Generic-client friendly | Per-field metadata | Subscribe to changes |
|---|---|---|---|---|
| 1. Individual Variables | ❌ | ✅ | ✅ | ✅ |
| 2. Structured DataType + one Variable | ✅ | ❌ | ❌ | ✅ |
| 3. Structured DataType + Variable + sub-Variables | ✅ | ✅ | ✅ | ✅ |
| 4. Methods (get/set) | ✅ | ✅ (no SDK) | ❌ | ❌ |
| 5. Events | ✅ | ✅ | per-field via subset | ✅ (server-driven) |
Recommendations:
- No transaction needed → Approach 1.
- Transaction needed and server can interpret structure → Approach 3.
- Server can't decode the structured payload → Approach 2.
- Server-side computed / triggered → Method.
- Event-driven (e.g. quality data on a finished part) → Events.
7.6 Lists / arrays of things
Four options: array Variable; array Variable with sub-Variables (ExposesItsArray vs HasStructuredComponent); referenced Objects (often grouped); ordered via OrderedListType. Pick based on whether you need order, per-entry metadata, or per-entry References.
7.7 EventTypes
Same considerations as ObjectTypes. Extensibility across the EventType hierarchy is best done with Interfaces.
7.8 ReferenceTypes
OPC UA distinguishes hierarchical (used for the model structure of TypeDefinitions) and non-hierarchical. Define a new ReferenceType only when no existing one carries the right semantic.
7.9 DataTypes — when to pick what
- Use the simplest type that does the job (integer over float; numeric OptionSet over OptionSet DataType).
- Combine multiple booleans → an OptionSet.
- Structured DataType pitfalls:
- "Allow subtypes" and "have optional fields" in the same Structure → not allowed together. Workarounds:
- Default values (incl. NULL) instead of optional fields.
- Two nested sub-structures:
OptionalFieldsandSubtypableFields. - Use
BaseDataType/Structurefor fields that need subtyping (loses type safety). - Use a length-0/1 array as an "optional" scalar.
- Max 32 optional fields per Structure.
- "Allow subtypes" and "have optional fields" in the same Structure → not allowed together. Workarounds:
7.10 Recursion
Avoid. If unavoidable: never use Mandatory/MandatoryPlaceholder for the recursive slot; for DataTypes use optional fields, arrays-allowed-empty, or NULL-allowing fields to terminate recursion. Code generators choke on direct self-referencing Structures.
7.11 Standardized Instances
Mark server-bound standardized Variables (capability/diagnostics) as non-static in the NamespaceMetadata Object so aggregating servers behave correctly.
7.12 Predefined values — Enum / MultiState / NodeId / StateMachine
| Mechanism | Extensible? | When to use |
|---|---|---|
Enumeration DataType | ❌ | Truly closed set, never to be extended. |
Enum + Other value | ⚠️ | Avoid — leaves the actual value undiscoverable. |
MultiStateValueDiscreteType / MultiStateDiscreteType | ✅ | Default choice when extension or vendor-specific values likely. |
NodeId pointing to ObjectType (e.g. ConditionClassId) | ✅ | Need hierarchical specialization of the "enum". |
| StateMachine (sub-state machines) | ✅ | Need transitions / causes / effects / Method-driven changes. |
7.13 NodeIds & Namespaces
Prefer few namespaces with many Nodes over many tiny namespaces. When a Client adds Nodes to a Server, let the Server assign the NodeId.
8. Configuration changes that touch the AddressSpace
Recommendation: use Methods. Avoid the generic NodeManagement Services — they have no transactional semantics, no place-restrictions, and can't carry server-only side-data (e.g. fieldbus addresses).
| Pattern | When to use |
|---|---|
Class Method 0:Create on the TypeDefinition | Create instances generically; only works for Objects (not Vars). |
| Add/Remove Methods on the parent where the instance lives | Recommended for normal granular changes (e.g. AddWriterGroup). |
FileTransfer (subtype of FileType) | Large atomic changes (e.g. full PubSub config, recipe download). |
Writing a structuring Variable (e.g. NumberOfSubmodules) | Discouraged — can't pick which children, no config for new ones. |
9. Dictionary References
External dictionaries (ECLASS, IEC CDD) live in two reserved Namespaces (one for IRDIs, one for URIs). A Companion Spec referencing dictionary entries should ship a separate flat-list UANodeSet for those entries (PA-DIM does this). The server integrator merges the dictionary UANodeSets — or omits them if the Client knows them out-of-band.
10. Using Existing Companion Specifications
Reuse base specs to maximise interoperability. Useful starting points:
| Spec | Provides |
|---|---|
| OPC 10000-100 Devices | Device identification, health, params, modular devices, FW update. |
| OPC 10000-110 Asset Mgmt Basics | Asset id, discovery, capabilities, maintenance log. |
| OPC 10000-200 Industrial Automation | Stacklights, statistics, calibration target. |
| OPC 10031-4 ISA-95 Job Control | Job management. |
| OPC 30050 PackML | Generic ISA-88 state machine reused by many specs. |
| OPC 40001-1 Machinery | Identification, discovery, monitoring building blocks. |
Composition > inheritance. If you subtype a reused type and the upstream spec later adds a more useful subtype, you're stuck. With composition you can swap the contained type freely.
11. Extending a Released Companion Specification
Within the same NamespaceUri:
- InstanceDeclarations — add as
Optionalanywhere; for "mandatory" use either a subtype or a new ConformanceUnit/Profile. - A grouped set of additions → first define an Interface or AddIn, then deploy it.
- Enumeration values — not allowed; deprecate the entire enum and replace it (painful) — better: had used a MultiState type from the start.
- Structure fields — not allowed; create a subtype if (and only if) every usage site already permits subtypes.
- OptionSet bits — numeric: forbidden, create a new OptionSet; OptionSet-DataType: subtype is OK if length unchanged.
- Replacing a TypeDefinition — deprecate old + add new (e.g.
FooType2). Cascades into every site that used the old one.
For a breaking change, mint a new NamespaceUri ending in V<MajorVersion>/ (e.g. …/V2/). New Namespace ⇒ new ConformanceUnits & Profiles too.
12. ConformanceUnits & Profiles
- A ConformanceUnit is the smallest testable unit. Define them for both the model contents and the model behaviour (Method semantics, optional-feature constraints).
- Granularity tip: each important optional feature should have its own ConformanceUnit so derived models can require it without redefining.
- Split "TypeDefinition is present" from "at least one instance behaves correctly" — they're different testable claims.
- For configurable products, state requirements as "the product can be configured to support at least one instance of …".
- Profiles bundle ConformanceUnits. Domain-specific specs should ship at least one application Profile (model + protocol). Generic / horizontal specs may publish only Facets or ConformanceUnits, to be composed by downstream specs.
13. BrowseNames in Text
| Where | Format |
|---|---|
| Spec prose (PDF / web) | Include the NamespaceIndex: 0:EngineeringUnits. |
Description Attribute in UANodeSet | Omit the index — the runtime index will differ. |
| Rare ambiguity in UANodeSet text | Spell out the full NamespaceUri instead of the index. |
14. Deprecation
Mechanism: IsDeprecated ReferenceType from the Node to a marker Object indicating the version where deprecation began.
- For end clients: prefer non-deprecated Nodes; fall back to deprecated only for old-server compatibility.
- For Companion Specs, deprecation alone can't actually remove a Node — it stays in the Namespace forever. So:
- Small change (one optional InstanceDeclaration, niche types) → deprecate in place.
- Larger / fundamental change → mint a new NamespaceUri (see §11.3) — cleaner and unambiguous.
- Place the deprecation marker as a child of
NamespaceMetadata, BrowseName e.g.<SpecName>Version1_02(real example: OPC 40501-1 v1.02).
15. Audit Events
Use the existing AuditEventType hierarchy as-is — for example AuditUpdateMethodEventType for Method calls. Subtype only for filtering, never to add fields: real-world audit consumers don't process custom fields, and the base events already carry what's needed (e.g. InputArguments for Methods).
Quick Heuristics — When in Doubt
- Naming? PascalCase + correct suffix. If unsure, look up a similar existing type.
- Mandatory vs optional? Optional + Profile/ConformanceUnit beats Mandatory.
- New version of an existing spec? Try to make it backward-compatible per §3 — only mint a new NamespaceUri (
…/V2/) when truly breaking. - Enum-like list of values? Reach for
MultiStateValueDiscreteType, not a plain Enumeration DataType. - Adding an aspect to an existing type hierarchy? Composition (Interface / AddIn) over subtyping.
- Mutating the AddressSpace? Methods on the parent. Only fall back to FileTransfer for big atomic changes.
- Need a new StatusCode? No — use
Uncertain+ a trailingStatusInt32 output argument. - Reusing another spec's type? Compose it; don't subtype it (so you can swap in their future subtypes).