Purpose

This skill is the common ground for every FrameworX display build, regardless of paradigm. Load it whenever you do display work. Pair with Skill Display Construction — Canvas for pixel-positioned HMI/P&ID displays or with Skill Display Construction — Dashboard for grid-based data displays.

What this skill covers:

What this skill does NOT cover: the equipment cookbook, controls reference, or grid-cell layout — those live in Canvas and Dashboard.

Prerequisites

Tools you use over and over

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

Section 1 — Theme-first mental model

The one-sentence rule

Do not hardcode hex colors unless the hex carries process meaning. Use FillTheme, StrokeTheme, ForegroundTheme, BackgroundTheme, BorderBrushTheme properties with semantic brush names from list_elements('ThemeColors').

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 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.

Pick a theme at the start of the build

Before placing any element, decide which theme family this solution is for:

Then use *Theme properties throughout — the displays render correctly under any theme, but look best under the one you designed for.

The brush catalogue

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): ElementRed, ElementOrange, ElementYellow are silently dropped at write time on Ellipse (and possibly other shape types). Prefer StateRed / ColorAmber / StateAlarm instead until resolved.

How theme properties work on the wire

When you write {"FillTheme": "ElementBlue"} the writer stores it as {"Fill": "theme:ElementBlue"}. Both input forms are equivalent:

Use FillTheme / StrokeTheme / ForegroundTheme / BackgroundTheme / BorderBrushTheme for readability — it documents your intent. The writer collapses to the prefixed form. If you mean a hardcoded color, use Fill/Stroke/Foreground etc. directly.

The display-root Background footgun

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.

Section 2 — Write mechanics

The canonical display envelope

{
  "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

Displays are document objects — read before write

Displays, Symbols, Scripts, Queries, and Reports are document objects: write_objects replaces the entire document on save. Omitted content is deleted.

When modifying an existing display:

  1. Read the full document: get_objects('DisplaysList', names=['MyDisplay'], detail='full')
  2. Modify your working copy (add/change/remove elements)
  3. Write the complete merged document back

Never send a partial update — you'll silently delete everything you didn't include.

MainPage is predefined in new solutions. Write main content directly into it — no need to create a new display for the landing screen.

Category and MCP-label guard

AI-created objects are automatically tagged with the MCP category, which makes them editable / overwritable by subsequent AI writes. Objects without this label are "owned" by the user and produce Skipping existing on write attempts. This is the user-protection mechanism — respect it. When you see Skipping existing, the user can add the MCP label in Designer if they want to allow AI edits.

solution_id on every write

Every write_objects, delete_objects, rename_objects, and designer_action call should include the solution_id from the most recent open_solution or create_solution response. This verifies the session is still connected to the expected solution.

Section 3 — The build loop

This cadence catches errors within the same turn they were introduced, before they accumulate.

  1. PLAN — what am I adding/changing? What tags does it bind to?
  2. READget_objects(detail='full') if modifying an existing display
  3. WRITEwrite_objects(data=[...])
  4. CHECKget_state(target='designer'), look at errorList
  5. FIX — if errorList non-empty, fix and go back to step 3
  6. DONE — move to next display or next task

Reading errorList from get_state

get_state on a display returns compile errors as a list:

{
  "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.

If errorList is absent or empty, the display compiled clean.

Never use screenshots to self-validate

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 and compile-time gotchas

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

Never use @Label. in display-element bindings. @Label. is only for symbol-definition internals.

Tag paths must be FULL paths

Include asset folders in every tag reference: Area1/Line1/Motor1.Speed, not Motor1.Speed.

Inline interpolation — the composite LinkedValue pattern

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:

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.

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:

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.

// ? 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).

Writer normalizations — what you write ≠ what you read back

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).

Section 5 — Spacing and typography tokens

Spacing scale

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

Typography ramp

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

Minimum sizes (hard floors)

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

Color hex fallbacks (for process meaning only)

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

Section 6 — Element types overview

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" works.

Section 7 — Symbols (all three sources)

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

{
  "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:

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-1274.

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:

{ "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:

{ "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.

Binding via SymbolLabels — never via direct properties

SymbolLabels is the ONLY way to push data into a symbol:

Section 8 — Layouts and asset navigation (brief)

Layouts

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:

A single display template can show data for whichever asset the operator selects. See the Dashboard skill for the AssetsTree + ChildDisplay master-detail pattern.

Section 9 — CodeBehind (brief pointer)

CodeBehind is client-side C# or VB.NET code embedded in each display. Lifecycle methods: DisplayOpening(), DisplayIsOpen(), DisplayClosing(), DialogOnOK().

The Contents field format starts with CSharp\r\n or VBdotNet\r\n before the code. Full CodeBehind guidance lives in the Skill Scripts Expressions skill.

Section 10 — Common pitfalls

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 .SourceIri or other system columns (TDEV-1273)

Use literal text

Polygon / Gridline without Points

Always include Points — min 3 for Polygon, 2 for Polyline/Gridline

Using 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: ''}

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:

Both assume this Basics skill is loaded.