Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

  • The theme-first mental model (no hex unless it carries process meaning)
  • write_objects mechanics on DisplaysList (document objects, read-before-write)
  • The build loop (write_objectsget_state → check errorList → fix → move on)
  • Binding and compile-time gotchas that hit every display (UDT inheritance, system tag columns)syntax that hits every display
  • Spacing, typography, and minimum sizing tokens shared between Canvas and Dashboard
  • Symbol-writing rules (Wizard, Library, Solution prefixes)

...

Tools you use over and over

ToolWhen
get_table_schema('DisplaysList')Once at start of session to confirm field names
list_elements('ThemeColors')Once to get the brush catalogue
list_elements('<ElementName>')Before using an element type you have not used this session
list_dynamics() / list_dynamics('<Type>')Before attaching a dynamic you have not used this session
get_objects('DisplaysList', names=['X'], detail='full')Before every modification to an existing display (document-object rule)
write_objects(table_type='DisplaysList', data=[...])After your plan is complete
get_state(target='designer')After every write, to check errorList

Section 1 — Theme-first mental model

...

Hex is reserved for things with domain meaning that shouldn't change across themes — heat = red, water = blue, alarm = red, batch progress = amber. Everything else should adapt to whichever of the 13 theme pairs the operator is running.

The 13 theme pairs

Light / Dark pairUse for
Light / DarkDefault office / default control room
Platinum / OnyxRefined corporate office / premium control room
Steel / GraphiteIndustrial office / industrial control room
Pearl / IndigoSoft UI emphasis, OEM branding
Sky / NavyRefreshing / deep contrast
Gold / CoffeeWarm accents for specialty applications
ContrastLight / ContrastDarkAccessibility, outdoor tablets, low-vision

Theme is switched at runtime via @Client.Theme = "Dark". Every properly-themed display reflows automatically.

...

Call list_elements('ThemeColors') for the authoritative list. The 12 brushes you'll use 90% of the time:

BrushMeaningTypical use
PanelBackgroundCard / section backgroundBackground Rectangle inside a zone
PageBackgroundFull-page backgroundThe display-root Background override
ControlBackgroundControl bodyGauge / chart / data-grid background
TextForegroundPrimary textHeadings, values, labels
TextSubtleForegroundMeta textUnits, captions, "UPDATED AT" stamps
TextAccentForegroundHighlighted textSection titles, accent links
AccentBrushThe solution's accent colorActive-state markers, selected-row borders, links
StateGreenRunning / OK stateIndicator fills, live-data values
StateRedStopped / fault stateIndicator fills, alarm text
StateAlarmActive alarm (yellow)Banded gauge danger zones, pulsing elements
OnFill / OffFillHPG two-state fillsStatus-indicator shape fills tied to a boolean
WaterProcess waterPipe runs carrying water/aqueous streams

Other brushes exist and are valid — ElementBlue, ElementGreen, AlarmHighPriority, ColorCyan, ColorSlate, ColorAmber, ColorTeal, ColorCoral, ColorPurple, ColorDimmed, PopupBackground, DefaultFill, DefaultStroke, DefaultBorder, and more. Check list_elements('ThemeColors').

?? Known issue (TDEV-1275)Note on Element*-family brushes: ElementRed, ElementOrange, ElementYellow are silently dropped at write time on Ellipse (and possibly other shape types). Prefer may not apply consistently on all shape types. If you don't see your intended color, prefer StateRed / ColorAmber / StateAlarm instead until resolved.

How theme properties work on the wire

...

Every newly created display gets "Background": "#FFFAFAFA" (light gray) baked in. If you're building for a dark theme, this shows through at every gap or transparent edge. Always set the root Background explicitly:

...

{
  "Name": "MyDisplay",
  "PanelType": "Canvas",
  "Size": "1600 x 900",
  "Width": 1600, "Height": 900,
  "Background": "theme:PageBackground",
  "Elements": [ /* ... */ ]
}

Or, for a full-bleed dark background on a dark-themed display, place a full-size Rectangle with FillTheme: "PageBackground" as the first element.

...

The canonical display envelope

...

languagejson
{
  "Name": "MyDisplay",
  "PanelType": "Canvas",
  "DisplayMode": "Page",
  "Navigate": "true",
  "Size": "1600 x 900",
  "OnResize": "StretchFill",
  "Width": 1600,
  "Height": 900,
  "Background": "theme:PageBackground",
  "Elements": [ /* ... */ ]
}

