Bulk Device Tag Operations

When your OPC UA server exposes hundreds or thousands of variable nodes that need to flow to MQTT, configuring individual tags in the protocol adapter UI is impractical. Bulk device tag operations solve this with two REST API endpoints and a panel in the HiveMQ Edge UI that lets you configure all tags and mappings for an adapter in a single round-trip.

How it Works

  • Browse Endpoint: Scan the device address space of a connected adapter. Download every discovered variable node as a CSV, JSON, or YAML file.
    The file includes suggested defaults for tag names and MQTT topics.

  • Import Endpoint: Accepts an edited browse file to create, update, or delete tags and mappings in one atomic operation.

File-Based Workflow

The file-based workflow design allows you to work with familiar tools that fit your process:

  • Spreadsheets for filtering and manual selection.

  • Formulas for generating patterns at scale.

  • Python scripts for automated tag configuration.

  • Command-line tools like curl for CI/CD pipelines. Iterate until the configuration matches your intent, then commit the result to HiveMQ Edge.

Getting Started

If you are new to this feature, begin with our Import Your First Tags Using the UI tutorial. When you want options, automation, or scripted pipelines, continue with Quickstart: OPC UA Tags to MQTT Using the REST API.

Bulk device tag operations currently support only OPC UA adapters. The underlying BulkTagBrowser interface is designed to accept additional protocols in future releases.

Quickstart: OPC UA Tags to MQTT Using the UI

This tutorial shows you the fastest way to get OPC UA data flowing to MQTT using bulk device tag operations. You stay entirely in the HiveMQ Edge UI, edit a few rows in a spreadsheet, and finish with tags publishing to MQTT.

This tutorial skips curl, advanced options, and import modes. These topics are covered individually in Quickstart: OPC UA Tags to MQTT Using the REST API and the following reference sections.

Before you begin: You need a running HiveMQ Edge instance and an OPC UA adapter connected to a device. If you do not have an adapter yet, create one in the Adapters screen in the HiveMQ Edge UI, or follow Step 2 of Quickstart: OPC UA Tags to MQTT Using the REST API.

Step 1: Open the Bulk Operations Panel

In the Edge UI, go to Adapters and click Edit on your OPC UA adapter.

The Device Tags tab panel opens. This is where bulk operations are performed.

Open the bulk operations panel

Step 2: Export the Address Space to a File

Leave Root ID, Max depth, and Format at their defaults. Click Browse & Download. Your browser saves a CSV file containing all discovered variable nodes to your downloads folder.

Downloaded file in browser

Step 3: Open the File in a Spreadsheet

In another tab, open the downloaded CSV in a spreadsheet tool such as Google Sheets. Go to sheet.new to create a new Google Sheet. Choose File → Import → Upload from computer and select the downloaded file. In the import dialog, select Replace spreadsheet*and Detect automatically, then click Import data.

You now see one row per discovered variable node. Editable columns appear first (tag_name, tag_description, northbound_topic).

CSV imported into Google Sheets

Step 4: Keep Only the Rows You Want

For this example, decide which two or three nodes you want to publish to MQTT. Delete all other data rows in the spreadsheet. Keep only one header row and a few data rows.

A row with a blank tag_name is skipped on import. You can leave unwanted rows in place, but deleting them keeps the file easy to read.
Spreadsheet trimmed to the kept rows

Step 5: Fill In the Tag Name and Topic

For each remaining row, fill in two cells:

  • tag_name: A name for the tag (for example, plant1-counter).

  • northbound_topic: The MQTT topic to publish on (for example, plant1/simulation/counter), or * to use the suggested default.

Leave all other columns unchanged. The defaults are sufficient for this example.

Spreadsheet with tag_name and northbound_topic filled in

Step 6: Download the Edited File as CSV

To export your updated spreadsheet, select File → Download → Comma-separated values (.csv). A new file (often with a slightly different name) is saved to your downloads folder.

Your edits are saved in the new CSV file. The original file from Step 2 is unchanged. The original file you downloaded in Step 2 is unchanged. Make sure you import the edited file. Importing the original file creates no tags because tag_name is empty on every row.

Step 7: Import the Edited File in Overwrite Mode

In the Device Tags panel of the Edge UI, go to the Import Device Tags section.

  1. Click the Choose file picker and select the edited CSV from Step 6.

  2. Set the Import mode to Overwrite.

  3. Click Import.

Overwrite mode ensures that the tags of the adapter match exactly what is in your file.

A green success message confirms the import. If the file contains errors, the message identifies the row and column to fix.

Import section with Overwrite mode and success toast

Step 8: Verify the Imported Tags

Open the Tags tab of the adapter. Each imported row appears as a tag linked to its OPC UA node_id.

Adapter Tags tab with new tags

To confirm data is flowing to MQTT, install the HiveMQ MQTT CLI (brew install hivemq/mqtt-cli/mqtt-cli on macOS) and subscribe to your MQTT topics:

mqtt sub -t 'my-opcua/simulation/#' -T

Messages arrive at the configured publishing interval. At this point, your OPC UA data is flowing to MQTT.

Next Steps

