Skip to main content
Version: 0.1

Tag types

A tag type (the API calls it a UDT, user-defined type) describes a piece of equipment once, as a set of members and parameters, so that every motor, valve or sensor of that kind is one instance with a few parameter values instead of a hand-built folder of tags. This page is for anyone who builds types or reads instances through the API. Tags explains the tag kinds the members mirror.

Why types​

Without a type, forty identical motors are forty folders of hand-made tags with forty copies of the same node ids, scaling and alarms. With a type, each motor is one instance: its type says what members it has and how each gets its value, and the instance supplies the few things that differ (a device name, a node prefix, a server). Editing the type changes every instance. Alarms can be defined once on the type and resolved per instance.

Where types live​

Types are tags of kind udt-type under the reserved root @types, in folders you create (POST /api/v1/udt/folders). A type has a path such as @types.Motors.Motor, a name, optional documentation and a typeColor. The folder @types.built-in belongs to Scadex (see Built-in types). No type or folder of yours can take a name that starts with @. The API routes are under /api/v1/udt; see Tag types API.

Parameters​

A parameter is a named value every instance fills in. Each has a name, a dataType and a defaultValue. The dashboard offers the data types String, Integer, Float, Boolean, OPC-UA Server and MQTT Broker; the last two pick a connection, so a type can say {OPCUA} for its server and each instance chooses one.

An instance stores only the parameters it sets (parameters, a JSON map of name to value); anything else takes the type's default.

Members​

A member is the template for one tag under every instance. Its memberPath is its path relative to the instance (Run, Drive.Speed), and its kind is a tag kind: opcua, mqtt, external (any driver), memory, derived, expression, folder, or udt for a nested type. The fields mirror a tag's own:

FieldMeaning
producerModuleId, producerInstanceRefThe driver module and the connection, usually a parameter reference such as {OPCUA}
bindingThe driver's binding document, such as {"nodeId": "ns=3;s={ID}.Run"} for OPC UA or a topic for MQTT
dataTypeThe data type the source reports
sourceTag, property, sourceBitFor a derived member: what it follows, which field, which bit. sourceTag is usually relative: ./Status names a sibling member
expressionFor an expression member
memoryStartValueFor a memory member
engUnit, formatString, scaleMode and the other scaling fields, deadbandMode, deadbandAs on a tag's numeric object
tooltip, documentationDescription and notes
enabledThe default for every instance's member tag; absent means enabled
alarmsAlarm templates every instance inherits; see Alarm templates
typeRef, memberParamsFor a nested type member
overridesOn a type that extends another: a sparse patch of the parent's member; see Inheritance

Every string field may hold placeholders, described next.

Nested type members​

A member of kind udt embeds another type: typeRef names it by path, and its members appear under the member's own path as a prefix. A Motor type with a member Temp of type Analog gives each instance Temp.Value, Temp.Alarm and so on. memberParams sets the nested type's parameters, as literals or as references to the outer type's own parameters: {"OPCUA": "{OPCUA}", "ID": "{ID}.{TagName}"}. An instance can also set a nested member's parameter directly, with the prefix: a parameter Cond.ID on the instance reaches the nested type under Cond.

The root member​

A type can name one member as its rootMemberKey. On an instance of a type with a root member, the group's own row shows that member's value, quality and read time, so a group whose source is one structured value (an S7 structure, a JSON document) shows that whole value at its top and the other members read fields out of it. The dashboard labels the setting Root member. In a chain of extending types, the nearest root member set wins.

Placeholders​

A {Name} in any field of a member is replaced per instance. The lookup order for a name is: the instance's parameter values, then the built-in names, then the type's parameter defaults. A name nothing answers to, or an unknown modifier, leaves the whole placeholder text as typed, so a mistake is visible rather than silently blank. Substitution is a single pass: a value that comes out of one placeholder is not scanned for more.

Built-inValue
{InstanceName}The last segment of the instance's path
{InstancePath}The instance's whole path
{InstanceParent}Everything above the instance
{TagName}The last segment of the member's path
{TagPath}The member's whole path within the instance
{RootMember}The type's root member key

Names such as {ID} and {OPCUA} are not built in; they are parameters that many types happen to declare. A parameter with the same name as a built-in wins.

