> For the complete documentation index, see [llms.txt](https://docs.cybus.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.cybus.io/connectors/shop-floor-connectors/opc-ua/opc-ua-client.md).

# OPC UA Client

Configure Connectware as an OPC UA client to exchange data with OPC UA servers on your industrial equipment.

This page describes how Connectware can act as an OPC UA client. Connectware can also act as an [OPC UA server](/connectors/servers/opc-ua-server.md).

{% hint style="info" %}
For more information on OPC UA, see the [OPC Foundation Reference](https://reference.opcfoundation.org/).
{% endhint %}

For configuration reference, see:

* [Connection Properties](/connectors/shop-floor-connectors/opc-ua/opc-ua-client/opcuaconnection.md)
* [Endpoint Properties](/connectors/shop-floor-connectors/opc-ua/opc-ua-client/opcuaendpoint.md)

## Node IDs

You identify each OPC UA node by its `nodeId` or its `browsePath`. NodeIds follow the string encoding rules from the OPC UA specification (see [OPC 10000-6, section 5.1.12](https://reference.opcfoundation.org/specs/OPC-10000-6/5.1.12)).

### NodeId Formats

Connectware supports three forms of NodeIds. Each form applies to `read`, `write`, and `subscribe` endpoints.

| Form            | Syntax                             | Example                                            |
| --------------- | ---------------------------------- | -------------------------------------------------- |
| Identifier only | `<identifier>`                     | `i=2258`                                           |
| Namespace index | `ns=<namespaceIndex>;<identifier>` | `ns=3;s=FastUInt2`                                 |
| Namespace URI   | `nsu=<namespaceURI>;<identifier>`  | `nsu=http://microsoft.com/Opc/OpcPlc/;s=FastUInt2` |

The identifier-only form refers to a node in the default namespace (namespace index `0`).

Use the namespace URI form when the namespace index of a server may change between deployments or restarts. Connectware resolves the URI to the current namespace index at runtime by querying the server's namespace array. The URI stays stable across server configuration changes, whereas the index depends on the server's namespace registration order.

### Identifier Types

The `<identifier>` component uses a type prefix followed by the value:

| Prefix | Type    | Example                                  |
| ------ | ------- | ---------------------------------------- |
| `i=`   | Numeric | `i=2258`                                 |
| `s=`   | String  | `s=TemperatureSensor/Output`             |
| `g=`   | GUID    | `g=09087e75-8e5e-499b-954f-f2a9603db28a` |
| `b=`   | Opaque  | `b=M/RbKBsRVkePCePcx24oRA==`             |

Opaque identifiers are Base64-encoded byte strings.

## OPC UA Subscriptions

OPC UA distinguishes between sessions, subscriptions, and monitored items.

* Each Connectware connection creates exactly one session in the OPC UA terminology, where Connectware acts as a client towards the server.
* Within one session, the set of data endpoints are grouped into subscriptions. A subscription carries parameters like `publishInterval` and `maxNotificationsPerPublish` that control the data flow between server and client.
* Within each subscription, the actual data endpoints (also called nodes) are subscribed as monitored items. Monitored items are configured by parameters like `nodeId`, `attributeId`, and `samplingInterval`, which control how the server collects the data. Each network request for creating these can create one or multiple monitored items per request.

## Connectware Subscriptions

Connectware abstracts this complexity by combining as many endpoints as possible into the same subscription and grouping monitored items creation into the same request. This is done according to the following criteria:

* Each OPC UA `Cybus::Connection` resource creates exactly one session.
* Within this session, all OPC UA `Cybus::Endpoint` resources with the same `publishInterval` property are combined into one subscription. This holds even across different service commissioning files using inter-service referencing, see [here](/data-flows/service-commissioning-files/resources.md#resource-id). Therefore, enabling or disabling additional services with additional endpoints will add or remove those endpoints from the currently available subscription.
* Every OPC UA `Cybus::Endpoint` resource corresponds to one monitored item
* Within one subscription and within the same service commissioning file, all endpoints (monitored items) with the same `samplingInterval` property are combined into one common creation request called `CreateMonitoredItemsGroupRequest`. This request works efficiently even for thousands of monitored items.

When endpoints are combined into one subscription, the subscription-level properties (`requestedLifetimeCount`, `requestedMaxKeepAliveCount`, `maxNotificationsPerPublish`, and `priority`) are taken from the first endpoint. Differing values on subsequent endpoints are ignored.

### Server Limits on Monitored Items

Some OPC UA servers are known for imposing certain limits on the number of monitored items, either in total, or per subscription, or per request.

In particular, an embedded OPC UA server on a Siemens S7-1500 or S7-1200 PLC is known for certain restrictions, see [System limits of OPC UA Server](https://support.industry.siemens.com/cs/document/109755846/what-are-the-system-limits-of-the-opc-ua-server-with-s7-1500-and-s7-1200-?dti=0\&dl=en\&lc=de-DE).

* For example, the maximum number of monitored items that can be created in one request might be limited to 1000 nodes. Many servers advertise this limit through a capability node (`nodeId` `ns=0;i=11714`). Connectware reads this value during connection setup and splits larger requests automatically. See [Setting the Maximum Monitored Items per Call](#setting-the-maximum-monitored-items-per-call).
* Additionally, a maximum number of monitored items in total might be configured in the TIA Portal project. Unfortunately, there is no known way to look up this value except directly in TIA Portal. If such a value is set, Connectware cannot create more endpoints than this limit.

#### Setting the Maximum Monitored Items per Call

The `maxMonitoredItemsPerCall` property on the `Cybus::Connection` resource caps how many monitored items Connectware creates per request.

* **Unset or `0` (default):** Connectware matches the server's limit. If the server does not report a limit, Connectware creates all monitored items in a single request.
* **Set explicitly:** Connectware uses your value. If your value exceeds the server's limit, Connectware uses the server's limit instead and logs a warning. Setting a value is useful when the server does not report a limit.

The same split applies on reconnect and resubscription. Each automatic split is written to the debug log.

The following example sets the connection to create at most 1000 monitored items per request:

{% code lineNumbers="true" %}

```yaml
resources:
  opcuaConnection:
    type: Cybus::Connection
    properties:
      protocol: Opcua
      connection:
        host: !ref opcuaHost
        port: !ref opcuaPort
        maxMonitoredItemsPerCall: 1000
```

{% endcode %}

### Example

This is an example configuration snippet with three endpoints (without the connection configuration):

{% code lineNumbers="true" expandable="true" %}

```yaml
resources:
  spindleSpeed:
    type: Cybus::Endpoint
    properties:
      protocol: Opcua
      connection: !ref opcuaConnection
      subscribe:
        nodeId: 'ns=1;s=spindleSpeed'
        publishInterval: 1000
        samplingInterval: 100
        maxNotificationsPerPublish: 100
  powerConsumption:
    type: Cybus::Endpoint
    properties:
      protocol: Opcua
      connection: !ref opcuaConnection
      subscribe:
        nodeId: 'ns=1;s=powerConsumption'
        publishInterval: 1000
        maxNotificationsPerPublish: 50
        samplingInterval: 1000
  temperature:
    type: Cybus::Endpoint
    properties:
      protocol: Opcua
      connection: !ref opcuaConnection
      subscribe:
        nodeId: 'ns=1;s=temperature'
        publishInterval: 15000
        samplingInterval: 15000
```

{% endcode %}

In the above example, two subscriptions will be created. One with `publishInterval` set to 1000ms and `maxNotificationsPerPublish` set to 100, and another with `publishInterval` set to 15000ms. The sampling of the individual source values will be set as expected by the specified `samplingInterval` property, but remember that OPC UA does not offer fixed data sampling but rather applies a change-of-value filter to each data point automatically.

## Service Commissioning File Example

### Basic Example

This is a simple OPC UA connectivity example that only subscribes to the `ServerStatus.State` node (`ns=0;i=2259`) of an OPC UA server. The proposed node (endpoint) can also be used if the server would otherwise close the connection, which has been observed for some specific versions of OPC UA servers on a S7 PLC.

The example will need some OPC UA server available in your network:

{% file src="/files/z0dVJbuxlvoFtd5ATmK5" %}

{% code title="opcua-simple-example.yml" lineNumbers="true" expandable="true" %}

```yaml
---
description: >

  Simple OPC UA connectivity example

metadata:
  name: Simple OPC UA connectivity
  version: 1.0.0
  provider: cybus
  homepage: https://www.cybus.io

parameters:
  opcuaHost:
    type: string
    description: OPC-UA Host
    default: 172.17.0.1

  opcuaPort:
    type: integer
    default: 4840

  opcuaUser:
    type: string
    default: ''

  opcuaPass:
    type: string
    default: ''

resources:
  opcuaConnection:
    type: Cybus::Connection
    properties:
      protocol: Opcua
      connection:
        host: !ref opcuaHost
        port: !ref opcuaPort
        username: !ref opcuaUser
        password: !ref opcuaPass

  serverStatusEndpoint:
    type: Cybus::Endpoint
    properties:
      protocol: Opcua
      connection: !ref opcuaConnection
      subscribe:
        nodeId: ns=0;i=2259
        samplingInterval: 2000
```

{% endcode %}

### Advanced Example

More complex example including an OPC UA server container from public Docker Hub:

{% file src="/files/1AvYd6S3WGdguWqKM0UO" %}

{% code title="opcua-example.yml" lineNumbers="true" expandable="true" %}

```yaml
---
description: >

  Simulated OPC UA server

metadata:
  name: Simulated OPC UA
  version: 1.0.0
  provider: cybus
  homepage: https://www.cybus.io

parameters:
  opcuaHost:
    type: string
    description: OPC-UA Host
    default: 172.17.0.1

  opcuaPort:
    type: integer
    default: 50000

  opcuaUser:
    type: string
    default: user

  opcuaPass:
    type: string
    default: user

definitions:
  CYBUS_MQTT_ROOT: cybus-factory/opcua

resources:
  machineSimulatorContainer:
    type: Cybus::Container
    properties:
      image: mcr.microsoft.com/iotedge/opc-plc
      ports:
        - !sub '${opcuaPort}:50000/tcp'
      command:
        - --unsecuretransport
        - --autoaccept
        - --defaultuser=user
        - --defaultpassword=user

  opcuaConnection:
    type: Cybus::Connection
    properties:
      protocol: Opcua
      connection:
        host: !ref opcuaHost
        port: !ref opcuaPort
        username: !ref opcuaUser
        password: !ref opcuaPass

  currentTime:
    type: Cybus::Endpoint
    properties:
      protocol: Opcua
      connection: !ref opcuaConnection
      topic: server/status/currenttime
      subscribe:
        nodeId: i=2258

  dipData:
    type: Cybus::Endpoint
    properties:
      protocol: Opcua
      connection: !ref opcuaConnection
      topic: dip-data
      subscribe:
        nodeId: ns=2;s=DipData

  alternatingBool:
    type: Cybus::Endpoint
    properties:
      protocol: Opcua
      connection: !ref opcuaConnection
      topic: alternating-bool
      subscribe:
        nodeId: ns=2;s=AlternatingBoolean

  negativeTrendData:
    type: Cybus::Endpoint
    properties:
      protocol: Opcua
      connection: !ref opcuaConnection
      topic: negative-trend-data
      subscribe:
        nodeId: ns=2;s=NegativeTrendData

  positiveTrendData:
    type: Cybus::Endpoint
    properties:
      protocol: Opcua
      connection: !ref opcuaConnection
      topic: positive-trend-data
      subscribe:
        nodeId: ns=2;s=PositiveTrendData

  randomSignedInt:
    type: Cybus::Endpoint
    properties:
      protocol: Opcua
      connection: !ref opcuaConnection
      topic: random-signed-int32
      subscribe:
        nodeId: ns=2;s=RandomSignedInt32

  randomUnsignedInt:
    type: Cybus::Endpoint
    properties:
      protocol: Opcua
      connection: !ref opcuaConnection
      topic: random-unsigned-int32
      subscribe:
        nodeId: ns=2;s=RandomUnsignedInt32

  spikeData:
    type: Cybus::Endpoint
    properties:
      protocol: Opcua
      connection: !ref opcuaConnection
      topic: spike-data
      subscribe:
        nodeId: ns=2;s=SpikeData

  stepUp:
    type: Cybus::Endpoint
    properties:
      protocol: Opcua
      connection: !ref opcuaConnection
      topic: step-up
      subscribe:
        nodeId: ns=2;s=StepUp
```

{% endcode %}

### Example with Method Endpoint

{% file src="/files/C4cg4Jc1fWYUT8kM3pxk" %}

{% code title="opcua-client-method-example.yml" lineNumbers="true" expandable="true" %}

```yaml
description: OPC UA method endpoint

metadata:
  name: opcua-method-endpoint
  version: 1.0.0

resources:
  opcuaConnection:
    type: Cybus::Connection
    properties:
      protocol: Opcua
      connection: # opc.tcp://opcuaserver.com:48010
        host: opcuaserver.com
        port: 48010

  multiplyEndpoint:
    type: Cybus::Endpoint
    properties:
      protocol: Opcua
      connection: !ref opcuaConnection
      publishError: true # will let the endpoint always publish an error object that contains 'message' and error 'code' when an error occurs
      write:
        nodeId: ns=2;s=Demo.Method.Multiply
        nodeType: Method # mark that endpoint as method
```

{% endcode %}

## Output Format on Reading

Subscribe endpoints and read endpoints publish values in different formats.

### Subscribe Endpoints

A `subscribe` endpoint publishes every value change as a JSON object with `timestamp` and `value`:

```json
{ "timestamp": 1629351968526, "value": 21.5 }
```

`timestamp` is the OPC UA [source timestamp](https://reference.opcfoundation.org/v104/Core/docs/Part4/7.7.3/), which the data source sets, in milliseconds since the Unix epoch.

{% hint style="info" %}
If 64-bit integers are used (which are unsupported in JSON but are supported in JavaScript by the BigInt class), the value is returned as a string containing the decimal number.
{% endhint %}

### Read Endpoints

A `read` endpoint with a `nodeId` or `browsePath` reads the node once for every message on its `/req` topic. Connectware publishes the OPC UA data value on the `/res` topic of the endpoint, as the server returns it. For the result of a batch endpoint, see [Reading a Batch](#reading-a-batch).

```json
{
  "value": { "dataType": "Double", "arrayType": "Scalar", "value": 21.5 },
  "statusCode": { "value": 0 },
  "sourceTimestamp": "2026-07-22T12:40:41.623Z",
  "sourcePicoseconds": 662600000,
  "serverTimestamp": "2026-07-22T12:40:41.623Z",
  "serverPicoseconds": 662600000
}
```

* `value`: The value of the node, with its OPC UA data type and array type.
* `statusCode`: The OPC UA status code of the value. `0` means Good.
* `sourceTimestamp` and `serverTimestamp`: When the data source produced the value and when the OPC UA server received it, as ISO 8601 date strings.
* `sourcePicoseconds` and `serverPicoseconds`: The part of each timestamp below one millisecond, in picoseconds.

The result is the data value alone, with no separate `timestamp` or `id` property.

If the server cannot read the node, for example because the node does not exist, `statusCode` contains a Bad status code. If the read fails as a whole, Connectware publishes an empty message on the `/res` topic (see [Failed Operations](/data-flows/service-commissioning-files/resources/cybus-endpoint.md#failed-operations)).

## Output Format on Writing

### Variables

When you write to an OPC UA variable through an endpoint with a `nodeId` or `browsePath`, Connectware publishes the result on the `/res` topic of the endpoint. For the result of a batch endpoint, see [Writing a Batch](#writing-a-batch).

```json
{ "id": "29194", "timestamp": 1629351968526, "result": { "value": 0 } }
```

* `id`: The `id` from your message, returned unchanged. If your message has no `id`, the result has none either.
* `timestamp`: When Connectware received the write result, in milliseconds since the Unix epoch.
* `result`: The OPC UA status code of the write in `result.value`. `0` means Good, and a Bad status code means the write failed.

If Connectware cannot carry out the write, for example because the value does not match the data type of the node, `result.value` contains the matching Bad status code. If you set [`publishError`](/data-flows/service-commissioning-files/resources/cybus-endpoint.md#publisherror) to `true` on the endpoint, the result then also contains an `error` object with a `message` and the same status code in `code`.

### Method Calls

Method calls follow the same pattern as variable operations:

1. Write the method call data to the `/set` topic.
2. Receive the result on the `/res` topic.

`result.value` is the OPC UA status code of the method call, and `result.outputArguments` lists the output arguments, if the method returns any. The `id` and the `error` object work the same way as for variables.

**Example**

```json
{
  "id": "12345",
  "timestamp": 1738323576339,
  "result": {
    "value": 0,
    "outputArguments": [
      {
        "dataType": "Double",
        "arrayType": "Scalar",
        "value": 48
      }
    ]
  }
}
```

## Input Format

### Variables

When writing to an OPC UA variable, format the data as a JSON object with a `value`. You can optionally include an `id`, which Connectware returns in the result.

{% code lineNumbers="true" %}

```json
{ "id": "29194", "value": 42 }
```

{% endcode %}

### Method Calls

Calling OPC UA methods follows the same pattern as writing to OPC UA variables.

The `value` has to include the input arguments like this. You can optionally include an `id` parameter.

{% code lineNumbers="true" %}

```json
{
  "id": "12345",
  "value": {
    "inputArguments": [
      {
        "value": 24
      },
      {
        "value": 2
      }
    ]
  }
}
```

{% endcode %}

## Batch Reading and Writing

With a batch endpoint, you read or write many OPC UA nodes with one endpoint and one MQTT message, instead of one endpoint per node. Connectware returns one result with the status of every node.

### Setting Up a Batch Endpoint

In a `read` or `write` endpoint, set `batch` instead of `nodeId` or `browsePath`. Batch endpoints are not available for `subscribe`. You can set up the nodes in either of the following ways:

* **Fixed list of nodes:** List the nodes in `batch.nodeIds`. Write messages then contain only the values, in the same order, and read requests contain no nodes.
* **Nodes in each message:** Leave `batch` empty. Each message names the nodes to read or write.

Each NodeId can appear only once in a batch.

The following example sets up a write endpoint with a fixed list of nodes and a read endpoint with nodes in each message:

{% code lineNumbers="true" %}

```yaml
resources:
  setpointsBatchWrite:
    type: Cybus::Endpoint
    properties:
      protocol: Opcua
      connection: !ref opcuaConnection
      write:
        batch:
          nodeIds:
            - 'ns=2;s=Setpoint1'
            - 'ns=2;s=Setpoint2'

  sensorsBatchRead:
    type: Cybus::Endpoint
    properties:
      protocol: Opcua
      connection: !ref opcuaConnection
      read:
        batch:
```

{% endcode %}

### Writing a Batch

Publish the batch to the `/set` topic of the endpoint. For the example endpoint, this is `services/${SERVICE_ID}/setpointsBatchWrite/set` by default, where `${SERVICE_ID}` is the [ServiceID](/data-flows/services/serviceid.md) of your service (see [Operation Results](/data-flows/service-commissioning-files/resources/cybus-endpoint.md#operation-results)). Put the values in a `batch` array. You can add an `id`, which Connectware returns in the result.

With a fixed list of nodes, the `batch` array contains one value per node, in the same order:

{% code lineNumbers="true" %}

```json
{ "id": "write-1", "batch": [42, 17.5] }
```

{% endcode %}

To write a structured value with a fixed list of nodes, wrap it in `{ "value": ... }`. Connectware treats every object with a `value` property as such a wrapper, so an unwrapped structure such as `{ "value": 42, "unit": "C" }` writes only `42`.

With nodes in each message, every item contains a `nodeId` and a `value`. A structured value goes directly into `value`, without a wrapper:

{% code lineNumbers="true" %}

```json
{
  "id": "write-2",
  "batch": [
    { "nodeId": "ns=2;s=Setpoint1", "value": 42 },
    { "nodeId": "ns=2;s=Setpoint2", "value": 17.5 }
  ]
}
```

{% endcode %}

Connectware publishes the result on the `/res` topic. `result` lists every node in the order of your message, each with its numeric OPC UA status code, where `0` means Good.

```json
{
  "timestamp": 1790762400000,
  "result": [
    { "nodeId": "ns=2;s=Setpoint1", "statusCode": 0 },
    { "nodeId": "ns=2;s=Setpoint2", "statusCode": 0 }
  ],
  "id": "write-2"
}
```

Connectware writes each node independently, so a node that fails does not stop the others. A node that was not written has a Bad status code. Reasons include the following:

* **Wrong data type:** The value does not match the data type of the node. Correct the value and send it again.
* **Connection dropped:** The batch was split into several requests (see [Batch Size Limits](#batch-size-limits)), and the connection dropped after at least one of them had completed. The nodes from the completed requests keep their status codes, and the result also contains an `error`. Send the nodes with a Bad status code again.

With a fixed list of nodes, send the whole batch again, because every message must contain one value per node.

If the message itself is invalid, Connectware writes nothing and publishes an error. A message is invalid, for example, in the following cases:

* `batch` is not an array.
* A NodeId appears twice.
* With a fixed list of nodes, the number of values does not match `batch.nodeIds`, or an item contains a `nodeId`.
* With nodes in each message, an item has no `nodeId` or no `value`.

If `batch` is not an array, for example, the result looks like this:

```json
{
  "id": "write-2",
  "timestamp": 1790762400000,
  "result": { "value": 2158690304 },
  "error": { "message": "Batch write data must be an array", "code": 2158690304 }
}
```

If the first request of a batch fails, for example because the connection dropped, Connectware has written none of the nodes. The result then has the same format as for an invalid message, with the status code of the failure in `result.value` and `error.code`, and no per-node list. Send the whole batch again.

### Reading a Batch

Publish a request to the `/req` topic of the endpoint. For the example endpoint, this is `services/${SERVICE_ID}/sensorsBatchRead/req` by default. Connectware reads the batch once for each request, so to read on a schedule, publish requests at that interval. You can add an `id`, which Connectware returns in the result. With a fixed list of nodes, send an empty object or only an `id`. With nodes in each message, list the nodes in `nodeIds`:

{% code lineNumbers="true" %}

```json
{ "id": "read-1", "nodeIds": ["ns=2;s=Temperature", "ns=2;s=Pressure"] }
```

{% endcode %}

Connectware publishes the values on the `/res` topic, in a different format from a single-node read. `values` lists every node in the order of your request, each with its value, status code, and timestamps:

```json
{
  "values": [
    {
      "nodeId": "ns=2;s=Temperature",
      "value": { "dataType": "Double", "arrayType": "Scalar", "value": 21.5 },
      "statusCode": 0,
      "serverPicoseconds": 438400000,
      "serverTimestamp": "2026-09-30T10:00:00.797Z",
      "sourcePicoseconds": 67200000,
      "sourceTimestamp": "2026-09-30T10:00:00.795Z"
    },
    {
      "nodeId": "ns=2;s=Pressure",
      "value": { "dataType": "Double", "arrayType": "Scalar", "value": 1.013 },
      "statusCode": 0,
      "serverPicoseconds": 440400000,
      "serverTimestamp": "2026-09-30T10:00:00.797Z",
      "sourcePicoseconds": 914800000,
      "sourceTimestamp": "2026-09-30T10:00:00.795Z"
    }
  ],
  "id": "read-1"
}
```

A node that could not be read has a Bad status code, and Connectware still returns the other nodes.

If the request itself is invalid, Connectware reads nothing and publishes an error. A request is invalid, for example, in the following cases:

* A NodeId appears twice.
* With a fixed list of nodes, the request contains `nodeIds`.
* With nodes in each message, the request contains no `nodeIds`.

### Batch Size Limits

Connectware splits large batches into several OPC UA requests. The `maxNodesPerWrite` and `maxNodesPerRead` connection properties set the maximum number of nodes per request:

* **Unset or `0` (default):** Connectware matches the server's limit. If the server does not report a limit, Connectware sends up to 1000 nodes per request.
* **Set explicitly:** Connectware uses your value. If your value exceeds the server's limit, Connectware uses the server's limit instead and logs a warning. Setting a value is useful when the server does not report a limit.

The following example sets the connection to send at most 500 nodes per write request and per read request:

{% code lineNumbers="true" %}

```yaml
resources:
  opcuaConnection:
    type: Cybus::Connection
    properties:
      protocol: Opcua
      connection:
        host: !ref opcuaHost
        port: !ref opcuaPort
        maxNodesPerWrite: 500
        maxNodesPerRead: 500
```

{% endcode %}

## Complex Data Types

OPC UA nodes can hold simple values (Boolean, Int32, String) or structured values (objects with fields, nested objects, arrays). Connectware maps structured OPC UA values to JSON objects, so you can read, subscribe, and write them using standard `Cybus::Endpoint` resources.

In OPC UA, complex data types are modeled as structured `DataTypes` and exchanged at runtime as `ExtensionObjects`. A complex data type is the logical definition of the data structure, while an `ExtensionObject` is the physical container used to transport that data over the network.

The following operations are supported for complex data types:

* **Read**: Retrieve the current value of a complex variable node.
* **Write**: Update a complex variable node with new structured data.
* **Subscribe**: Receive notifications when a complex variable changes.
* **Method call**: Invoke OPC UA methods with complex data type parameters.

### Prerequisites

Before working with complex data types, ensure the following:

* Identify the `nodeId` of the variable node that exposes the structured value on the OPC UA server.
* For method calls, identify the `nodeId` of the corresponding method node.
* Make sure the OPC UA server already defines the structured DataType for that node.
* Know the exact structure of the DataType as defined on the server, including field names, nesting, scalar versus array definitions, and data types. The JSON payload used in Connectware must match this structure exactly.
* Case sensitivity is important. Use the exact field name as defined on the OPC UA server. For example, if the field name is `MachineName`, then `machineName` will not work (OPC UA standard).

### Example Structure

In the examples below, the complex value has this JSON structure:

{% code lineNumbers="true" %}

```json
{
  "brand": "Cybus",
  "movement": [
    { "backward": false, "forward": false, "left": false, "right": false },
    { "backward": true, "forward": false, "left": true, "right": false }
  ],
  "name": "Robo1",
  "parts": ["arm-1", "arm-2", "arm-3"],
  "version": "8.0.01",
  "sno": 42,
  "mfg": "2026-01-10T10:30:00.000Z"
}
```

{% endcode %}

When writing data, the JSON structure must match the data type defined on the OPC UA server for that node.

#### Service Commissioning File Example

No special configuration is required to work with complex data types. Use standard `Cybus::Endpoint` resources with the `nodeId` of the complex variable node:

{% code lineNumbers="true" %}

```yaml
resources:
  opcuaConnection:
    type: Cybus::Connection
    properties:
      protocol: Opcua
      connection:
        host: !ref opcuaHost
        port: !ref opcuaPort

  complexDataEndpoint:
    type: Cybus::Endpoint
    properties:
      protocol: Opcua
      connection: !ref opcuaConnection
      subscribe:
        nodeId: 'ns=2;s=ComplexDataNode'
```

{% endcode %}

### Subscribing and Receiving Complex Values

1. Create a `Cybus::Endpoint` with the `nodeId` of the complex variable node. No special configuration is required for structured data.
2. Subscribe as usual using the `nodeId`.
3. The structured object appears inside the `value` field of each message.

Incoming message format:

{% code lineNumbers="true" %}

```json
{ "timestamp": , "value": }
```

{% endcode %}

### Writing a Complex Value to a Variable Node

To write complex data to a variable node, send the JSON object as the payload's `value`.

**Example Topic**

`services/robot/writeRobotEndpoint/set`

**Payload**

Put the structured object inside `value`.

{% code lineNumbers="true" %}

```json
{
  "value": {
    "brand": "Cybus",
    "movement": [{ "backward": false, "forward": false, "left": false, "right": false }],
    "name": "Robo-1",
    "parts": ["arm-1"],
    "version": "2.5.01",
    "sno": 784.6,
    "mfg": "2026-01-22T10:30:00.000Z"
  }
}
```

{% endcode %}

**Result**

You will receive the operation result on the `/res` topic, same as writing primitives.

### Calling a Method with a Complex Input Parameter

1. Publish the method call payload to the endpoint `/set` topic.
2. Read the call result on the `/res` topic.

**Example Topic**

`services/robot/setRobotMethod/set`

**Payload**

Include the structured object inside `inputArguments`. In this example, `value` holds the entire object as defined in the OPC UA method for the first argument.

{% code lineNumbers="true" %}

```json
{
  "value": {
    "inputArguments": [
      {
        "value": {
          "brand": "Cybus",
          "movement": [{ "backward": true, "forward": true, "left": true, "right": true }],
          "name": "Robo-2",
          "parts": ["arm-1", "arm-2"],
          "version": "5.0.01",
          "sno": 546.6,
          "mfg": "2026-01-22T10:30:00.000Z"
        }
      }
    ]
  }
}
```

{% endcode %}

**Result**

Method calls follow the same `/set` and `/res` pattern as variable writes.

### Deadband Limitations

The `deadbandType` and `deadbandValue` options are generally **not applicable** to complex data types.

However, if a primitive numeric field inside a complex structure is accessible via its own variable node (i.e., having its own `nodeId`), you can create a separate endpoint for that specific node and apply deadband settings there.

## Reconnection Behavior

In OPC UA connections, as with any network connections, the connection can be lost. When this happens, Connectware's `OpcuaConnection` will automatically switch into a reconnecting state and repeatedly try to re-establish the connection.

You can control the exact behavior of how often this is attempted by the optional `connectionStrategy` properties, see [Connection Properties](/connectors/shop-floor-connectors/opc-ua/opc-ua-client/opcuaconnection.md). The waiting time before the first retry is `initialDelay` milliseconds. Each subsequent delay doubles until it reaches `maxDelay`, which must be greater than `initialDelay`. `randomisationFactor` (a number between `0` and `1`, default `0`) adds random variation to each delay. `maxRetry` sets the maximum number of retries (default `1e9`).

Example with initially 500ms waiting time, increasing up to 10 seconds:

{% code lineNumbers="true" %}

```yaml
resources:
  myConnection:
    type: Cybus::Connection
    properties:
      protocol: Opcua
      connection:
        host: !ref opcuaHost
        port: !ref opcuaPort
        options:
          connectionStrategy:
            initialDelay: 500
            maxDelay: 10000
```

{% endcode %}

For further details, see the documentation of the internally used package `backoff`: <https://www.npmjs.com/package/backoff>

## Events for OPC UA

To subscribe to events, add the `fields` and `eventTypes` properties to a `Cybus::Endpoint` resource, as the following example shows.

Write the field names as qualified names. Often, a field name also needs to contain a namespace identifier, for example `4:MyQualifiedName.3:SubitemQualifiedName`. Field names also often need a sub-type, which you append to the name in dot notation.

Connectware publishes an event only if `fields` contains `EventType` and the type of the event is exactly one of the types in `eventTypes`. Subtypes do not match. To receive events of a subtype, add the NodeId of that subtype to `eventTypes`. `eventTypes` needs at least one entry.

{% file src="/files/STMEza6f3aVdE1BeAPbw" %}

{% code title="opcua-events-example.yml" lineNumbers="true" expandable="true" %}

```yaml
---
description: >

  Simple OPC UA connectivity example

metadata:
  name: example for opcua connectivity
  version: 1.0.0
  provider: cybus
  homepage: https://www.cybus.io

parameters:
  opcuaHost:
    type: string
    description: OPC-UA Host
    default: 172.17.0.1

  opcuaPort:
    type: integer
    default: 4840

  opcuaUser:
    type: string
    default: ''

  opcuaPass:
    type: string
    default: ''

resources:
  opcuaConnection:
    type: Cybus::Connection
    properties:
      protocol: Opcua
      connection:
        host: !ref opcuaHost
        port: !ref opcuaPort
        username: !ref opcuaUser
        password: !ref opcuaPass

  # Listen to events of the listed types from the server node
  # eventTypes matches exactly, so list every event type you want to receive
  serverEventsEndpoint:
    type: Cybus::Endpoint
    properties:
      protocol: Opcua
      connection: !ref opcuaConnection
      subscribe:
        nodeId: i=2253
        eventTypes:
          # SystemEventType, replace with the event types of your server
          - 'i=2130'
        fields:
          - 'EventType'
          - 'ReceiveTime'
          - 'Message'
          - 'SourceName'
          - 'Severity'
```

{% endcode %}

{% hint style="info" %}
An event endpoint needs at least one entry in `fields`. If `fields` is missing or empty, Connectware skips the endpoint and logs an error. Connectware still subscribes to the other endpoints of the connection.
{% endhint %}

### Identifying Alarms and Conditions

To find out which condition instance an Alarms & Conditions event belongs to, add `ConditionId` to `fields`. Connectware publishes the NodeId of the condition instance as a string. Events from different instances of the same alarm type carry different ConditionIds.

Unlike the other fields, `ConditionId` is not a browse name. Connectware requests it from the server as defined for the [ConditionType in OPC UA Part 9](https://reference.opcfoundation.org/specs/OPC-10000-9/5.5.2). For events that are not conditions, and on servers that do not support Alarms & Conditions, the value is `null`.

The following endpoint subscribes to the alarm events of the server and requests the ConditionId. It uses the `opcuaConnection` connection from the [event subscription example](#events-for-opc-ua). In `eventTypes`, set the event type of the alarms you want to receive. You can look it up in the `EventType` field of an alarm event, for example in the event view of an OPC UA client such as UaExpert. Because `eventTypes` matches exactly, a base type such as `AlarmConditionType` (`i=2915`) does not match the alarms of its subtypes.

{% code lineNumbers="true" %}

```yaml
alarmEvents:
  type: Cybus::Endpoint
  properties:
    protocol: Opcua
    connection: !ref opcuaConnection
    subscribe:
      nodeId: i=2253
      eventTypes:
        # ExclusiveLimitAlarmType, replace with the event type of your alarms
        - 'i=9341'
      fields:
        - 'EventType'
        - 'SourceName'
        - 'ConditionName'
        - 'ConditionId'
```

{% endcode %}

A message from this endpoint looks like this:

```json
{
  "EventType": "ns=0;i=9341",
  "SourceName": "Tank1",
  "ConditionName": "LevelAlarm",
  "ConditionId": "ns=2;s=Tank1.LevelAlarm"
}
```

## Accessing Status Codes for Values

The quality of a data value in OPC UA is represented by predefined status codes. The `statusCode` is transferred to the `$context` object and can be retrieved within the Rule Engine of a `Cybus::Endpoint` via `$context.raw.statusCode` as shown in the example below.

{% file src="/files/Ii9WGOxJDA6wOqZRzQD7" %}

{% code title="opcua-status-codes-example.yml" lineNumbers="true" expandable="true" %}

```yaml
description: OPC UA Status Code Example
metadata:
  name: OPC UA Status Code Example
  version: 1.0.0
parameters:
  ipAddress:
    type: string
  port:
    type: integer
    default: 4840
  samplingIntervalMs:
    type: integer
    default: 1000
  publishIntervalMs:
    type: integer
    default: 1000
resources:
  opcuaConnection:
    type: Cybus::Connection
    properties:
      protocol: Opcua
      connection:
        host: !ref ipAddress
        port: !ref port

  A_000:
    type: Cybus::Endpoint
    properties:
      protocol: Opcua
      connection: !ref opcuaConnection
      subscribe:
        nodeId: ns=1;s=0
        samplingInterval: !ref samplingIntervalMs
        publishInterval: !ref publishIntervalMs
      rules:
        - transform:
            expression: |
              { 
                "statusCode": {
                  "name": $context.raw.statusCode.name,
                  "value": $context.raw.statusCode.value,
                  "description": $context.raw.statusCode.description
                },
                "value": $.value,
                "timestamp": $.timestamp
              }
      topic: A/000
```

{% endcode %}


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.cybus.io/connectors/shop-floor-connectors/opc-ua/opc-ua-client.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