When you are comfortable with the basic flow, explore the following topics to expand the same workflow for larger setups and more control:

  • Automate the workflow

    Move to Quickstart: OPC UA Tags to MQTT Using the REST API to run the same workflow with curl, including the asterisk wildcard for accepting defaults in bulk and pointers to JSON and YAML formats.

  • Control how updates are applied

    Learn about Conflict Resolution Modes to decide when to create, update, overwrite, or delete tags with the`MERGE_SAFE`, MERGE_OVERWRITE, CREATE, DELETE modes.

  • Understand every field

    See the Device Tag File Schema for a full explanation of what each of the 21 columns means and what happens when you leave a cell blank.

  • Scale to larger configurations

    Use additional workflows such as scripted pipelines (Python), full-sync, and wildcard onboarding to generate consistent tag names and MQTT topics across hundreds or thousands of nodes.

Quickstart: OPC UA Tags to MQTT Using the REST API

This end-to-end walkthrough takes you from a connected OPC UA adapter to a fully configured set of tags and MQTT mappings. You use curl for the API calls and a spreadsheet editor (Google Sheets) to edit the tag file.

Before You Begin

You need:

  • A running HiveMQ Edge instance on localhost:8080.

  • * A reachable OPC UA server (a simulation server such as Prosys OPC UA Simulation Server works well).

  • curl and jq installed on your system.

This walkthrough uses an adapter named my-opcua.

Step 1: Get an Authentication Token

All HiveMQ Edge management endpoints require a bearer token issued to a user with the admin role. HTTP basic auth is not accepted on these endpoints.

Store the token in the $TOKEN shell variable for use in subsequent requests:

TOKEN=$(curl -s -X POST http://localhost:8080/api/v1/auth/authenticate \
  -H 'Content-Type: application/json' \
  -d '{"userName":"admin","password":"hivemq"}' \
  | jq -r .token)

Replace the default credentials with your own.

The token expires. If a later request returns HTTP 401 Unauthorized (with body \{"title":"Unauthorized","detail":"JWT token has expired"\} or similar), run the command again to refresh $TOKEN.

Step 2: Ensure the OPC UA Adapter Exists

The create call is conditional: run it only if the check tells you no adapter is there.

Check whether the my-opcua adapter is already there and connected:

curl -s 'http://localhost:8080/api/v1/management/protocol-adapters/adapters' \
  -H "Authorization: Bearer $TOKEN" | jq '.items[] | {id, status: .status.connection}'

Act on the result of the check:

Output Action

\{ "id": "my-opcua", "status": "CONNECTED" \}

The adapter is ready. Skip to Step 3.

\{ "id": "my-opcua", "status": "DISCONNECTED" \}

The adapter exists but cannot reach the server. Verify the OPC UA server endpoint is reachable from HiveMQ Edge and the adapter`uri` is correct. Do not create a new adapter.

No output

No adapter exists yet. Create the adapter.

To create the adapter, replace <host>, <port>, and <path> in the uri with your OPC UA server values (for example, opc.tcp://localhost:53530/OPCUA/SimulationServer):

curl -s -X POST \
  'http://localhost:8080/api/v1/management/protocol-adapters/adapters/opcua' \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "id": "my-opcua",
    "type": "opcua",
    "config": {
      "id": "my-opcua",
      "uri": "opc.tcp://<host>:<port>/<path>",
      "overrideUri": false,
      "security": { "policy": "NONE" },
      "opcuaToMqtt": { "publishingInterval": 1000 }
    }
  }'

The adapter connects automatically. Re-run the check and confirm the adapter status is CONNECTED before you continue to step 3.

If you run the create call when an adapter with the same id already exists, the server rejects it with HTTP 400 and a body like:

{
  "type": "http://nowhere/AdapterFailedValidation",
  "title": "Adapter failed validation",
  "detail": "The provided adapter was invalid",
  "status": 400,
  "errors": [ { "detail": "Invalid user supplied data", "parameter": "id" } ]
}

Delete the existing adapter first (DELETE /api/v1/management/protocol-adapters/adapters/my-opcua) or choose a different id.

Step 3: Browse the Adapter Address Space

Download all OPC UA variable nodes as a CSV file. The browse endpoint scans every variable node reachable from the OPC UA ObjectsFolder and writes one row per node:

curl -s -X POST \
  'http://localhost:8080/api/v1/management/protocol-adapters/adapters/my-opcua/device-tags/browse' \
  -H 'Accept: text/csv' \
  -H "Authorization: Bearer $TOKEN" \
  -o my-opcua-device-tags.csv

Inspect the first few rows:

head -3 my-opcua-device-tags.csv

You see a header row with 21 columns followed by one row per node. Each row represents one discovered variable node.

Editable fields appear first (so the columns you edit are visible without scrolling in a spreadsheet), followed by read-only informational fields from the device:

tag_name,tag_name_default,tag_description,northbound_topic,northbound_topic_default,...,node_path,namespace_uri,namespace_index,node_id,data_type,access_level,node_description
,plant/static/int32,,,my-opcua/plant/static/int32,...,/Plant/Static/Int32,http://example.com/UA/,2,ns=2;s=Int32,Int32,READ_WRITE,"32-bit integer variable"
,plant/static/double,,,my-opcua/plant/static/double,...,/Plant/Static/Double,http://example.com/UA/,2,ns=2;s=Double,Double,READ_WRITE,"Double-precision float"

At this stage, tag_name is empty on every row, so the file would create nothing if you imported it as-is. tag_name_default already contains a suggestion derived from the node’s full browse path; northbound_topic_default and southbound_topic_default are populated the same way. In Step 4 you decide which rows become tags by filling in tag_name (or typing a single asterisk, *, to accept the default).

Browse results are deduplicated by node_id. The OPC UA address space is a graph — the same variable can be reachable through multiple hierarchical paths — but each unique node appears only once in the output.

