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

#TopicOne-liner
1ScopeNon-normative best-practice guide; not every server uses every concept.
2Naming ConventionsPascalCase everywhere; specific suffix rules per NodeClass.
3Backward CompatibilityWhat you may and may not change while keeping the same NamespaceUri.
4StatusCodesDon't invent your own β€” request additions to the base spec; use DiagnosticInfo.
5App-specific Method statusesAdd a trailing Status Int32 output arg + return Uncertain StatusCode.
6Creating a Companion SpecificationUse OPCF templates, NodeSet-Validator, define test cases early.
7Using Modelling ConceptsThe big chapter: ObjectTypes, VariableTypes, Methods, DataTypes, Events, References.
8Configuration changes to AddressSpacePrefer Methods over NodeManagement Services or writing Variables.
9Dictionary ReferencesShip a separate UANodeSet of dictionary entries; servers merge them.
10Using existing Companion SpecsPrefer composition over inheritance to survive future subtype additions.
11Extending an existing Companion SpecAdd optional only; for breaking changes use a new …/V2/ NamespaceUri.
12ConformanceUnits & ProfilesSmallest testable unit = ConformanceUnit; Profiles bundle them.
13BrowseNames in textUse 0:Name in spec prose; omit NamespaceIndex inside UANodeSet Description text.
14DeprecationIsDeprecated ReferenceType; for Companion Specs, prefer a new NamespaceUri instead.
15Audit eventsReuse 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

NodeClassSuffixExamples
ObjectTypeTypeServerType, BaseEventType
VariableTypeType (or VariableType if ambiguous)PropertyType, BaseDataVariableType
Structured DataTypeDataTypeRedundantServerDataType
Enumeration DataTypenone (or Enum)NodeClass, ServerState
Built-in / SimplenoneInt32, UtcTime
ReferenceTypenone β€” verb-basedHasComponent, Organizes; inverse e.g. ComponentOf
Object/Variable/Method/Viewnone β€” 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 (Id vs ID is illegal β€” use NetworkId/DeviceId).
  • BrowseNames of TypeDefinitions must be unique within their Namespace; avoid clashing with names from the base spec.
  • UANodeSet filenames: Opc.Ua.<ShortName>.NodeSet2.xml for Companion Specs, *NodeSet2.xml for 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 / OptionalPlaceholder InstanceDeclarations.
  • 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 IsAbstract from true β†’ false (never the reverse).

Forbidden without a new Namespace

  • Adding Mandatory / MandatoryPlaceholder InstanceDeclarations.
  • 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, or ArrayDimensions (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

  1. Subtype + new ConformanceUnit/Profile that requires it.
  2. Add optional members + ConformanceUnit/Profile making them mandatory.
  3. 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 DataTypeDictionary Nodes in your UANodeSet for compatibility with old apps even though the modern way is DataTypeDefinition.
  • 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 Placeholder ModellingRule.
  • 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 Mandatory with a sensible default if the value is at least writeable; use Optional if the server simply can't provide it. Avoid mandatory-but-returns-Bad pattern.
  • Placeholder combinations (grouping Object Γ— placeholder rule):
    GroupingPlaceholderUse when
    MandatoryMandatoryPlaceholderAt least one child of this type required.
    MandatoryOptionalPlaceholderGroup has its own functionality (e.g. an Add Method).
    OptionalMandatoryPlaceholderGroup only appears when at least one child exists.
    OptionalOptionalPlaceholderFuture 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.
  • 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 Create factory).

7.5 Granularity of structured data β€” five choices

For something like an IP configuration:

ApproachTransactionalGeneric-client friendlyPer-field metadataSubscribe 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:
      1. Default values (incl. NULL) instead of optional fields.
      2. Two nested sub-structures: OptionalFields and SubtypableFields.
      3. Use BaseDataType / Structure for fields that need subtyping (loses type safety).
      4. Use a length-0/1 array as an "optional" scalar.
    • Max 32 optional fields per Structure.

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

MechanismExtensible?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).

PatternWhen to use
Class Method 0:Create on the TypeDefinitionCreate instances generically; only works for Objects (not Vars).
Add/Remove Methods on the parent where the instance livesRecommended 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:

SpecProvides
OPC 10000-100 DevicesDevice identification, health, params, modular devices, FW update.
OPC 10000-110 Asset Mgmt BasicsAsset id, discovery, capabilities, maintenance log.
OPC 10000-200 Industrial AutomationStacklights, statistics, calibration target.
OPC 10031-4 ISA-95 Job ControlJob management.
OPC 30050 PackMLGeneric ISA-88 state machine reused by many specs.
OPC 40001-1 MachineryIdentification, 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 Optional anywhere; 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

WhereFormat
Spec prose (PDF / web)Include the NamespaceIndex: 0:EngineeringUnits.
Description Attribute in UANodeSetOmit the index β€” the runtime index will differ.
Rare ambiguity in UANodeSet textSpell 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

  1. Naming? PascalCase + correct suffix. If unsure, look up a similar existing type.
  2. Mandatory vs optional? Optional + Profile/ConformanceUnit beats Mandatory.
  3. New version of an existing spec? Try to make it backward-compatible per Β§3 β€” only mint a new NamespaceUri (…/V2/) when truly breaking.
  4. Enum-like list of values? Reach for MultiStateValueDiscreteType, not a plain Enumeration DataType.
  5. Adding an aspect to an existing type hierarchy? Composition (Interface / AddIn) over subtyping.
  6. Mutating the AddressSpace? Methods on the parent. Only fall back to FileTransfer for big atomic changes.
  7. Need a new StatusCode? No β€” use Uncertain + a trailing Status Int32 output argument.
  8. Reusing another spec's type? Compose it; don't subtype it (so you can swap in their future subtypes).