Skip to main content

VariableTypes

In OPC UA, VariableTypes serve as reusable blueprints for variables. They define the data type, value rank, default value, and nested metadata properties that any instance of this variable type must carry.

This guide explains how to define custom VariableTypes, instantiate them within ObjectTypes, and configure optional properties using qualified-name path segment rules.


1. Defining a Variable Type

Custom VariableTypes are declared under the top-level variableTypes section in your YAML model.

Defining MyVariableType
variableTypes:
- browseName: MyVariableType
dataType: ua:Double
value: 0

At minimum, a variable type declaration requires:

  • browseName: The unique identifier of your variable type (by convention, suffixed with Type).
  • dataType: The underlying OPC UA data type (e.g. ua:Double, ua:String, or a custom DataType).
  • value (optional): A default initial value for instances of this type.

Variable Attributes & Metadata

A VariableType describes the shape of the values its instances hold:

Configuring variable attributes
variableTypes:
- browseName: MySensorValueType
dataType: ua:Double
valueRank: 1 # Array of any size
accessLevel is not a VariableType attribute

accessLevel and minimumSamplingInterval describe a Variable, not the type it is typed by. Written on a variableTypes: entry they are rejected by the schema and ignored by the compiler — set them on the member instead:

Attributes that belong on the variable
objectTypes:
- browseName: MyInstrumentType
components:
# accessLevel and minimumSamplingInterval describe a VARIABLE, so they
# belong on the member — not on the VariableType it is typed by
- browseName: Reading
dataType: ua:Double
accessLevel: CurrentRead | CurrentWrite | HistoryRead
minimumSamplingInterval: 100 # fastest sampling rate, in milliseconds

Here is a breakdown of the supported attributes:

  • valueRank: Determines if the variable holds a single value, an array, or a multi-dimensional matrix:

    • -1 (Scalar): Single value (default if omitted).
    • 1 (OneDimension): A one-dimensional array of any size.
    • 2 (TwoDimensions): A two-dimensional matrix.
    • 0 (OneOrMoreDimensions) / -2 (Any): Flexible array dimensions.
  • arrayDimensions: A list of integers specifying the exact size of each dimension (mandatory for multi-dimensional matrices, e.g. [2, 3] for a 2x3 matrix).

  • accessLevel (on a variable, not on the type): A bitwise flag string that defines client permissions. Multiple flags are combined with |:

    • CurrentRead: Clients can read the current value.
    • CurrentWrite: Clients can write the current value.
    • HistoryRead: Access to historical data.
    • HistoryWrite: Ability to modify historical data.
    • StatusWrite: Ability to write status codes.
  • minimumSamplingInterval (on a variable, not on the type): Specifies the fastest rate (in milliseconds) at which the server should sample this variable's value.


2. Instantiating a Variable Type

Once a VariableType is defined, you can instantiate it as a component or property inside an ObjectType declaration by referencing its name in the typeDefinition property.

Instantiating inside an ObjectType
objectTypes:
- browseName: MyObjectType
# `components:`, not `properties:` — a Property must be a ua:PropertyType,
# so a member typed by a custom VariableType is a component
components:
- browseName: MyVariable
typeDefinition: MyVariableType
modellingRule: Mandatory # or Optional
Automatic Inference

The OPC UA Modeler automatically infers that MyVariable is a Variable node because its typeDefinition points to a registered VariableType.


3. Optional Properties & Exposing Optionals

VariableTypes can have their own nested metadata (like engineering units, ranges, or custom properties).

Any property or component declared on a type with modellingRule: Optional is not instantiated by default. An instantiating node must explicitly opt-in to inherit it by listing the target property in its optionals: list.

A VariableType with an Optional property
# a VariableType with an Optional property — MyOptionalVariableType is a
# VARIABLE type, so it belongs here and not under objectTypes:
- browseName: MyOptionalVariableType
dataType: ua:Double
value: 0
properties:
- browseName: Units
dataType: ua:String
modellingRule: Optional
Instantiating it and opting in
instances:
- browseName: MyObject
typeDefinition: ua:BaseObjectType
organizedBy: /ua:Objects
components:
- browseName: MyVariable
typeDefinition: MyOptionalVariableType
optionals:
- Units

4. The optionals: List — Qualified-Name Path Segment Rules

Each entry in the optionals: block is a path through the type hierarchy of the instance's typeDefinition.

[!IMPORTANT] Strict Qualified BrowseNames (v4.0.0+) Every segment of an optional path must be fully qualified with the namespace alias of the namespace that declares it. The alias is omitted only when the segment belongs to your model's own namespace. Earlier modeler versions tolerated bare names because they matched on leaf names; the current compiler resolves paths segment-by-segment and will reject unqualified paths.

Here is how to resolve paths for different namespace scenarios:

A. Single-Segment Optionals (Foreign Namespace)

When the optional property is declared directly on the base type from a foreign namespace, prefix it with that namespace's alias (e.g., di: for Device Integration):

Single-segment optional properties
# di:DeviceType declares SerialNumber (Optional) and Manufacturer (Optional)
- browseName: Axis1
# di:DeviceType is abstract; instantiate a concrete subtype of it
typeDefinition: MyDeviceType
organizedBy: /ua:Objects
optionals:
- di:SerialNumber # qualified — required for foreign-namespace segments
- di:Manufacturer

B. Nested / Deep Optionals

When an optional property is nested inside intermediate components, list the full path segment-by-segment.

  • Separate segments using a dot (.).
  • Each individual segment must carry its own namespace alias if it belongs to a foreign namespace.