The endpoint streams results as it walks the address space, so running a full browse against servers with thousands of nodes is safe. A medium-sized simulation server such as Prosys typically returns around 500 variable nodes spread over 5-10 namespaces. The overall operation has a 120-second timeout.

Need to scope the browse to a specific subtree? Pass rootId (URL-encoded) and maxDepth as query parameters. See Browse Device Address Space for details.

Step 4: Edit the File

You now have a spreadsheet-ready CSV file with every node the device exposes. Decide which rows become tags and how they map to MQTT topics. Delete all rows you do not want to publish to MQTT:

  1. Open Google Sheets and create a new spreadsheet.

  2. Go to FileImportUpload and select my-opcua-device-tags.csv.

  3. In the import dialog, select Replace spreadsheet as the import location and Detect automatically for the separator.

  4. Click Import data.

You see the file laid out with one row per discovered node.

Rules of thumb for this step:

  • A row with a blank tag_name is a no-op — nothing is created for it on import. You can either delete such rows or leave them in the file; either way they have no effect.

  • A row with tag_name filled in becomes a tag on import. If northbound_topic is also set, a northbound mapping is created too; if southbound_topic is also set, a southbound mapping is created too. Leaving a topic blank means the corresponding mapping is not created (even though the _default column suggests one).

Two practical patterns:

Pattern A: Select and name individually. Delete unwanted rows or leave them with an empty tag_name. Rows with an empty tag_name are ignored during import. For each remaining row, fill in tag_name with your preferred name. If you want the value published to MQTT, also fill in northbound_topic. If you want to accept MQTT writes that flow back to the device, also fill in southbound_topic.

Pattern B: Accept defaults in bulk. Delete unwanted rows or leave them with an empty tag_name. Rows with an empty tag_name are ignored during import. For each remaining row, type a single asterisk character (*, U+002A) into tag_name and northbound_topic. If you also want subscriptions, type an asterisk into southbound_topic as well. On import, each asterisk resolves to the suggested default value from the corresponding _default column of the same row.

The asterisk is not a whitespace. A blank (empty) cell leaves the field unchanged or unset. An asterisk (*) applies the suggested default value for that field.

The two behaviors are opposite.

You can use spreadsheet formulas to apply naming conventions in bulk. For example, to generate topic names from node paths using your own naming convention:

=ARRAYFORMULA(IF(A2:A<>"", "plant1/" & LOWER(REGEXREPLACE(O2:O, "[^A-Za-z0-9]+", "/")), ""))

The formula reads node_path (column O) and writes a topic pattern such as plant1/simulation/counter into northbound_topic for every non-empty row.

  1. When the file looks correct, select FileDownloadComma-separated values (.csv).

  2. Save the downloaded file as my-opcua-device-tags-edited.csv in the same directory as the original.

Step 5: Import the Edited File

Upload the edited CSV. The default conflict-resolution mode for imports is MERGE_SAFE. This mode creates new tags and rejects any change that would silently overwrite an existing one. See Conflict Resolution Modes for the full decision matrix.

curl -s -X POST \
  'http://localhost:8080/api/v1/management/protocol-adapters/adapters/my-opcua/device-tags/import?mode=MERGE_SAFE' \
  -H 'Content-Type: text/csv' \
  -H "Authorization: Bearer $TOKEN" \
  --data-binary @my-opcua-device-tags-edited.csv | jq .

A successful import returns an ImportResult with counts and a per-tag action list:

{
  "tagsCreated": 7,
  "tagsUpdated": 0,
  "tagsDeleted": 0,
  "northboundMappingsCreated": 7,
  "northboundMappingsDeleted": 0,
  "southboundMappingsCreated": 0,
  "southboundMappingsDeleted": 0,
  "tagActions": [
    { "name": "plant1-counter", "action": "CREATED" },
    { "name": "plant1-random", "action": "CREATED" },
    { "name": "plant1-temperature", "action": "CREATED" }
  ]
}

If the response is 400 Bad Request, the body contains a list of validation errors with row numbers and column names. See Validation for how to interpret and resolve errors.

Step 6: Verify the Imported Tags

Confirm that the added tags and northbound mappings exist on the adapter:

curl -s \
  'http://localhost:8080/api/v1/management/protocol-adapters/adapters/my-opcua/tags' \
  -H "Authorization: Bearer $TOKEN" | jq .

You see every tag you imported, each with its tag name and the OPC UA node_id it references:

{
  "items": [
    {
      "name": "plant1-counter",
      "description": "",
      "definition": { "node": "ns=3;i=1001" }
    },
    {
      "name": "plant1-random",
      "description": "",
      "definition": { "node": "ns=3;i=1002" }
    }
  ]
}

To confirm that data is flowing, use the HiveMQ MQTT CLI to subscribe to the MQTT topics you configured:

mqtt sub -t 'plant1/#' -T

The -t flag selects the topic filter to subscribe to and -T (--show-topics) prepends the topic name to each received payload so that you can see which tag produced which message. Host and port default to localhost:1883.

Install the MQTT CLI on macOS with brew install hivemq/mqtt-cli/mqtt-cli. Run mqtt sub --help for the full option reference.

The adapter publishes a message for each tag every publishing interval. You now have a fully configured HiveMQ Edge instance produced from a single browse, a few spreadsheet edits, and one import.