Non-negotiable rules

  • The identifier field is Name. Never ObjectName.
  • PanelType is required. "Canvas" or "Dashboard". Omitting it silently defaults to Canvas.
  • No JsonFormat wrapper. All properties at the top level.
  • DisplayMode controls window style: "Page" (normal), "Dialog" (modal popup), "Popup" (floating non-modal), "PopupWindow" (separate window — not available on Web, avoid unless explicitly requested).
  • OnResize: "StretchFill" is the sensible default. Use "NoAction" for fixed-pixel displays.
  • DisplaysDraw is NOT a writable table. It is the Designer visual editor UI. Use DisplaysList for all display create/edit operations.

...

get_state on a display returns compile errors as a list:

...

languagejson

...

{
  "errorList": [
    {
      "ID": 0,
      "ErrorCode": "BC30456",
      "IsWarning": false,
      "Line": 8,
      "Column": -1,
      "Location": "Uid_41_TTextBlock_LinkedValue_e1",
      "ErrorText": "error BC30456: 'Status' is not a member of 'UserType'."
    }
  ]
}

The Location string tells you which element (by Uid) and which property (LinkedValue, Expression, etc.) has the problem. Use it to find and fix the exact property.

...

write_objects success IS the confirmation. The errorList check covers compile correctness. Screenshots are only for sharing visual context with the user — not for the AI to confirm its own work.

Section 4 — Binding

...

syntax

@Tag. is the only binding prefix you'll use in displays

...

The single most important text pattern. Instead of two TextBlocks (value + unit) side by side, use ONE with composite LinkedValue:

...

{
  "Type": "TextBlock",
  "LinkedValue": "Temperature: {@Tag.Reactor/Temperature_C} °C",
  "FontFamily": "Inter",
  "FontSize": 14,
  "Width": 220, "Height": 22
}

Patterns:

  • "Flow: {@Tag.X} GPM" — label + value + unit
  • "{@Tag.X} NTU" — value + unit
  • "pH: {@Tag.X}" — label + value
  • "Operator: {@Tag.Shift/CurrentOperator}" — label + bound string

UDT inheritance limitation — TDEV-1272

When a tag is typed by a UDT with a BaseUserType, members defined on the base UDT DO NOT resolve in LinkedValue bindings.

Example: S88_Unit has BaseUserType = S88_Equipment. S88_Equipment declares Status, AssetTag, Location, Description.

  • ? {@Tag.PharmaCo/Plant/CellA/Mixer_M101/Attr.Status} — fails with BC30456: 'Status' is not a member of 'UserType'
  • ? {@Tag.PharmaCo/Plant/CellA/Mixer_M101/Attr.Running} — works (Running is defined directly on S88_Unit)

Workaround: bind only to members defined directly on the tag's UDT, not inherited members. Redeclare common members on each derived UDT if necessary (defeats the purpose of inheritance but it's the only fix until TDEV-1272 ships). The runtime object model DOES inherit correctly — this is purely a design-time binding compiler gap.

Tag-row system columns not bindable — TDEV-1273

Columns on the UnsTags row that aren't UDT members (like SourceIri) cannot be bound:

  • ? {@Tag.<path>/Attr.SourceIri} — fails with BC30456: 'SourceIri' is not a member of 'UserType'

Workaround: use literal text. For ontology-aware displays, hardcode the SourceIri value from the UDT/tag definition into the display.

Polygon / Polyline / Gridline / Spline silently skip on missing Points

If you write a Polygon without Points, nothing renders and no error is produced — the element is simply invisible.

...

languagejson

Polygon / Polyline / Gridline / Spline silently skip on missing Points

If you write a Polygon without Points, nothing renders and no error is produced — the element is simply invisible.

// 

...

Silently invisible
{ "Type": "Polygon", "Left": 100, "Top": 100, "Width": 50, "Height": 50 }

// 

...

Renders correctly
{ "Type": "Polygon", "Left": 100, "Top": 100, "Width": 50, "Height": 50, "Points": "25,0 50,50 0,50" }

Polygon auto-closes (last point connects back to first). Polyline doesn't. For pipe runs use Gridline (constrains to horizontal/vertical segments — the P&ID convention).

...

The writer normalizes several shapes on save. When you later read the display back, you get the normalized form:

You wroteStored as
{"FillTheme": "ElementBlue"}{"Fill": "theme:ElementBlue"}
{"Type": "Cylinder", Left, Top, Width, Height}{"Type": "Path", "Data": "M 0,20 C ..."}
{"Type": "Hexagon", ...}{"Type": "Polygon", "Points": "50,0 100,25 ..."}
{"Type": "SvgGroup", "SvgContent": "<svg>..."}{"Type": "ShapeGroup", "Children": [...]}
{"Pens": [{TrendPen}]}Flat array OR wrapper — writer accepts both

Consequence: when round-tripping a display for edits, don't expect to see "Type": "Cylinder" — you'll see a "Path" with auto-generated Data. Edit the Path, or add new Cylinders alongside (they normalize too).

Section 5 — Spacing and typography tokens

Spacing scale

TokenPixelsWhere
xs4Gap between label and value in a stacked pair
sm8Gap between sibling sub-elements within a card
md16Gap between sibling cards in a row
lg24Card interior padding (content vs card edge)
xl32Zone padding (zone background Rectangle vs content)
2xl48Major section separator
3xl64Display-level margins

Typography ramp

Use FontFamily: "Inter" (or the solution's chosen font) universally.

RoleFontSizeWhen
Hero26–32Display title, single-metric hero number
H120–22Main section headings
H216Sub-section headings, card titles
Body13–14Bound values, primary text
Meta10–11Labels, units, captions
Micro9Timestamps, very-low-priority meta

Minimum sizes (hard floors)

ElementMinRecommended
Button100×32130×40
TextBox / NumericTextBox120×28160×32
ComboBox160×28200×32
Slider200×28260×32
ToggleSwitch60×2880×32
CircularGauge / RadialGauge150×150180×180
SemiCircle200×120240×140
LinearGauge (horizontal)260×80300×100
CenterValue120×120140×140
TrendChart400×200500×300
BarChart300×200400×240
PieChart200×200240×240
AlarmViewer400×220600×240
AssetsTree200×300240×400
DataGrid400×200600×300
Wizard symbol (TANK/PUMP/etc.)60×6080×80

Color hex fallbacks (for process meaning only)

RoleHex
Heat / reaction#FFEF4444 (red)
Cold / water#FF38BDF8 (cyan)
Running / active#FF34D399 (green)
Alarm / warning#FFF59E0B (amber)
Accent / link / data#FF38BDF8 or theme AccentBrush
Text on dark#FFF3F4F6 or theme TextForeground
Meta text#FF64748B or theme TextSubtleForeground

Section 6 — Element types overview

This tells you what exists. Canvas and Dashboard skills cover how to use each category.

CategoryMembersCanvas?Dashboard?
ShapesRectangle, Ellipse, Polygon, Polyline, Path, Gridline, Spline?
First-class auto-shapesCylinder, Gear, Arrow, Cloud, Star, Hexagon, Pentagon, Trapezoid?
ContainerShapeGroup, SvgGroup, Group?
InteractionTextBlock, Label, Button, CheckBox, ComboBox, DataGrid, ListBox, NumericTextBox, PasswordBox, PushButton, RadioButton, Slider, TextBox, ToggleSwitch??
GaugesCenterValue, CircularGauge, Compass, DigitalGauge, LinearGauge, RadialGauge, RangeCircular, SemiCircle??
ChartsBarChart, DigitalMeter, DrillingChart, PieChart, PieChartPlus, Timeline, TrendChart, XYChart??
ViewerAlarmViewer, AlarmAreas, AssetsTree, Carousel, ChildDisplay, Expander, FlowPanel, MapsOSM, PdfViewer, ProgressBar, TabControl, WebBlazor, WebBrowser??
EditorsDatePicker, DateTimePicker, TimePicker, MediaElement, MenuItem, PageSelector??
DashboardCell?
IndustrialIconsIndustrialIcon (character-code icon font)??

?? Avoid (phantom entries, TDEV-1274): Triangle, Octagon, Parallelogram, Hill, Curve, Senoid, Wave, T-Junction. Listed by list_elements() but cannot be created — neither Type: "X" alone nor with SymbolName: "Wizard/X" worksCall list_elements() for the authoritative, up-to-date catalog of every element type in the current release. When in doubt, always discover at runtime rather than relying on a static list.

Section 7 — Symbols (all three sources)

Every symbol uses Type: "Symbol". Not Type: "Pump", not Type: "Valve". One element type, many SymbolName values.

...

...

languagejson
{
  "Type": "Symbol",
  "SymbolName": "Wizard/PUMP",
  "Left": 400, "Top": 300,
  "Width": 80, "Height": 80,
  "SymbolLabels": [
    { "Type": "SymbolLabel", "Key": "State", "LabelName": "State", "LabelValue": "@Tag.Pump1/Running", "FieldType": "Expression" },
    { "Type": "SymbolLabel", "Key": "RPM",   "LabelName": "RPM",   "LabelValue": "@Tag.Pump1/Speed",   "FieldType": "Expression" }
  ]
}

The 5 Wizard symbols

The complete Wizard catalog is 5 symbols:

SymbolNameWhat it isTypical SymbolLabels
Wizard/BLOWERIndustrial blower/fanState, RPM
Wizard/MOTORElectric motorState, RPM
Wizard/PUMPPump (styles selectable in Designer)State, RPM
Wizard/TANKStorage tankLevel, Alarm
Wizard/VALVEValve (various styles)State, Position

Anything else you see in legacy docs (Wizard/Triangle, Wizard/Curve, etc.) does not exist — see TDEV-1274Always treat list_elements('Wizard') as the authoritative runtime catalog — if an entry does not appear there, do not attempt to use it.

Library symbols (~1600)

Call list_elements('Library/HMI') or specific subfolders (Library/HMI/Pumps, Library/HMI/Valves, etc.) to discover. Library symbols auto-import on first reference:

...

...

languagejson
{ "Type": "Symbol", "SymbolName": "Library/HMI/Pumps/CentrifugalPump", "Left": 200, "Top": 200, "Width": 120, "Height": 80 }

Solution symbols (your own)

User-created symbols in the current solution use Solution/ prefix:

...

languagejson
{ "Type": "Symbol", "SymbolName": "Solution/MyCustomTank", "Left": 100, "Top": 100, "Width": 80, "Height": 100 }

Sizing

All symbols scale to any proportional size. Default is 65×65 — use 40×40 for compact, 80×80 for medium, 120×120 for large. Maintain aspect ratio; don't stretch asymmetrically.

...

Layout regions: Header, Footer, Menu, Submenu, Content. The Startup layout defines which display loads into each region.

...

get_table_schema('DisplaysLayouts')
get_objects('DisplaysLayouts', names=['Startup'], detail='full')

Asset navigation

For plant-wide navigation with dynamic content:

...

Section 10 — Common pitfalls

MistakeFix
Hardcoding hex without thinking about theme switchingUse *Theme properties; reserve hex for process-meaning

Binding to an inherited UDT member (TDEV-1272)

Bind to direct members only; redeclare common members on derived UDTs

Binding to .SourceIri or other system columns (TDEV-1273)

Use literal text

process-meaning
Polygon / Gridline without PointsAlways include Points — min 3 for Polygon, 2 for Polyline/GridlineUsing Triangle / Curve / Hill / T-Junction (TDEV-1274)

Phantom entries — use Class A shapes or compose from primitives

Using ObjectName instead of NameField is always Name
Using DisplaysDraw as table_typeVisual editor UI, not a writable table. Use DisplaysList
Omitting PanelTypeRequired — "Canvas" or "Dashboard"
Wrapping the envelope in JsonFormatProperties go at top level, no wrapper
Partial write on an existing displayAlways read-modify-write the complete document
Using @Label.X in a display-element binding@Label. is for symbol internals only — use @Tag.
Setting colors without clearing themeSet value AND clear theme: {Fill: '#FF3498DB', FillTheme: ''}
Relying on a static element/symbol listAlways call list_elements() / list_dynamics() to get the authoritative catalog at runtime

Section 11 — Quick reference

...

# Session startup
get_table_schema('DisplaysList')
list_elements('ThemeColors')

# Before first use of a type this session
list_elements('<ElementName>')
list_dynamics('<DynamicName>')

# Read-modify-write an existing display
get_objects('DisplaysList', names=['X'], detail='full')
write_objects(table_type='DisplaysList', data=[...])
get_state(target='designer')  # verify errorList empty

Display envelope template:

...

{
  "Name": "MyDisplay",
  "PanelType": "Canvas",
  "DisplayMode": "Page",
  "Navigate": "true",
  "Size": "1600 x 900",
  "OnResize": "StretchFill",
  "Width": 1600,
  "Height": 900,
  "Background": "theme:PageBackground",
  "Elements": []
}

Next skills

After the basics are internalized, load the paradigm-specific skill:

...