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

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

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

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:

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

Note on Element*-family brushes: ElementRed, ElementOrange, ElementYellow may not apply consistently on all shape types. If you don't see your intended color, prefer StateRed / ColorAmber / StateAlarm.

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 syntax

@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:

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

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

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

Always 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:

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

MistakeFix
Hardcoding hex without thinking about theme switchingUse *Theme properties; reserve hex for process-meaning
Polygon / Gridline without PointsAlways include Points — min 3 for Polygon, 2 for Polyline/Gridline
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:

Both assume this Basics skill is loaded.