Next Steps

  • Iterate on your configuration

    Re-browse the adapter, adjust your file, and re-import with `MERGE_OVERWRITE` to update existing tags and mappings without removing others.
     See <<import-modes>> for a full walkthrough of the modes.
  • Shape the data you publish

    Use Data Hub data combiners to merge multiple tags into a single MQTT payload, add computed fields, or filter what gets published.

  • Expand to more sources

    Add more adapters to connect additional OPC UA servers or other protocols using the protocol adapter guide.

Browse and Import Through the UI

The HiveMQ Edge UI includes a Browse and Import panel for every adapter that supports bulk operations. The panel performs exactly the same browse and import calls as the REST API, with the downloaded file written by the browser and the import file chosen from disk. Use the Browse and Import panel when you want a point-and-click experience or a quick sanity check without leaving the UI.

This section covers the technical details of bulk device tag operations. If you are new to this feature, start with Quickstart: OPC UA Tags to MQTT Using the UI for a visual walkthrough before consulting this reference.

The Browse section mirrors the REST parameters:

  • Root ID — optional OPC UA node ID to scope the browse (for example, ns=2;s=Data). Leave blank to start from ObjectsFolder.

  • Max depth — maximum recursion depth. Leave blank or set to 0 for unlimited.

  • Format — CSV, JSON, or YAML. The downloaded file is named tags-{adapterId}-{timestamp}.{ext}.

Click Browse. The browser downloads the file. Open it in your preferred editor, make your changes, and save.

The Import section accepts the edited file. The Import mode dropdown maps to the REST mode query parameter:

  • Merge (safe) — MERGE_SAFE (default): create new tags, reject changes that would silently overwrite an existing tag.

  • Merge (overwrite) — MERGE_OVERWRITE: create new tags, update changed ones, keep everything else.

  • Overwrite — OVERWRITE: full sync; the adapter ends up matching the file exactly.

  • Create only — CREATE: assert the adapter starts empty and the file defines every tag.

  • Delete — DELETE: remove tags listed in the file, keep the rest.

Select the edited file, pick an import mode, and click Import. A success toast confirms the import. Validation failures surface as error toasts with the full list of issues.

The newly created (or updated) tags appear immediately in the adapter’s Tags tab and in Data Hub combiners.

The UI panel is ideal for ad-hoc work. Scripted pipelines, CI provisioning, and bulk edits across multiple adapters are better served by the REST API — see Additional Workflows.

Browse Device Address Space

The browse endpoint scans the address space of a connected adapter and returns every discovered variable node as a downloadable file:

POST /api/v1/management/protocol-adapters/adapters/{adapterId}/device-tags/browse
Table 1. Browse query parameters
Parameter Default Mandatory Description

rootId

i=85 (ObjectsFolder)

no

The OPC UA node ID to start browsing from. URL-encode the value (for example, ns%3D2%3Bs%3DData).

maxDepth

0 (unlimited)

no

Maximum recursion depth. 0 browses the entire subtree. Negative values are rejected with 400.

Table 2. Browse content negotiation
Accept Header Output Format

text/csv

CSV (default when no Accept header is sent)

application/json

JSON

application/yaml

YAML

The response includes a Content-Disposition: attachment; filename="{adapterId}-device-tags.{ext}" header so browsers offer the file as a download. Results are sorted by node path for deterministic output.

Table 3. Browse response codes
Status Description

200

File body containing the discovered variable nodes.

400

Invalid maxDepth value (for example, negative).

404

Adapter not found.

409

Adapter does not support bulk browsing, or the browse operation failed.

504

Browse timed out (120-second limit).

Authentication requires the admin role.

Example: Browse as CSV

curl -s -X POST \
  'http://localhost:8080/api/v1/management/protocol-adapters/adapters/my-opcua/device-tags/browse' \
  -H 'Accept: text/csv' \
  -H "Authorization: Bearer $TOKEN" \
  -o my-opcua-device-tags.csv

Example: Browse as JSON for a Subtree

curl -s -X POST \
  'http://localhost:8080/api/v1/management/protocol-adapters/adapters/my-opcua/device-tags/browse?rootId=ns%3D2%3Bs%3DData&maxDepth=2' \
  -H 'Accept: application/json' \
  -H "Authorization: Bearer $TOKEN" \
  -o my-opcua-device-tags.json

The Device Tag File

Every browse output and import input uses a shared 21-field schema. The fields fall into two categories:

  • Informational fields — populated by the browse, read-only context for the user.

  • Editable fields — user-configurable. The _default fields are pre-populated by the browse with suggested values; the remaining editable fields are empty on browse output and filled in by the user before import.

Editable Fields

Every editable field accepts one of three kinds of value, and the result is the same across all fields:

  • Blank (empty cell) — the field is treated as "not provided". What that means in practice depends on the field and is stated explicitly in the If blank column of the table below. For required fields such as tag_name, blank means "skip this row entirely". For optional fields such as max_qos, blank means "use the server-side default".

  • A single asterisk character (*, U+002A) — resolves to the corresponding _default column on the same row. For example, typing an asterisk into the tag_name cell resolves to the value in tag_name_default. See Wildcard Resolution.

  • Any other value — used literally.

A blank cell and a cell containing an asterisk do opposite things. Blank is ignored; an asterisk expands to the cell’s _default.

Columns ending in _default are populated by the browse and provide the target for the asterisk. You can overwrite them, but most workflows leave them as-is and type an asterisk where you want the default applied.

Table 4. Editable fields (user fills in before import)
# Field Type Description If blank

1

tag_name

String

Tag name to create. Must not contain whitespace. Maximum 256 characters.

