Tags
A tag is one named value the gateway keeps current: a reading from a device, a number someone typed, a result computed from other tags, or a figure about the gateway itself. This page explains what a tag is made of and how its value comes to be, for anyone who configures tags or reads them through the API or the SDK.
Identity and paths
Every tag has two identifiers:
| Field | What it is |
|---|---|
id | A stable public id (a UUID). It never changes, even when the tag is renamed or moved. Other records point at the tag by this id. |
path | The tag's place in the tree, dot-separated: Line1.Motor01.Speed. The last segment is the name; everything before it is the parentPath. |
Folders are tags too, with kind folder. A folder has no value; it only groups what sits under it. Tags can also sit under other tags, not only under folders: the tree in the dashboard shows a tag's children under it, which is how a status word and the bit tags that read it stay together.
Inside the gateway each tag also has a numeric id for the hot path. Those are never recycled. You see only the public id and the path.
Reserved names
A name that starts with @ is reserved for the gateway. Two roots exist:
@typesholds the tag types (UDTs). Types and their folders live under it; see Tag types. The folder@types.built-inis written by Scadex itself and cannot be changed.@gatewayholds the gateway's own diagnostics and the system tags described under System tags and gateway diagnostics.
You cannot create a tag or folder whose name starts with @.
Tag kinds
The kind field says where a tag's value comes from. The runtime treats each kind differently; the fields that matter for one kind are grouped under a sub-object of the same name in the API (memory, derived, expression, binding).
| Kind | Value comes from | Writable |
|---|---|---|
memory | Whatever was last written to it, starting from memoryStartValue | Yes |
driver kinds (opcua, mqtt, external) | A driver module, through the tag's binding | If the driver allows it |
derived | Another tag: its whole value, one property of it, or one bit | Passed through to the source, with limits |
expression | A JavaScript expression over other tags | No |
system | The gateway itself (@gateway.performance.*) | No |
folder | Nothing; a grouping node | No |
udt | An instance of a tag type (UDT); its members are ordinary tags under it | Per member |
udt-type | A tag type definition under @types | No |
Memory tags
A memory tag holds a value the gateway stores. It starts at memoryStartValue and keeps the last value written to it in memoryValue. Setting memoryValue to null through the API resets the tag to its start value.
A memory tag has a scope:
| Scope | Where the value lives | Who sees it |
|---|---|---|
gateway (default) | The event backbone, with memoryValue kept for reset and rebuild | Everyone |
user | A per-user row (TagMemoryUserValue) | That user's own /sync connections |
session | The /sync connection itself | Nobody else; gone when the connection closes |
User- and session-scoped values are private. They never enter the shared event timeline, so they have no replay and no history, and scaling does not apply to them. Their only shared event is the start value, which is also what derived and expression tags read. A session-scoped tag can only be written over /sync.
Driver-bound tags
A tag that a driver module supplies is owned through a neutral tuple, the binding:
| Field | Meaning |
|---|---|
moduleId | The module that supplies the value, such as io.scadex.driver.opcua |
instanceId | The connection inside that module (a server, a broker). May be null |
configuration | A JSON document the driver understands: a node id, a topic and payload format, an address |
Core stores the configuration as is and never interprets it. It does not know what an OPC UA node id or an MQTT topic is; only the driver does. This is why Core has no driver-specific tables or controllers, and why a third-party driver gets the same tag workflow as the built-in ones. When you create a tag through the API, the fields are producerModuleId, producerInstanceId and binding.
The kinds opcua and mqtt are the two built-in drivers' tags; the API presents their binding fields under opcua (serverId, nodeId) and mqtt (mqttServerId, topic, payloadFormat) for convenience. The kind external is any other driver's tag, with only the generic binding.
When the module that owns a tag is stopped, or removed, the tag stays. It is marked Bad with the detail "The module that supplies this value was stopped." and keeps its last value until the module runs again. Tags are never deleted by a module's lifecycle. See Architecture for the module boundary and the modules section for lifecycle.
Derived tags
A derived tag follows a source tag named by sourceTag, which is an absolute path or a relative one (./Sibling, ../Other; a single . is the tag this one sits inside). It carries the source's quality and timestamp. Three forms exist:
- The whole value. No
propertyand nosourceBit. A write to the derived tag goes through to the source. - One property of a structured value.
propertynames a field of an object or an element of an array, such asmotor.speedorrows[0][2]. A write is refused: the tag reads a part, and the gateway cannot rebuild the whole. - One bit of a word.
sourceBitis a bit number from 0 (the lowest) to 63, and the tag readstruewhile that bit is 1. The source has to hold a whole number; a signed word keeps its bit pattern and an unsigned one reads up to bit 63. A write sets or clears only that bit: the gateway reads the word, changes the bit and writes the whole word back. Two writes to different bits close together are applied one after the other and both stay. Anything the source changes between the read and the write is lost, as always when a word is shared this way. SettingsourceBitto-1through the API stops following a bit.
A derived tag whose source has no whole-number value cannot read a bit, and says so in qualityDetail.
Expression tags
An expression tag is a JavaScript function body, so it needs return. It reads other tags with tag(path) and tags[path]. Only literal paths in tag('...') count as dependencies; a computed path reads as undefined. The result takes the worst quality of its inputs. A script error, a result that is not a finite number, or a dependency cycle gives Bad quality with the message in qualityDetail, and the tag keeps its last value.
Expressions run in a sandbox in separate worker processes, with limits of 50,000 statements, 16 MiB and 250 ms of CPU. Expression tags covers the language and the editor; the scripting section covers the sandbox.
System tags and gateway diagnostics
Two kinds of @gateway tags exist, and they behave differently.
@gateway.performance.memory, managedHeap, cpu, threads and busyPoolThreads are real tags of kind system. Core creates them at start and publishes a value every ten seconds through the tag runtime, so they have history like any tag, can carry alarms, and can be trended on a screen. Their units are their own (MB, %). Nobody can edit, move, delete or write them.
The other @gateway tags live only in memory, are read-only, and have no history: @gateway.version, @gateway.uptime (unit s), @gateway.module.count, and for each module @gateway.module.<id>.id, .name, .version, .state and .error, with dots in the module id replaced by _. Names are lowercase.
@gateway.time is the gateway's clock as an ISO-8601 string in the gateway's zone, to the whole second, and @gateway.tz is that zone. The clock is not stored: a replay produces it from its own position. The dashboard's clock is this tag.
Values, quality and timestamps
Each value a tag carries has three parts:
| Part | Meaning |
|---|---|
value | The current value. Numbers, booleans, strings, byte arrays and JSON structures (objects, arrays) are all possible. |
quality | Good, Uncertain or Bad, with an optional qualityDetail saying why |
timestamp | When the source produced the value, in Unix milliseconds on /sync and in the REST API |
Data types
The runtime's value kinds are Null, Boolean, Int64, UInt64, Double, String, Bytes and Json. A structured value (an object a controller sends over OPC UA, a JSON payload from MQTT) is one value of kind Json; a derived tag with a property reads a field out of it, and a tag type can name such a member as the one that holds the whole structure (see Tag types). The dataType field on a tag is a number the driver reports from the source, shown in the Data type column; it is kept as metadata.
Quality
| Quality | Meaning |
|---|---|
Good | The value can be trusted |
Uncertain | The value is there but something is wrong with how it was produced; alarms can go active on it but not clear |
Bad | The value cannot be trusted. The last value is kept so you can see what it was |
What makes a tag Bad or Uncertain:
- The module that supplies it was stopped or removed (Bad, "The module that supplies this value was stopped.").
- Its expression threw, returned something that is not a finite number, or depends on itself (Bad, with the script error in
qualityDetail). - Its scaling configuration is wrong, such as equal raw limits (the source value passes through Uncertain, "Scaling is not applied: ...").
- A bit tag whose source is not a whole number (no bits to read).
- The driver itself reports a bad or uncertain status from the device.
Alarms treat quality as described in Alarms: an Uncertain value can raise an alarm but not clear it, and a Bad value changes nothing and marks outstanding alarms stale.
Scaling and deadband
Scaling turns the source's raw value into an engineering value. It runs inside the tag runtime before the value gets its sequence number, so the event timeline, the current state, /sync, replay and history all hold the engineering value. The raw value travels with it as rawValue.
The settings are in the tag's numeric object:
| Field | Meaning |
|---|---|
scaleMode | off, linear, squareRoot, exponentialFilter, bitInversion or map |
rawLow, rawHigh | The raw range for linear and squareRoot |
engLowLim, engHighLim | The engineering range (default 0 to 100) |
engLimitMode | noClamp (default) or clamp: whether the result is held inside the engineering range |
scaleFactor | The factor for exponentialFilter |
bitCount | The word width for bitInversion |
scaleMap | For map: a JSON array of { raw, scaled } pairs |
deadbandMode | off, absolute or percent |
deadband | The deadband size |
A deadband is checked after scaling. A value that moves less than the deadband from the last one produces no event at all. It never suppresses a quality change or a write. In percent mode the percentage is of the last value.
Writes are inverse-scaled before they reach the source. A write is refused rather than altered when the value is outside a clamped range, when the limits are equal, or when a value map has no single raw value for it. An exponential filter has no inverse, so a write passes through as written.
Bad scaling configuration passes the source's value through as Uncertain and refuses writes. Filter state and the deadband's last value are in memory, so both restart with the gateway.
Units, format and description
| Field | Where | Meaning |
|---|---|---|
engUnit | numeric | The engineering unit, shown beside the value |
formatString | numeric | How the value is formatted for display, such as 0 for a whole number |
tooltip | meta | A short description, shown in the Description column |
documentation | meta | Longer notes |
enabled | tag | Whether the runtime reads and publishes the tag |
History settings
A tag also carries a history row (enabled, storageProvider, sampleMode, minTimeBetweenSample, maxTimeBetweenSample, deadband style and mode). These settings are stored but nothing acts on them yet: the historian records every accepted value of every tag, and history reads come from the historian or the event backbone, not from this table. See History and replay.
Enabling and disabling
A disabled tag (enabled: false) is not read and publishes nothing; the dashboard shows it as Disabled. Disabling a tag clears anything outstanding on its alarms, with the reason recorded in alarm history. A tag type member has an enabled default that an instance can override.
Writes
Every change to a tag's value goes through one path in the runtime, whether it comes from a screen, the REST API, a script or a module. Clients never talk to a driver directly.
- A write needs the operator role or higher. On
/syncit istag.write {path, value}; over REST it isPOST /api/v1/runtime/tags/{id}/write. - Every write is recorded against the principal who asked for it before it is attempted, with the outcome (
accepted,rejected,failedorpending). A write that cannot be recorded is not attempted. The dashboard shows a tag's writes in its Audit tab, for admins. See Audit log. - A write to a scaled tag is inverse-scaled first, and may be refused, as described above.
- A write to a derived tag that follows a property is refused; one that follows a bit changes only that bit.
- Writes are refused while the connection is in a replay.
POST /api/v1/runtime/tags/{id}/valuesinjects a reading as if the source had reported it. It needs the engineer role and is audited with operationpublish.
A value becomes current only after the event backbone has durably accepted it, and only then do subscribers, alarms and the historian see it. Architecture follows that path.
The tag browser
The dashboard's Tags page shows every tag in one tree with three groups: the gateway's own tags, the types under @types, and the project's tags. Selecting a row opens it in a side panel beside the tree; there are no modal editors.
Columns. Tag, Description, Quality, Current value, Data type and Timestamp. Value, quality and timestamp are live, over /sync. Quality shows as Good, Disabled, No value, or the detail of an Uncertain or Bad value.
Tree. Folders open and close. An instance of a tag type shows its members under it, and tags that were added to an instance are marked as added. A tag sits under the tag it belongs to, not only under folders: the bit tags of a status word are listed under the word, in bit order, lowest first, and the word shows a bit map of which bits are read.
Search and filters. Search tags finds a tag by name or path. Filters narrow the tree by kind: OPC UA, MQTT, Memory, Derived, Expression, UDT instance, Driver.
Menu actions on a row or a folder: Add tag, New folder, New type, Duplicate, Copy, Enable, Disable, Delete. Rows can be dragged to move them; the API behind that is POST /api/v1/tags/{id}/move with the new parent path, and a copy lands through POST /api/v1/tags/{id}/copy with a new name and target parent. Deleting a folder or an instance deletes everything under it.
Side panel. The editor has sections for general settings, the binding and scaling, and tabs for the type's parameters and instances where they apply, the tag's history, its write audit (admins), its metadata and its raw JSON. Changing a field saves it to the tag; a member of a type instance shows a dot beside each field that the instance overrides, so you can see what differs from the type.
Restart. Restart on a tag, a folder, an instance or a type makes the source send its current value again: drivers re-create their subscriptions and computed tags are rebuilt (POST /api/v1/tags/{id}/restart, operator).
Import and export
A subtree can be exported as JSON (GET /api/v1/tags/export?path=) and imported under another path (POST /api/v1/tags/import with targetPath and the exported data). The file carries each tag's path relative to the exported root, its kind, binding, scaling and metadata (version: 1, type: "tag-export"). Types have their own export, described in Tag types. The REST details are in Tags API.