...
write_objects mechanics on DisplaysList (document objects, read-before-write)write_objects → get_state → check errorList → fix → move on)...
| Tool | When |
|---|---|
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 |
...
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.
| Light / Dark pair | Use for |
|---|---|
Light / Dark | Default office / default control room |
Platinum / Onyx | Refined corporate office / premium control room |
Steel / Graphite | Industrial office / industrial control room |
Pearl / Indigo | Soft UI emphasis, OEM branding |
Sky / Navy | Refreshing / deep contrast |
Gold / Coffee | Warm accents for specialty applications |
ContrastLight / ContrastDark | Accessibility, 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:
| Brush | Meaning | Typical use |
|---|---|---|
PanelBackground | Card / section background | Background Rectangle inside a zone |
PageBackground | Full-page background | The display-root Background override |
ControlBackground | Control body | Gauge / chart / data-grid background |
TextForeground | Primary text | Headings, values, labels |
TextSubtleForeground | Meta text | Units, captions, "UPDATED AT" stamps |
TextAccentForeground | Highlighted text | Section titles, accent links |
AccentBrush | The solution's accent color | Active-state markers, selected-row borders, links |
StateGreen | Running / OK state | Indicator fills, live-data values |
StateRed | Stopped / fault state | Indicator fills, alarm text |
StateAlarm | Active alarm (yellow) | Banded gauge danger zones, pulsing elements |
OnFill / OffFill | HPG two-state fills | Status-indicator shape fills tied to a boolean |
Water | Process water | Pipe 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.
...
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.
...
...
| language | json |
|---|
{
"Name": "MyDisplay",
"PanelType": "Canvas",
"DisplayMode": "Page",
"Navigate": "true",
"Size": "1600 x 900",
"OnResize": "StretchFill",
"Width": 1600,
"Height": 900,
"Background": "theme:PageBackground",
"Elements": [ /* ... */ ]
}Name. Never ObjectName.PanelType is required. "Canvas" or "Dashboard". Omitting it silently defaults to Canvas.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:
...
| language | json |
|---|
...
{
"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.
...
@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 stringWhen 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.
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.
If you write a Polygon without Points, nothing renders and no error is produced — the element is simply invisible.
...
| language | json |
|---|
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 wrote | Stored 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).
| Token | Pixels | Where |
|---|---|---|
| xs | 4 | Gap between label and value in a stacked pair |
| sm | 8 | Gap between sibling sub-elements within a card |
| md | 16 | Gap between sibling cards in a row |
| lg | 24 | Card interior padding (content vs card edge) |
| xl | 32 | Zone padding (zone background Rectangle vs content) |
| 2xl | 48 | Major section separator |
| 3xl | 64 | Display-level margins |
Use FontFamily: "Inter" (or the solution's chosen font) universally.
| Role | FontSize | When |
|---|---|---|
| Hero | 26–32 | Display title, single-metric hero number |
| H1 | 20–22 | Main section headings |
| H2 | 16 | Sub-section headings, card titles |
| Body | 13–14 | Bound values, primary text |
| Meta | 10–11 | Labels, units, captions |
| Micro | 9 | Timestamps, very-low-priority meta |
| Element | Min | Recommended |
|---|---|---|
| Button | 100×32 | 130×40 |
| TextBox / NumericTextBox | 120×28 | 160×32 |
| ComboBox | 160×28 | 200×32 |
| Slider | 200×28 | 260×32 |
| ToggleSwitch | 60×28 | 80×32 |
| CircularGauge / RadialGauge | 150×150 | 180×180 |
| SemiCircle | 200×120 | 240×140 |
| LinearGauge (horizontal) | 260×80 | 300×100 |
| CenterValue | 120×120 | 140×140 |
| TrendChart | 400×200 | 500×300 |
| BarChart | 300×200 | 400×240 |
| PieChart | 200×200 | 240×240 |
| AlarmViewer | 400×220 | 600×240 |
| AssetsTree | 200×300 | 240×400 |
| DataGrid | 400×200 | 600×300 |
| Wizard symbol (TANK/PUMP/etc.) | 60×60 | 80×80 |
| Role | Hex |
|---|---|
| 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 |
This tells you what exists. Canvas and Dashboard skills cover how to use each category.
| Category | Members | Canvas? | Dashboard? |
|---|---|---|---|
| Shapes | Rectangle, Ellipse, Polygon, Polyline, Path, Gridline, Spline | ? | — |
| First-class auto-shapes | Cylinder, Gear, Arrow, Cloud, Star, Hexagon, Pentagon, Trapezoid | ? | — |
| Container | ShapeGroup, SvgGroup, Group | ? | — |
| Interaction | TextBlock, Label, Button, CheckBox, ComboBox, DataGrid, ListBox, NumericTextBox, PasswordBox, PushButton, RadioButton, Slider, TextBox, ToggleSwitch | ? | ? |
| Gauges | CenterValue, CircularGauge, Compass, DigitalGauge, LinearGauge, RadialGauge, RangeCircular, SemiCircle | ? | ? |
| Charts | BarChart, DigitalMeter, DrillingChart, PieChart, PieChartPlus, Timeline, TrendChart, XYChart | ? | ? |
| Viewer | AlarmViewer, AlarmAreas, AssetsTree, Carousel, ChildDisplay, Expander, FlowPanel, MapsOSM, PdfViewer, ProgressBar, TabControl, WebBlazor, WebBrowser | ? | ? |
| Editors | DatePicker, DateTimePicker, TimePicker, MediaElement, MenuItem, PageSelector | ? | ? |
| Dashboard | Cell | — | ? |
| IndustrialIcons | IndustrialIcon (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.
Every symbol uses Type: "Symbol". Not Type: "Pump", not Type: "Valve". One element type, many SymbolName values.
...
...
| language | json |
|---|
{
"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 complete Wizard catalog is 5 symbols:
| SymbolName | What it is | Typical SymbolLabels |
|---|---|---|
Wizard/BLOWER | Industrial blower/fan | State, RPM |
Wizard/MOTOR | Electric motor | State, RPM |
Wizard/PUMP | Pump (styles selectable in Designer) | State, RPM |
Wizard/TANK | Storage tank | Level, Alarm |
Wizard/VALVE | Valve (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.
Call list_elements('Library/HMI') or specific subfolders (Library/HMI/Pumps, Library/HMI/Valves, etc.) to discover. Library symbols auto-import on first reference:
...
...
| language | json |
|---|
{ "Type": "Symbol", "SymbolName": "Library/HMI/Pumps/CentrifugalPump", "Left": 200, "Top": 200, "Width": 120, "Height": 80 }User-created symbols in the current solution use Solution/ prefix:
...
| language | json |
|---|
{ "Type": "Symbol", "SymbolName": "Solution/MyCustomTank", "Left": 100, "Top": 100, "Width": 80, "Height": 100 }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')For plant-wide navigation with dynamic content:
...
| Mistake | Fix | |
|---|---|---|
| Hardcoding hex without thinking about theme switching | Use *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 | Use literal text | |
| process-meaning | ||
| Polygon / Gridline without Points | Always 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 Name | Field is always Name | |
Using DisplaysDraw as table_type | Visual editor UI, not a writable table. Use DisplaysList | |
Omitting PanelType | Required — "Canvas" or "Dashboard" | |
Wrapping the envelope in JsonFormat | Properties go at top level, no wrapper | |
| Partial write on an existing display | Always 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 theme | Set value AND clear theme: {Fill: '#FF3498DB', FillTheme: ''} | |
| Relying on a static element/symbol list | Always call list_elements() / list_dynamics() to get the authoritative catalog at runtime |
...
# 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 emptyDisplay envelope template:
...
{
"Name": "MyDisplay",
"PanelType": "Canvas",
"DisplayMode": "Page",
"Navigate": "true",
"Size": "1600 x 900",
"OnResize": "StretchFill",
"Width": 1600,
"Height": 900,
"Background": "theme:PageBackground",
"Elements": []
}After the basics are internalized, load the paradigm-specific skill:
...