The row is skipped: no tag and no mappings are created for it. Any northbound_topic or southbound_topic set on a blank-tag_name row is rejected with MAPPING_WITHOUT_TAG.

2

tag_name_default

String

Auto-generated default tag name derived from the full sanitized browse path (no adapter prefix). For example, /Plant/Static/Int32 becomes plant-static-int32. Using the full path guarantees uniqueness when the same display name appears in multiple folders.

Browse always fills this in. If it is somehow empty and tag_name=*, the row is rejected with WILDCARD_NO_DEFAULT.

3

tag_description

String

Optional tag description. Maximum 1024 characters.

The tag is created with an empty description.

4

northbound_topic

String

MQTT publish topic. Must not contain MQTT wildcards (+, #) or null bytes. Maximum 65,535 characters (MQTT specification limit).

No northbound mapping is created for this row. The tag itself is still created if tag_name is set.

5

northbound_topic_default

String

Auto-generated default: {adapterId}/{sanitizedPath}.

Browse always fills this in. If it is somehow empty and northbound_topic=*, the row is rejected with WILDCARD_NO_DEFAULT.

6

southbound_topic

String

MQTT subscribe topic filter. Must not contain null bytes. Maximum 65,535 characters (MQTT specification limit).

No southbound mapping is created for this row, even though southbound_topic_default is populated. Type an asterisk (*) or a literal value if you want a subscription.

7

southbound_topic_default

String

Auto-generated default: {adapterId}/write/{sanitizedPath}.

Browse always fills this in. If it is somehow empty and southbound_topic=*, the row is rejected with WILDCARD_NO_DEFAULT.

8

southbound_field_mapping

String

Source-to-destination field mapping instructions for southbound writes. CSV format: source->dest;source2->dest2.

No field mapping is applied; the southbound payload is forwarded as-is.

9

max_qos

Integer

MQTT QoS level: 0, 1, or 2.

Default QoS 1 is used.

10

message_expiry_interval

Long

MQTT message expiry interval in seconds. Valid range: 1 to 4,294,967,295 (MQTT 5 maximum).

No expiry is set. Messages are retained by the broker indefinitely per the MQTT 5 specification.

11

include_timestamp

Boolean

Include a timestamp in the MQTT payload.

Default true is used.

12

include_tag_names

Boolean

Include the tag name in the MQTT payload.

Default false is used.

13

include_metadata

Boolean

Include metadata in the MQTT payload.

Default false is used.

14

mqtt_user_properties

String

MQTT 5 user properties. CSV format: key=value;key2=value2. Maximum 256 characters per key and per value.

No user properties are attached.

Informational Fields

Table 5. Informational fields (populated by browse, read-only)
# Field Type Description

15

node_path

String

Hierarchical browse path (for example, /Data/Static/Int32).

16

namespace_uri

String

OPC UA namespace URI.

17

namespace_index

Integer

OPC UA namespace index.

18

node_id

String

OPC UA node identifier (for example, ns=2;s=Int32).

19

data_type

String

Human-readable data type name (for example, Int32, Double, Boolean).

20

access_level

String

READ, WRITE, READ_WRITE, or NONE.

21

node_description

String

Server-provided node description when available.

In CSV output, editable fields appear first (columns 1-14) so that the fields you edit are immediately visible when you open the file in a spreadsheet. Informational fields follow (columns 15-21). On import, columns can appear in any order because the CSV parser is header-driven and case-insensitive.

Wildcard Resolution

You can set tag_name, northbound_topic, or southbound_topic to a single asterisk character (*, U+002A). Before validation, the system replaces that asterisk with the value in the corresponding _default column on the same row:

  • An asterisk in tag_name becomes the value in tag_name_default.

  • An asterisk in northbound_topic becomes the value in northbound_topic_default.

  • An asterisk in southbound_topic becomes the value in southbound_topic_default.

This is the fastest way to onboard a large number of nodes when the generated defaults are acceptable.

If the corresponding _default column is empty or missing, the row is rejected with error code WILDCARD_NO_DEFAULT (see Error Codes).

The cells in the example table below use the word "asterisk" (instead of the symbol) wherever the value should be a single * character, because asterisks are easy to miss in monospace text.
Table 6. Example: typical combinations on a single row
tag_name northbound_topic southbound_topic Result

blank

(ignored)

(ignored)

Row is skipped entirely — no tag, no mappings. Any topic values on this row have no effect because the tag itself is not created.

asterisk

asterisk

blank

Tag created from tag_name_default; northbound mapping created from northbound_topic_default; no southbound mapping.

asterisk

asterisk

asterisk

Tag, northbound mapping, and southbound mapping all created from the row’s _default columns.

plant1-counter

plant1/counter

blank

Tag plant1-counter created; northbound mapping published on plant1/counter; no southbound mapping.

plant1-counter

blank

blank

Tag plant1-counter created with no mappings — the tag exists in HiveMQ Edge but nothing is published or subscribed.

For blank behavior on other fields (for example, max_qos, include_timestamp), see the If blank column of Editable Fields.

Format-Specific Details

CSV (RFC 4180): Flat rows with a 21-column header. Field mappings are compact-encoded as source->dest;source2->dest2. User properties are compact-encoded as key=value;key2=value2. Delimiter characters inside values are URL-encoded on export (%3B for ;, %2D%3E for ->, %3D for =, %25 for %) and automatically decoded on import. Rows are sorted by node_path. UTF-8 encoding with \r\n line endings. The UTF-8 byte order mark (BOM) is automatically stripped on import, so CSV files saved from Excel on Windows are handled correctly.

JSON: Nested structure with logical grouping:

{
  "rows": [
    {
      "node": {
        "node_path": "/Data/Static/Int32",
        "namespace_uri": "http://example.com/UA/",
        "namespace_index": 2,
        "node_id": "ns=2;s=Int32",
        "data_type": "Int32",
        "access_level": "READ_WRITE",
        "node_description": "32-bit integer variable"
      },
      "tag": {
        "tag_name": "my-sensor",
        "tag_name_default": "data-static-int32",
        "tag_description": "Temperature sensor"
      },
      "northbound": {
        "topic": "plant1/sensors/temperature",
        "topic_default": "my-opcua/data/static/int32",
        "max_qos": 1,
        "message_expiry_interval": 3600,
        "include_timestamp": true,
        "include_tag_names": false,
        "include_metadata": false,
        "mqtt_user_properties": {
          "source": "opcua",
          "plant": "north"
        }
      },
      "southbound": {
        "topic": "plant1/commands/temperature",
        "topic_default": "my-opcua/write/data/static/int32",
        "field_mapping": [
          { "source": "value", "destination": "value" }
        ]
      }
    }
  ]
}

Null fields are omitted. The northbound and southbound objects are omitted entirely when all their fields are null. On browse output, the editable fields are null (except for _default fields), so the northbound and southbound objects typically contain only the topic_default field.

JSON and YAML use the same nested structure; CSV uses flat column names. The import parser accepts all three formats transparently based on the Content-Type header. The schema tables in Editable Fields and Informational Fields use the flat CSV column names for consistency. The mapping between the two naming conventions is:

Table 7. CSV column ↔ JSON/YAML key mapping
CSV column name JSON / YAML key path

tag_name

tag.tag_name

tag_name_default

tag.tag_name_default

tag_description

tag.tag_description

northbound_topic

northbound.topic

northbound_topic_default

northbound.topic_default

southbound_topic

southbound.topic

southbound_topic_default

southbound.topic_default

southbound_field_mapping

southbound.field_mapping

max_qos

northbound.max_qos

message_expiry_interval

northbound.message_expiry_interval

include_timestamp

northbound.include_timestamp

include_tag_names

northbound.include_tag_names

include_metadata

northbound.include_metadata

mqtt_user_properties

northbound.mqtt_user_properties

node_path

node.node_path

namespace_uri

node.namespace_uri

namespace_index

node.namespace_index

node_id

node.node_id

data_type

node.data_type

access_level

node.access_level

node_description

node.node_description

YAML: Identical structure to JSON in YAML wire format. The document start marker (---) is suppressed.

Import Device Tags

The import endpoint accepts a tag file and creates, updates, or deletes tags and mappings on the specified adapter:

POST /api/v1/management/protocol-adapters/adapters/{adapterId}/device-tags/import
Table 8. Import query parameters
Parameter Default Mandatory Description

mode

MERGE_SAFE

no

Conflict resolution mode. See Conflict Resolution Modes.

validateNodes

false

no

Reserved for future use. Always false.

The Content-Type header determines the input format: text/csv, application/json, or application/yaml. Only UTF-8 is accepted.

Table 9. Import response codes
Status Description

200

Import successful. Returns an ImportResult JSON object with counts and per-tag actions.

400

Parse error or validation errors. See Error Response Format for the response format.

404

Adapter not found.

409

Adapter does not support bulk operations.

415

Unsupported or non-UTF-8 Content-Type. Only text/csv, application/json, and application/yaml with UTF-8 encoding are accepted.

500

Unexpected import failure.

Authentication requires the admin role.

Import Result

A successful import returns a JSON object with counters and a list of per-tag actions.

When an import is a pure no-op (for example, MERGE_SAFE against a file identical to the adapter’s current state), all counters are 0 and tagActions may be null rather than an empty array. Clients should treat a missing or null tagActions as "nothing to report".
{
  "tagsCreated": 3,
  "tagsUpdated": 1,
  "tagsDeleted": 0,
  "northboundMappingsCreated": 4,
  "northboundMappingsDeleted": 1,
  "southboundMappingsCreated": 2,
  "southboundMappingsDeleted": 0,
  "tagActions": [
    { "name": "sensor-1", "action": "CREATED" },
    { "name": "sensor-2", "action": "CREATED" },
    { "name": "sensor-3", "action": "CREATED" },
    { "name": "actuator-1", "action": "UPDATED" }
  ]
}

Conflict Resolution Modes

The mode query parameter controls how imported tags interact with tags that already exist on the adapter. Tags are matched by their OPC UA node_id, which is the stable device identifier: renaming a tag (same device node, different name) is therefore detected as an update, not as a delete followed by a create.

Each tag in play falls into one of four buckets depending on where it appears:

  • Edge-only — the node_id exists on the adapter but not in the file.

  • File-only — the node_id is in the file but does not exist on the adapter.

  • Both (identical) — same node_id, same tag name, same description, same northbound and southbound mappings.

  • Both (different) — same node_id, but any part of the tag definition differs (name, description, or mappings).

The matrix below uses the following verbs for each bucket:

  • Create — the tag (and any mappings defined on its row) is added to the adapter.

  • Keep — the tag already on the adapter is left unchanged.

  • Overwrite — the tag on the adapter is replaced with the file’s version (including mappings).

  • Delete — the tag and all its mappings are removed from the adapter.

  • No-op — nothing changes (the file and the adapter already agree).

  • Error — the entire import is rejected before any mutation; fix the file or pick a different mode.

Table 10. Conflict resolution matrix
Mode Edge-only File-only Both (identical) Both (different)

CREATE

Error

Create

No-op

Error

DELETE

Delete

Error

Keep

Error

OVERWRITE

Delete

Create

Keep

Overwrite

MERGE_SAFE

Keep

Create

Keep

Error

MERGE_OVERWRITE

Keep

Create

Keep

Overwrite

MERGE_SAFE is the default — the safest option for incremental additions. OVERWRITE is the full-sync mode that makes the adapter match the file exactly.

Pick the mode that matches your intent:

  • Start with MERGE_SAFE for first-time imports and incremental additions.

  • Use MERGE_OVERWRITE when you need to update existing tags without removing unrelated ones.

  • Use OVERWRITE when the file is the single source of truth and the adapter must match it exactly.

  • Use CREATE to assert that the adapter starts empty and the file defines every tag.

  • Use DELETE to remove a known set of tags while keeping everything else.

Namespace URI Resolution

OPC UA namespace indices can change when a server restarts or its configuration changes. The namespace_uri column in the tag file carries the stable canonical namespace identifier. On import, HiveMQ Edge resolves the namespace URI against the live OPC UA server’s namespace table and updates the namespace index in the node ID when necessary. Exported tag files therefore remain portable across server restarts and namespace reconfigurations.

If the adapter is not connected at import time or the namespace URI is absent, the original node ID is used unchanged.

Example: Import CSV with MERGE_SAFE

curl -s -X POST \
  'http://localhost:8080/api/v1/management/protocol-adapters/adapters/my-opcua/device-tags/import?mode=MERGE_SAFE' \
  -H 'Content-Type: text/csv' \
  -H "Authorization: Bearer $TOKEN" \
  --data-binary @my-opcua-device-tags-edited.csv | jq .

Example: Import JSON with OVERWRITE

curl -s -X POST \
  'http://localhost:8080/api/v1/management/protocol-adapters/adapters/my-opcua/device-tags/import?mode=OVERWRITE' \
  -H 'Content-Type: application/json' \
  -H "Authorization: Bearer $TOKEN" \
  -d @my-opcua-device-tags.json | jq .

OVERWRITE mode deletes tags that exist on the adapter but are not in the file. Tags referenced by a data combiner cannot be deleted and trigger the TAG_IN_USE_BY_COMBINER validation error.

Multi-Mapping

A single tag can publish to multiple MQTT topics. To configure multiple northbound mappings for one tag, include multiple rows in the file that share the same node_id and tag_name.

Each row can contribute one northbound mapping (if northbound_topic is set) and one southbound mapping (if southbound_topic is set). Mappings are accumulated across rows and deduplicated by value, so a tag with multiple rows ends up with the union of distinct mappings.

If you intend exactly one southbound mapping per tag, set southbound_topic on one row only and leave it blank on the other rows in the same group.

Example: One tag with two northbound topics (CSV)
tag_name,tag_name_default,tag_description,northbound_topic,northbound_topic_default,southbound_topic,southbound_topic_default,southbound_field_mapping,max_qos,message_expiry_interval,include_timestamp,include_tag_names,include_metadata,mqtt_user_properties,node_path,namespace_uri,namespace_index,node_id,data_type,access_level,node_description
my-sensor,,Temperature,plant1/sensors/temp,,,,,,,,,,,,2,ns=2;s=Temp,Double,READ,
my-sensor,,Temperature,plant1/archive/temp,,,,,0,,,,,,,2,ns=2;s=Temp,Double,READ,

In this example, the tag my-sensor is created once. Two northbound mappings are created: one publishing to plant1/sensors/temp with the default QoS and one publishing to plant1/archive/temp with QoS 0.

Validation

Before any tag is created, updated, or deleted, the entire file is validated. Errors are collected and returned together so that you can fix them in as few edit cycles as possible. If any validation error is found, no mutations are applied.

A few row-level rules listed in Error Codes are checked by the import engine only in certain modes, and a small number are reserved for a future release (whitespace in tag names, MQTT wildcards in northbound topics). If you rely on one of these for data-quality enforcement, also validate client-side before calling the import endpoint.

Error Response Format

Validation errors are returned as a JSON object containing a summary and the full list of errors:

{
  "title": "Validation Failed",
  "detail": "2 validation error(s) found",
  "errors": [
    {
      "row": 3,
      "column": "tag_name",
      "value": "my sensor",
      "code": "INVALID_TAG_NAME",
      "message": "Tag name must not contain whitespace"
    },
    {
      "row": null,
      "column": "tag_name",
      "value": "sensor-1",
      "code": "TAG_CONFLICT",
      "message": "Tag for node 'ns=2;s=Temp' exists with different definition. Use MERGE_OVERWRITE mode."
    }
  ]
}

The row field is 1-indexed and corresponds to the data row in the file (excluding the header for CSV). row is null for file-level or cross-reference errors. The column field uses CSV field names for consistency across all formats.

Parse errors (malformed CSV, invalid JSON) return a simpler structure:

{
  "title": "Bad Request",
  "detail": "Failed to parse file: ..."
}

Error Codes

Table 11. Validation error codes
Code Phase Description

ADAPTER_NOT_FOUND

Pre-check

The specified adapter ID does not exist.

DUPLICATE_NODE

File-level

The same node_id maps to different tag names within the file. Multiple rows with the same node_id and the same tag_name are allowed for Multi-Mapping.

DUPLICATE_TAG_NAME

File-level

The same tag_name appears with a different node_id or description. Multiple rows with the same tag_name and identical definition are allowed for Multi-Mapping.

INVALID_TAG_NAME

Row-level

Tag name contains whitespace, exceeds 256 characters, or the tag description exceeds 1024 characters.

INVALID_TOPIC

Row-level

Topic is empty, exceeds 65,535 characters (MQTT specification limit), or contains null bytes. Northbound topics also must not contain MQTT wildcards (+, #).

INVALID_QOS

Row-level

QoS value is not 0, 1, or 2.

INVALID_EXPIRY

Row-level

Message expiry interval is outside the valid range of 1 to 4,294,967,295 seconds.

INVALID_FIELD_MAPPING

Row-level

Empty source or destination in a field mapping instruction.

INVALID_USER_PROPERTIES

Row-level

Empty key in an MQTT user property, or key/value exceeds the maximum length of 256 characters.

INVALID_NODE_ID

Row-level

Missing node_id on a row that defines a tag.

MAPPING_WITHOUT_TAG

Row-level

A northbound or southbound topic is set but no tag_name is provided.

WILDCARD_NO_DEFAULT

Row-level

A row uses the * (asterisk) wildcard but the corresponding _default field is empty or null.

TAG_CONFLICT

Cross-reference

A tag conflict exists between the file and the adapter. The specific condition depends on the import mode (for example, CREATE rejects tags that already exist; MERGE_SAFE rejects tags with changed definitions). Also triggered when a file tag name collides with an existing edge-only tag in MERGE_SAFE or MERGE_OVERWRITE modes.

TAG_IN_USE_BY_COMBINER

Cross-reference

A tag being deleted is referenced by a data combiner. Applies to DELETE and OVERWRITE modes.

UPDATE_FAILED

Post-mutation

The atomic configuration update failed.

Additional Workflows

Quickstart: OPC UA Tags to MQTT Using the REST API shows the most common path: browse, edit, import with MERGE_SAFE. The following workflows cover other common scenarios.

Full Sync

Make the adapter match the file exactly. Tags that are not in the file are deleted, new tags are created, and changed tags are overwritten:

curl -s -X POST \
  'http://localhost:8080/api/v1/management/protocol-adapters/adapters/my-opcua/device-tags/import?mode=OVERWRITE' \
  -H 'Content-Type: text/csv' \
  -H "Authorization: Bearer $TOKEN" \
  --data-binary @my-opcua-device-tags.csv | jq .

Accept All Defaults with Wildcards

To quickly import every browsed node with the generated defaults, set the editable fields to a single asterisk (*):

# Browse as JSON, then use jq to set wildcards on every row.
curl -s -X POST \
  'http://localhost:8080/api/v1/management/protocol-adapters/adapters/my-opcua/device-tags/browse' \
  -H 'Accept: application/json' \
  -H "Authorization: Bearer $TOKEN" \
  | jq '.rows[] |= (.tag.tag_name = "*" | .northbound.topic = "*")' \
  > my-opcua-wildcards.json

# Import -- wildcards resolve to the corresponding _default value.
curl -s -X POST \
  'http://localhost:8080/api/v1/management/protocol-adapters/adapters/my-opcua/device-tags/import?mode=CREATE' \
  -H 'Content-Type: application/json' \
  -H "Authorization: Bearer $TOKEN" \
  -d @my-opcua-wildcards.json | jq .

Scripted Pipeline

A Python script can read the browse output, apply a naming convention, and import the result. This pattern is useful for CI pipelines that provision HiveMQ Edge instances from a device inventory:

import requests
import json

EDGE = "http://localhost:8080"
ADAPTER = "my-opcua"
TOKEN = requests.post(
    f"{EDGE}/api/v1/auth/authenticate",
    json={"userName": "admin", "password": "hivemq"}
).json()["token"]

headers = {"Authorization": f"Bearer {TOKEN}"}

# Browse as JSON
browse = requests.post(
    f"{EDGE}/api/v1/management/protocol-adapters/adapters/{ADAPTER}/device-tags/browse",
    headers={**headers, "Accept": "application/json"},
).json()

# Keep only nodes under /Simulation/ and apply a naming convention.
# (Filtering by access_level would work too, but many simulation servers expose only READ signals.)
browse["rows"] = [
    row for row in browse["rows"]
    if row["node"]["node_path"].startswith("/Simulation/")
]
for row in browse["rows"]:
    row["tag"]["tag_name"] = "plant1-" + row["tag"]["tag_name_default"]
    row["northbound"]["topic"] = "plant1/" + row["node"]["node_path"].strip("/").lower()

# Import with MERGE_OVERWRITE to update existing tags
result = requests.post(
    f"{EDGE}/api/v1/management/protocol-adapters/adapters/{ADAPTER}/device-tags/import",
    params={"mode": "MERGE_OVERWRITE"},
    headers={**headers, "Content-Type": "application/json"},
    data=json.dumps(browse),
).json()

print(f"Created: {result['tagsCreated']}, Updated: {result['tagsUpdated']}, Deleted: {result['tagsDeleted']}")

Supported Adapters

Bulk device tag operations are available for any adapter that implements the BulkTagBrowser interface.

Currently, the following adapters support bulk operations:

For OPC UA, the browse operation performs an asynchronous recursive traversal of the server address space, collecting every variable node and reading its data type, access level, and description attributes. Continuation points are drained promptly so that resource-constrained PLCs (such as Siemens S7-1500) return complete results even for deep address spaces.