A placeholder can carry modifiers after a |: {InstanceName|lower}. The modifiers are upper, lower, trim, capitalize, title, snake, kebab, pascal and camel. The word-based ones split the value at non-alphanumeric characters and at camel-case boundaries, so HTTPServer_v2|kebab gives http-server-v2. Several modifiers chain: {ID|trim|upper}.

Inside a nested type the built-ins belong to the member being read: {TagName} in the nested dtl type is doc or YEAR. In the memberParams the outer type hands down, {TagName} and {TagPath} are the nesting member's own name and path.

Instances​

An instance is a tag of kind udt whose children are the type's members, materialized as real tags. Create one with POST /api/v1/udt/instances (typeId, a path or a parent path and name, and parameters), or from the dashboard. Each member tag carries udtInstanceId and udtMemberKey, and is otherwise an ordinary tag: it has a value, quality, history, alarms, and the runtime reads it like any other. Member configuration is not copied into the tag rows; it is resolved from the type at run time, so a change to the type reaches instances without rewriting each tag.

Per-instance overrides​

A member tag's overrides is a JSON map of the fields that this one instance changes, in the same names as a member (binding, producerInstanceRef, sourceTag, sourceBit, engUnit, enabled, alarms, ...). The dashboard shows a dot beside every overridden field, with Reset override to return to the type. A binding override merges into the type's binding by key, so changing the node of one member keeps the rest of the binding. An override always wins over the type and over any parent type.

Alarms can be overridden in two ways: alarmOverrides changes single fields of single alarms, keyed by the type's name for the alarm ({"High": {"severity": 200}}), and the rest still follows the type; an alarms key replaces the whole list for that instance, which then wins.

Re-syncing​

Editing a type re-syncs its instances, and the instances of every type that extends it: members the type defines are rebuilt, members it dropped are removed, and new members are added. POST /api/v1/udt/instances/{id}/sync does the same for one instance on demand. Changing an instance's parameters, name, place or type re-resolves every placeholder.

Inheritance​

A type can extend another: parentTypePath on create or update names the parent, and an empty string makes the type stand alone again. The dashboard calls this Extends. The child gets the parent's parameters and members at the same paths, so an instance of MotorWithSpeed has Run and Fault as an instance of Motor does, plus the Speed it adds. This differs from a nested type member, which puts the other type's members under a prefix. An instance of a child is accepted wherever its parent type is asked for, such as a screen parameter.

  • A child changes an inherited member with a sparse patch: a member row with overrides holding only the fields it changes, in the same names as a tag's overrides. Moving a member to another OPC UA node is {"binding": {"nodeId": "ns=4;s={Id}.Drive.Run"}}; the server, data type, scaling and alarms keep following the parent. A binding patch merges into the parent's binding by key, so changing an MQTT topic keeps its payloadFormat.
  • A row at an inherited path without overrides replaces the member in full. Inherited members can be disabled (enabled: false) but not removed. A patch must name a member the parent has, and cannot target a nested type.
  • Resolution order for a member tag: the parent's definition, then each child's patch down the chain, at most 8 types deep, then the instance's own overrides, which always win. Parameters merge by name and the child's default wins. The root member is the nearest one set.
  • A type's response has members and parameters as its own rows, and, only when it extends another, effectiveMembers and effectiveParameters with the merged view. Each effective member names the type that defines it in fromTypePath.
  • A type that others extend cannot be deleted.

Everything downstream reads the merged type, so substitution, materialization, alarms and derived or expression members need nothing of their own.

Tags added to an instance​

An instance can carry tags its type has no member for, anywhere inside it: beside its members, under a member, inside a nested type, or under another added tag. Create one with its path under the instance (POST /api/v1/tags, or Add tag on the instance, a member or a nested group in the tag browser), or drop an existing tag onto the instance. The gateway makes it the instance's: udtInstanceId names the instance and there is no udtMemberKey. The tree marks it as added, and GET /api/v1/udt/instances/{id} lists these as extraTags, apart from memberTags.