The type declaring the nested optional
# di:DeviceType declares ParameterSet (in the 'di' namespace), and
# our MyDeviceType adds an Optional Temperature inside ParameterSet.
- browseName: MyDeviceType
subtypeOf: di:DeviceType
components:
- browseName: di:ParameterSet
components:
- browseName: Temperature
dataType: ua:Double
modellingRule: Optional
Nested / deep optional path
instances:
- browseName: DeviceA
typeDefinition: MyDeviceType
organizedBy: /ua:Objects
optionals:
- di:ParameterSet.Temperature # segments joined with dots, each qualified appropriately

[!CAUTION] Namespace Colon vs Path Dot The colon (:) separates a namespace alias from its browseName within a single segment. It is not a path separator.

  • Correct: di:ParameterSet.Temperature
  • Malformed: di:ParameterSet:Temperature

C. Own-Namespace Optionals

For elements declared inside your own model, you can either omit the namespace prefix entirely (the own: prefix is implicit) or explicitly write own::

The type declaring it
objectTypes:
- browseName: MyDeviceOwnType
subtypeOf: di:DeviceType
properties:
# a name of our OWN, deliberately not one di:DeviceType already declares —
# the lesson here is the missing prefix, not shadowing
- browseName: AssetTag
dataType: ua:String
modellingRule: Optional
Own-namespace optional segments
instances:
- browseName: Device1
typeDefinition: MyDeviceOwnType
organizedBy: /ua:Objects
optionals:
- AssetTag # own namespace, alias optional
- own:AssetTag # equivalent

Troubleshooting Common Mistakes

If an optional path is malformed, the modeler's compiler or LSP diagnostics will output a helpful warning:

Symptom (warning text)CauseFix
Optional "Foo" is not a valid child of type "BarType". It will be ignoredMissing namespace alias on a foreign-namespace segmentPrefix with the alias declaring Foo (e.g. robotics:Foo).
did you mean "di:ParameterSet.robotics:ActualSpeed" ?Bare leaf name; the compiler resolved a fuzzy matchCopy/paste the suggested fully qualified path from the warning.
Optional "X" is not marked as optional in the type definitionThe targeted node is already Mandatory in the type definitionRemove from optionals: (it is already instantiated automatically).

Full example

📄 Full working example — variable-types.model.yaml
variable-types.model.yaml
# yaml-language-server: $schema=../../../../opcua-modeler-ex/schemas/nodeset2.schema.json
#
# One document, each top-level section appearing exactly once. A concept that
# needs both a type and an instance carries a `-type` and an `-instance` tag
# rather than repeating a section key — repeating them is what left an earlier
# version of this file with three `objectTypes:` keys, and therefore unloadable.
#
namespaceUri: http://sterfive.com/UA/doc-examples/variable-types/
version: 1.0.0
publicationDate: "2026-05-26T00:00:00Z"

namespaces:
- di

variableTypes:
- browseName: MyVariableType
dataType: ua:Double
value: 0

- browseName: MySensorValueType
dataType: ua:Double
valueRank: 1 # Array of any size

# a VariableType with an Optional property — MyOptionalVariableType is a
# VARIABLE type, so it belongs here and not under objectTypes:
- browseName: MyOptionalVariableType
dataType: ua:Double
value: 0
properties:
- browseName: Units
dataType: ua:String
modellingRule: Optional

objectTypes:
- browseName: MyObjectType
# `components:`, not `properties:` — a Property must be a ua:PropertyType,
# so a member typed by a custom VariableType is a component
components:
- browseName: MyVariable
typeDefinition: MyVariableType
modellingRule: Mandatory # or Optional

- browseName: MyInstrumentType
components:
# accessLevel and minimumSamplingInterval describe a VARIABLE, so they
# belong on the member — not on the VariableType it is typed by
- browseName: Reading
dataType: ua:Double
accessLevel: CurrentRead | CurrentWrite | HistoryRead
minimumSamplingInterval: 100 # fastest sampling rate, in milliseconds

# di:DeviceType declares ParameterSet (in the 'di' namespace), and
# our MyDeviceType adds an Optional Temperature inside ParameterSet.
- browseName: MyDeviceType
subtypeOf: di:DeviceType
components:
- browseName: di:ParameterSet
components:
- browseName: Temperature
dataType: ua:Double
modellingRule: Optional

- browseName: MyDeviceOwnType
subtypeOf: di:DeviceType
properties:
# a name of our OWN, deliberately not one di:DeviceType already declares —
# the lesson here is the missing prefix, not shadowing
- browseName: AssetTag
dataType: ua:String
modellingRule: Optional

instances:
- browseName: MyObject
typeDefinition: ua:BaseObjectType
organizedBy: /ua:Objects
components:
- browseName: MyVariable
typeDefinition: MyOptionalVariableType
optionals:
- Units

# di:DeviceType declares SerialNumber (Optional) and Manufacturer (Optional)
- browseName: Axis1
# di:DeviceType is abstract; instantiate a concrete subtype of it
typeDefinition: MyDeviceType
organizedBy: /ua:Objects
optionals:
- di:SerialNumber # qualified — required for foreign-namespace segments
- di:Manufacturer

- browseName: DeviceA
typeDefinition: MyDeviceType
organizedBy: /ua:Objects
optionals:
- di:ParameterSet.Temperature # segments joined with dots, each qualified appropriately

- browseName: Device1
typeDefinition: MyDeviceOwnType
organizedBy: /ua:Objects
optionals:
- AssetTag # own namespace, alias optional
- own:AssetTag # equivalent
📄 Generated NodeSet2.xml — variable-types.model.Nodeset2.xml
variable-types.model.Nodeset2.xml
# ⚠ File not found: ./_examples/variable-types.model.Nodeset2.xml