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 does NOT cover: the equipment cookbook, controls reference, or grid-cell layout — those live in Canvas and Dashboard.
Steel (light office) or Onyx (dark control room).| 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 |
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.
| 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.
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.
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').
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.
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.
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:
...
Or, for a full-bleed dark background on a dark-themed display, place a full-size Rectangle with FillTheme: "PageBackground" as the first element.
{
"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.Displays, Symbols, Scripts, Queries, and Reports are document objects: write_objects replaces the entire document on save. Omitted content is deleted.
...
MainPage is predefined in new solutions. Write main content directly into it — no need to create a new display for the landing screen.
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 writeEvery 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.
This cadence catches errors within the same turn they were introduced, before they accumulate.
get_objects(detail='full') if modifying an existing displaywrite_objects(data=[...])get_state(target='designer'), look at errorListerrorList non-empty, fix and go back to step 3errorList from get_stateget_state on a display returns compile errors as a list:
...
If errorList is absent or empty, the display compiled clean.
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@Tag.<path> — the tag's Value. Do NOT append .Value; it's implicit (@Tag.X == @Tag.X.Value).@Tag.<path>/Attr.<member> — a UDT member on the tag.@Tag.<path>.<udt_member> — alternate dot syntax for UDT members.@Dataset.Query.<QueryName> — dataset query result (for DataGrid, ComboBox DataTable source).@Client.Context.<property> — session-scoped client context (AssetName, AssetPath, Theme).@Client.<property> — general client-scoped tags (@Client.Theme, @Client.AlarmPage.Filter).@Now — current time.Never use @Label. in display-element bindings. @Label. is only for symbol-definition internals.
Include asset folders in every tag reference: Area1/Line1/Motor1.Speed, not Motor1.Speed.
The single most important text pattern. Instead of two TextBlocks (value + unit) side by side, use ONE with composite LinkedValue:
...
"Flow: {@Tag.X} GPM" — label + value + unit"{@Tag.X} NTU" — value + unit"pH: {@Tag.X}" — label + value"Operator: {@Tag.Shift/CurrentOperator}" — label + bound stringIf you write a Polygon without Points, nothing renders and no error is produced — the element is simply invisible.
...
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) | ? | ? |
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.
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 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 |
Always 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:
{ "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:
{ "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.
SymbolLabels is the ONLY way to push data into a symbol:
Key must match a label key defined inside the symbol (case-sensitive).LabelValue uses @Tag.X / @Client.Context.X / @Now / composite strings.FieldType: "Expression" is the default and handles tag paths and expressions.@Label.X in display-element SymbolLabels. @Label. is a symbol-definition internal only.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:
...
A single display template can show data for whichever asset the operator selects. See the Dashboard skill for the AssetsTree + ChildDisplay master-detail pattern.
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.
| Mistake | Fix |
|---|---|
| Hardcoding hex without thinking about theme switching | Use *Theme properties; reserve hex for process-meaning |
| Polygon / Gridline without Points | Always include Points — min 3 for Polygon, 2 for Polyline/Gridline |
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 empty...
{
"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:
...