It is an ordinary tag in every other way: any kind, bindings, scaling, alarms, history, derived and expression tags that read siblings by a relative path. None of the type's machinery touches it:

  • A sync of the instance, or an edit of its type, keeps it. Only the type's members are rebuilt or removed.
  • It goes where the instance goes: moving or renaming the instance moves it, copying the instance copies it, deleting the instance deletes it. Duplicate makes another beside it.
  • It can be moved or copied into another instance, or out to anywhere else; a tag that leaves is nobody's again and keeps the values its placeholders had come to. An instance cannot hold another instance, and a member of an instance cannot be moved.
  • If the type later gets a member with the same name, the member is the type's and the added tag steps aside, with what is under it, to <name>_extra (_extra2, ...); the gateway logs a warning. If the type drops a member that held added tags, they move up to the top of the instance as <member>_<name>.

Placeholders in added tags. An added tag reads its instance's parameters as if it were part of the type. In its connection (producerInstanceId), its binding, its sourceTag, its property and its expression, a {Param} or built-in is replaced with the instance's value, using the scope the tag sits in: the instance's own at its top, and inside a nested type what the nesting member hands down. The tag's rows hold the resolved values, which is all the runtime reads. What was typed is kept as overrides.template and resolved again whenever the instance changes. The editor shows the typed text and, beside it, what it comes to.

Alarm templates​

A member's alarms is a list of alarm definitions (name, condition, setpoint, deadband, onDelayMs, severity, enabled) that every instance inherits for that member. name, setpoint and deadband may hold {Param} references, resolved per instance when the alarm service reloads. Two extra names describe the instance: {Description} is the instance's description (its first line of notes when it has no description, its name when it has neither) and {Documentation} its notes. So a type can name an alarm {Description}: Drive fault and each motor's alarm reads "C01 Exhaust fan: Drive fault" with nothing to override. A parameter of the same name wins. Changing an instance's description renames its alarms on the next reload.

Such an alarm shows inherited: true in the alarms API and is edited on the type or in the member tag's overrides, not through the alarms API. Alarms covers conditions, states and quality.

Built-in types​

Some types are the same on every project, so the gateway carries them. They live under @types.built-in, and a type response says builtIn: true. The gateway writes them at every start: added when missing, written again when this version defines them differently, left alone when they are the same, so a gateway on a newer version brings the newer definition and its instances re-read.

Nobody edits, moves, renames or deletes them, and nothing can be added to their folder: the API answers 400 ("built into Scadex") to the type endpoints, the folder endpoints, import into the folder, and deleting their nodes through /tags. The dashboard opens them read-only. Your own types use them like any other: as a nested member (typeRef), as an instance, or as the type another extends (parentTypePath).

dtl​

@types.built-in.dtl is the Siemens S7 DTL (date and time, long). Its parameters are ID (String) and OPCUA (OPC-UA Server).

MemberKindReads
docopcuaThe whole structure the controller sends: node ns=3;s={ID} on server {OPCUA}. It is the root member
YEAR, MONTH, DAY, WEEKDAY, HOUR, MINUTE, SECOND, NANOSECONDderived./doc, property {TagName}: each reads its own name out of the structure

WEEKDAY is 1 for Sunday through 7 for Saturday. All fields are formatted as whole numbers (0) except the nanoseconds. To use it, nest it in your own type with memberParams {"OPCUA": "{OPCUA}", "ID": "{ID}.{TagName}"}.

Import and export​

Types export as JSON (GET /api/v1/udt/types/export, type: "udt-type-export", version: 1), each with its path relative to the exported folder, its parameters, members, root member and parentTypePath as an absolute path. Import (POST /api/v1/udt/types/import) creates parents first and rebases a parent that is in the same file. Import into @types.built-in is refused.

Converting an OPC UA variable​

The OPC UA driver can make a type out of a node on a server: from the child variables of an object, or from the fields of one structured variable, in which case the result says fromStructure: true and lists any skipped fields. The dashboard offers this from the server's browse tree; see OPC UA.

Types in the dashboard​

Types are a group of their own on the Tags page (older links to a separate types page redirect there). New type creates one; the type editor opens in the side panel with Type structure (the member tree, New member, New folder, Nested type, Filter members), Parameters, Root member, Extends and Documentation. Each member's kind is one of OPC UA, MQTT, Memory, Derived, Expression or Nested type, with the same fields a tag has. Selecting an instance shows its parameters and the type's structure; selecting a member tag shows the resolved fields with an override dot where the instance differs. A type's instances are listed on its Instances tab.