# How to implement batch OPC UA subscriptions in AS6

**URL:** <https://community.br-automation.com/t/how-to-implement-batch-opc-ua-subscriptions-in-as6/10577>\
**Category:** How-To & Wikis\
**Tags:** opc-ua, library, automation-studio-6\
**Created:** [March 20, 2026, 1:06pm UTC](https://community.br-automation.com/t/how-to-implement-batch-opc-ua-subscriptions-in-as6/10577 "2026-03-20T13:06:43Z")\
**Posts on this page:** 2\
**Page:** 1

<div class="post-metadata">

**Author:** ![tomas.miculka](https://avatars.discourse-cdn.com/v4/letter/t/FF8800/32.png) [@tomas.miculka](https://community.br-automation.com/u/tomas.miculka)\
**Post date:** [March 20, 2026, 1:06pm UTC](https://community.br-automation.com/t/how-to-implement-batch-opc-ua-subscriptions-in-as6/10577/1 "2026-03-20T13:06:43Z")

</div>

## Background — Why the Change Was Made

If you are migrating a project from Automation Studio 4 to AS6 and you use OPC UA subscriptions, you will immediately hit a build error that stops the entire compilation:

```auto
'UA_MonitoredItemAdd' is not defined
'UA_MonitoredItemRemove' is not defined
'UA_GetNamespaceIndex' is not defined

```

This is not a bug in your code — **B&R intentionally removed these function blocks** when AS6 introduced the updated AsOpcUac library, which is now based on the **OPC 30001 PLC client specification (IEC 61131-3 v1.2)**. The old single-item function blocks (`UA_MonitoredItemAdd`, `UA_MonitoredItemRemove`) were listed as obsolete in that client specification, and B&R took the opportunity of the AS6 major release (which permits breaking changes) to remove them entirely and replace them with list-based equivalents.

> “These blocks are listed as obsolete in the client specification. The type of application is fundamentally different from the way the new list blocks must be applied, which is why the decision was made to discontinue the blocks.”  
> — [Migration to AR 6, AsOpcUac help](https://help.br-automation.com/#/en/6/communication/opcua/libraries/asopcuac/general/migration_6.html)

The architectural difference is fundamental: where the old API registered one node per function block call, the new API registers **up to 64 nodes in a single call**. This is not just a renaming — it requires rethinking the subscription setup loop.

**The good news** : beyond fixing the build, the new approach delivers a significant performance benefit during subscription initialization. In a real-world project with ~2,400 monitored variables split across 8 modules, we measured ~60× fewer state machine iterations needed to set up all subscriptions.

* * *

## Complete API Change Table

Here is a consolidated reference of all renames and removals in the AsOpcUac library between AS4 and AS6. This covers everything you will encounter during migration:

### Function Blocks

| AS4 (Old) | AS6 (New) | Notes |
| --- | --- | --- |
| `UA_MonitoredItemAdd` | `UA_MonitoredItemAddList` | Registers up to 64 nodes per call |
| `UA_MonitoredItemRemove` | `UA_MonitoredItemRemoveList` | Removes a list of items; requires `SubscriptionHdl` |
| `UA_GetNamespaceIndex` | `UA_NamespaceGetIndex` | Renamed for spec conformance; now case-sensitive on NamespaceUri |

**Online help references:**

- [`UA_MonitoredItemAddList`](https://help.br-automation.com/#/en/6/communication/opcua/libraries/asopcuac/fbks/ua_monitoreditemaddlist/ua_monitoreditemaddlist.html)
- [`UA_MonitoredItemRemoveList`](https://help.br-automation.com/#/en/6/communication/opcua/libraries/asopcuac/fbks/ua_monitoreditemremovelist/ua_monitoreditemremovelist.html)
- [`Migration to AR 6 — full change list`](https://help.br-automation.com/#/en/6/communication/opcua/libraries/asopcuac/general/migration_6.html)

### Data Types

| AS4 (Old) | AS6 (New) |
| --- | --- |
| `UAMonitoringSettings` | `UAMonitoringParameter` |
| `UAIdentifierType_String` | `UAIT_String` |
| `UAIdentifierType_Numeric` | `UAIT_Numeric` |
| `UASecurityMsgMode_None` | `UASMM_None` |
| `UASecurityPolicy_None` | `UASP_None` |
| `UANodeAdditionalInfo.AttributeId` | `UANodeAdditionalInfo.AttributeID` (capital D) |

* * *

## The Core Migration: UA\_MonitoredItemAdd → UA\_MonitoredItemAddList

### What Changed in the Interface

The new `UA_MonitoredItemAddList` FB has several significant interface differences beyond the name:

| Parameter | AS4 approach | AS6 approach |
| --- | --- | --- |
| Target variables | Plain `STRING` per item | `UAMonitoredVariables` structure — `.Values` holds the variable name |
| Quality IDs | Output DWORD per item | Must specify variable name in `UAMonitoredVariables.NodeQualityIDs` (persistent array!) |
| Timestamps | Output per item | Optional — specify variable name in `UAMonitoredVariables.TimeStamps` |
| Monitoring settings | `UAMonitoringSettings` | `UAMonitoringParameter` (renamed) |
| Sync mode | Implicit via QueueSize | Explicit `SyncMode` input — use `UAMS_FwSync` for firmware sync |
| Subscription handle | Not required | `SubscriptionHdl` is now mandatory input |
| `MinLostValueCount` | N/A | New IN/OUT array — required for queue overflow tracking |
| Items per call | 1 | Up to 64 (`MAX_INDEX_MONITORLIST + 1 = 64`) |

### Important: NodeQualityIDs Requires a Persistent Variable

The most non-obvious aspect of the new API: the `NodeQualityIDs` field in `UAMonitoredVariables` does **not** receive the quality value directly — it holds the **name of a variable** where the firmware will write the quality. That variable must remain valid for the entire lifetime of the monitored item (until `UA_MonitoredItemRemoveList` is called).

The practical solution is to declare a persistent array and reference its elements by name:

```st
(* Variable declaration *)
NodeQualityIDs_ALL : ARRAY[0..MAX_NODEIDS_INDEXES] OF DWORD;
sQualIdxStr : STRING[10];

(* Build element name string at runtime — in the chunking loop *)
Variables_Chunk[iItemsInChunk].Values := NodeUrl[iSourceIdx];
brsitoa(INT_TO_DINT(iSourceIdx), ADR(sQualIdxStr));
Variables_Chunk[iItemsInChunk].NodeQualityIDs := 'NodeQualityIDs_ALL[';
brsstrcat(ADR(Variables_Chunk[iItemsInChunk].NodeQualityIDs), ADR(sQualIdxStr));
brsstrcat(ADR(Variables_Chunk[iItemsInChunk].NodeQualityIDs), ADR(']'));

```

> **Note on string assignment** : Use `:=` for the initial string prefix (`'NodeQualityIDs_ALL['`). The AS compiler does not accept multi-character string literals as the argument to `ADR()`, so `brsstrcpy(ADR(...), ADR('prefix'))` will fail with “Expression expected”. Single-character literals (like `']'`) are accepted.

The B&R sample project (`OpcUa_Sample_Array`) uses `getMembName()` for the same purpose. Using `brsitoa` + direct `:=` + `brsstrcat` from `AsBrStr` achieves the same result without needing `getMembName`.

* * *

## Before / After Code Examples

### Variable Declarations (.var file)

**AS4:**

```pascal
UA_MonitoredItemAdd_0 : UA_MonitoredItemAdd;
UA_MonitoredItemAdd_1 : UA_MonitoredItemAdd;
MonitoringSettings_0 : UAMonitoringSettings;

```

**AS6:**

```pascal
UA_MonitoredItemAddList_0 : UA_MonitoredItemAddList;
UA_MonitoredItemAddList_1 : UA_MonitoredItemAddList;
MonitoringSettings_0 : UAMonitoringParameter;

(* Chunk arrays — max 64 items per call *)
ChunkSize : UINT;
ChunkCount : UINT;
iChunkStartIdx : INT;
iItemsInChunk : INT;
iChunk : INT;
iSourceIdx : INT;

NodeHdls_Chunk : ARRAY[0..63] OF DWORD;
Variables_Chunk : ARRAY[0..63] OF UAMonitoredVariables; (* NOT STRING anymore! *)
NodeAddInfo_Chunk : ARRAY[0..63] OF UANodeAdditionalInfo;
MonitoringSettings_Chunk : ARRAY[0..63] OF UAMonitoringParameter;
MonitoredItemHdls_Chunk : ARRAY[0..63] OF DWORD;
NodeErrorIDs_Chunk : ARRAY[0..63] OF DWORD;
ValuesChanged_Chunk : ARRAY[0..63] OF BOOL;
MinLostValueCount_Chunk : ARRAY[0..63] OF UINT;
NodeQualityIDs_ALL : ARRAY[0..MAX_NODEIDS_INDEXES] OF DWORD; (* persistent! *)
sQualIdxStr : STRING[10];

```

* * *

### Action File — MonitoredItemAdd.st

**AS4:**

```st
UA_MonitoredItemAdd_0(
    Execute := ExecuteMonitoredItemAdd_0,
    NodeHandle := NodeHdl_0[index],
    Variable := NodeUrl[index],
    MonitoringSettings:= MonitoringSettings_0,
    Timeout := T#10s);

```

**AS6:**

```st
UA_MonitoredItemAddList_0(
    Execute := ExecuteMonitoredItemAddList_0,
    SubscriptionHdl := SubscriptionHdl_0,
    NodeHdlCount := ChunkCount,
    NodeHdls := NodeHdls_Chunk,
    SyncMode := UAMS_FwSync,
    NodeAddInfos := NodeAddInfo_Chunk,
    Timeout := T#10s,
    Variables := Variables_Chunk,
    MonitoringParameter := MonitoringSettings_Chunk,
    ValuesChanged := ValuesChanged_Chunk,
    MinLostValueCount := MinLostValueCount_Chunk);

(* IMPORTANT: Explicitly copy outputs back — they are not written to chunk arrays automatically *)
IF UA_MonitoredItemAddList_0.Done OR UA_MonitoredItemAddList_0.Error THEN
    MonitoredItemHdls_Chunk := UA_MonitoredItemAddList_0.MonitoredItemHdls;
    NodeErrorIDs_Chunk := UA_MonitoredItemAddList_0.NodeErrorIDs;
END_IF;

```

* * *

### State Machine — Chunking Pattern (ADD state)

This is the core of the migration for large variable lists. Instead of iterating one item at a time, you fill a 64-item buffer and call the batch FB once per chunk.

**AS4 (single-item loop — 500 iterations for 500 variables):**

```st
IF CaseClientReadList = UA_MONITORED_ITEM_ADD THEN
    ExecuteMonitoredItemAdd_0 := TRUE;
    IF UA_MonitoredItemAdd_0.Done THEN
        MonitoredItemHdl_0[index] := UA_MonitoredItemAdd_0.MonitoredItemHdl;
        (* Find next node and continue *)
        index := index + 1;
        CaseClientReadList := UA_NODE_GET_HANDLE;
    END_IF;
END_IF;

```

**AS6 (chunking loop — ~8 iterations for 500 variables):**

```st
IF CaseClientReadList = UA_MONITORED_ITEM_ADD THEN
    (* Step 1: Fill chunk arrays — up to 64 valid items starting from current index *)
    iChunkStartIdx := index;
    iItemsInChunk := 0;

    FOR iChunk := 0 TO (ChunkSize - 1) DO
        iSourceIdx := iChunkStartIdx + iChunk;
        IF (iSourceIdx <= MAX_NODEIDS_INDEXES) AND
           (NodeID[iSourceIdx].Identifier <> '') AND
           (NodeUrl[iSourceIdx] <> '') THEN

            NodeHdls_Chunk[iItemsInChunk] := NodeHdl_0[iSourceIdx];
            NodeAddInfo_Chunk[iItemsInChunk] := NodeInfo[iSourceIdx];
            MonitoringSettings_Chunk[iItemsInChunk].SamplingInterval := MonitoringSettings_0.SamplingInterval;

            (* Build target variable name for Values and NodeQualityIDs *)
            Variables_Chunk[iItemsInChunk].Values := NodeUrl[iSourceIdx];
            brsitoa(INT_TO_DINT(iSourceIdx), ADR(sQualIdxStr));
            Variables_Chunk[iItemsInChunk].NodeQualityIDs := 'NodeQualityIDs_ALL[';
            brsstrcat(ADR(Variables_Chunk[iItemsInChunk].NodeQualityIDs), ADR(sQualIdxStr));
            brsstrcat(ADR(Variables_Chunk[iItemsInChunk].NodeQualityIDs), ADR(']'));

            iItemsInChunk := iItemsInChunk + 1;
        END_IF;
    END_FOR;
    ChunkCount := iItemsInChunk;

    (* Step 2: Trigger batch call using FlipFlop alternating instances *)
    IF iItemsInChunk > 0 THEN
        IF NOT(FlipFlop) THEN
            ExecuteMonitoredItemAddList_0 := TRUE;
        ELSE
            ExecuteMonitoredItemAddList_1 := TRUE;
        END_IF;
    END_IF;

    (* Step 3: Wait for Done and copy results back *)
    IF UA_MonitoredItemAddList_0.Done OR UA_MonitoredItemAddList_1.Done THEN
        FOR iChunk := 0 TO (iItemsInChunk - 1) DO
            iSourceIdx := iChunkStartIdx + iChunk;
            MonitoredItemHdl_0[iSourceIdx] := MonitoredItemHdls_Chunk[iChunk];
            IF NodeErrorIDs_Chunk[iChunk] <> 0 THEN
                BadVariables[BadVarsIndex] := NodeID[iSourceIdx].Identifier;
                BadVariablesUrl[BadVarsIndex] := NodeUrl[iSourceIdx];
                BadVariables_ErrorID[BadVarsIndex] := DWORD_TO_UDINT(NodeErrorIDs_Chunk[iChunk]);
                BadVarsIndex := BadVarsIndex + 1;
            END_IF;
        END_FOR;

        (* Step 4: Advance index to next chunk *)
        FOR i := (iChunkStartIdx + ChunkSize) TO MAX_NODEIDS_INDEXES DO
            IF NodeID[i].Identifier <> '' THEN
                index := i;
                CaseClientReadList := UA_NODE_GET_HANDLE;
                EXIT;
            END_IF;
        END_FOR;

        FlipFlop := UA_MonitoredItemAddList_0.Done;
    END_IF;
END_IF;

```

* * *

### Remove Action — MonitoredItemRemove.st

The remove FB also changed — it now requires `SubscriptionHdl` and `MonitoredItemHdlCount`. Reuse the `MonitoredItemHdls_Chunk` array (set `[0]` for single-item removal):

**AS4:**

```st
UA_MonitoredItemRemove_0(
    Execute := ExecuteMonitoredItemRemove_0,
    MonitoredItemHdl := MonitoredItemHdl_0[index],
    Timeout := T#10s);

```

**AS6:**

```st
(* In state machine before triggering: *)
MonitoredItemHdls_Chunk[0] := MonitoredItemHdl_0[index];

(* In action file: *)
UA_MonitoredItemRemoveList_0(
    Execute := ExecuteMonitoredItemRemove_0,
    SubscriptionHdl := SubscriptionHdl_0,
    MonitoredItemHdlCount := 1,
    MonitoredItemHdls := MonitoredItemHdls_Chunk,
    Timeout := T#10s);

```

* * *

## Performance Impact

The batch API eliminates thousands of round-trips during subscription initialization. Real-world data from a project with 2,404 total monitored variables:

| Module | Variables | AS4 iterations | AS6 iterations | Speed-up |
| --- | --- | --- | --- | --- |
| BoolSubscription | 801 | 801 | ~13 | **62×** |
| Subscription | 500 | 500 | ~8 | **62×** |
| AuxSubscription | 351 | 351 | ~6 | **58×** |
| WizSubscription | 351 | 351 | ~6 | **58×** |
| StringSubscription | 301 | 301 | ~5 | **60×** |
| VizuSubs | 100 | 100 | ~2 | **50×** |
| **Total** | **2,404** | **2,404** | **~40** | **~60×** |

For small lists (\< 64 items), no chunking loop is needed — simply fill the chunk array once and set `ChunkCount` to the actual item count.

* * *

## Known Limitation — ValuesChanged / MinLostValueCount in Chunked Setup

The AS6 help states: _“The variables connected to IN/OUT parameters must remain valid over the service life of the MonitoredItem.”_

In a chunked setup, the `ValuesChanged_Chunk` and `MinLostValueCount_Chunk` arrays are reused for every chunk — meaning only the **last chunk’s** monitored items have valid change-detection and queue overflow tracking via these arrays.

**Impact in practice** : This is typically a non-issue when `QueueSize = 0` (firmware sync mode, no queuing), because queue overflows cannot occur and the application logic does not rely on `ValuesChanged_Chunk` after setup is complete. If you need per-item `ValuesChanged` tracking in STANDBY state, you need separate permanent arrays matching your full variable list.

* * *

## Tips for Migration

1. **Do your renames globally** — use Find & Replace across the project for type/enum renames (they affect many files). The full mapping is in the [Migration to AR 6 help page](https://help.br-automation.com/#/en/6/communication/opcua/libraries/asopcuac/general/migration_6.html).

2. **Verify against the AS6 help and sample project** — The AS6 interface for `UA_MonitoredItemAddList` changed significantly between the release candidate (used in some early documentation) and the final release. Cross-check parameter names against the official help before coding.

3. **The B&R sample project** for AS6 OPC UA is located at `\AS6\Samples\OpcUa_Sample.zip` in the AS installation folder. The relevant example is `OpcUa_Sample_Array/ClientBasic/MonitoredItemList`.

4. **Start with INIT** — add `ChunkSize := 64;` to your `_INIT` state. This makes the magic number explicit and easy to document.

5. **Small modules (\< 64 items)** don’t need the full chunking loop — just populate `Variables_Chunk[0]`, set `ChunkCount := 1`, and call the batch FB. Same API, simpler usage.

6. **ADR() with string literals** — `ADR('multi-char-string')` is not valid in B&R AS. Use `:=` for initial assignment, then `brsstrcat` to append. `ADR(']')` with a single character works.

7. **Type conversion** — AS6 removed implicit `DWORD → UDINT`. Use `DWORD_TO_UDINT(x)`. Also, C-style cast `DINT(x)` is invalid — use `INT_TO_DINT(x)` or `UINT_TO_DINT(x)` depending on the source type.

* * *

## Online Help References

| Topic | Link |
| --- | --- |
| `UA_MonitoredItemAddList` | [B&R Online Help](https://help.br-automation.com/#/en/6/communication/opcua/libraries/asopcuac/fbks/ua_monitoreditemaddlist/ua_monitoreditemaddlist.html) |
| `UA_MonitoredItemRemoveList` | [B&R Online Help](https://help.br-automation.com/#/en/6/communication/opcua/libraries/asopcuac/fbks/ua_monitoreditemremovelist/ua_monitoreditemremovelist.html) |
| Migration to AR 6 (full change table) | [B&R Online Help](https://help.br-automation.com/#/en/6/communication/opcua/libraries/asopcuac/general/migration_6.html) |

---

<div class="post-metadata">

**Author:** ![michael\_w](https://avatars.discourse-cdn.com/v4/letter/m/FF8800/32.png) [@michael\_w](https://community.br-automation.com/u/michael_w)\
**Post date:** [March 20, 2026, 3:27pm UTC](https://community.br-automation.com/t/how-to-implement-batch-opc-ua-subscriptions-in-as6/10577/2 "2026-03-20T15:27:48Z")

</div>

Hi there,

thanks for the article.  
For me, it’s a little bit too long for basically just little changes (filling lists vs single items isn’t a huge deal, imo), but that’s just my personal opinion.

Anyway a few things I found during reading:

> [@tomas.miculka](#):
>
> In a real-world project with ~2,400 monitored variables split across 8 modules, we measured ~60× fewer state machine iterations needed to set up all subscriptions.

~~Unsure why you only find a factor of 60 here when 2400 / 64 (per List) equals to 37.5, so 38 list calls? Or do I understand your state machine iterations wrong somehow?~~

Edit: I think I got it wrong in my thinking… factor should be around 64 due to that being the list limit, so 60 seems about right

* * *

> [@tomas.miculka](#):
>
> **Note on string assignment** : Use `:=` for the initial string prefix (`'NodeQualityIDs_ALL['`). The AS compiler does not accept multi-character string literals as the argument to `ADR()`, so `brsstrcpy(ADR(...), ADR('prefix'))` will fail with “Expression expected”. Single-character literals (like `']'`) are accepted.

This is not correct.  
Simple example I set up just now and it compiled without warning and error:

![image](https://europe1.discourse-cdn.com/brcommunity/original/2X/b/b767ca8d49bb0f2082a4458da29eaf6116135d93.png)

Unsure where you would get an error there, but this is valid ST syntax and works across various AS versions.

Best regards
