Versions Compared

Key

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

title: "New Solution — First Solution Creation with Tags, Simulator, and DashboardBuild a Complete Starter Solution" tags: [getting-started, beginner, first-solution, simulator, tutorial] description: "Create a complete starter solution from scratch: plan UNS structure, create tags, connect a Value Simulator device, configure historian logging, basic and alarms, and build a dashboard display, and start runtime" version: "12.0" author: "Tatsoft"

...

What This Skill Does

Build a complete starter solution from scratch: plan the UNS tag structure, create tags organized by asset, connect a Value Simulator for testing, configure historian logging and basic alarms, and create a dashboard to visualize everythingdisplay, and start runtime so the user sees live data.

This skill follows the Four Pillars build order:

...

  • FrameworX Designer installed and running
  • MCP tools connected

MCP Tools and Tables

CategoryItems
Toolsopen_solution, get_table_schema, write_objects, get_objects, list_protocols, list_elements, list_dynamics, designer_action, get_solution_info
TablesUnsTags, UnsUserTypes, UnsEnumerations, DevicesChannels, DevicesNodes, DevicesPoints, HistorianHistorianTags, HistorianHistorianTables, AlarmsItems, DisplaysList

Implementation Steps

Step 1: Create the Solution

...

  • Blank — empty solution, you build everything
  • HeaderLayout — pre-configured layout with header region (saves Step 8 layout layout work later)

After open_solution completes, the full engineering context is delivered. Proceed to the next step — do not pre-fetch schemas.

Step 2:

...

Fetch the tag schema first:

get_table_schema('UnsTags')

Create tags organized by asset path using / folders:

...

Plan the UNS (Tag Structure Design)

Before creating any tags, analyze the user's requirements and decide on the tag structure.

Key decision: Simple variables vs UserTypes (UDTs).

  • Simple variables — use when equipment is unique or has few tags. Each tag is an independent path: Plant/Tank1/Level, Plant/Tank1/Temperature.
  • UserTypes — use when multiple instances share the same member structure. Define a Motor type once with members Speed, Current, Running, Fault — then instantiate as Plant/Line1/Motor1, Plant/Line1/Motor2. All instances auto-get the same members.

Decision rule: If the user describes 2+ pieces of identical equipment (e.g., "3 pumps", "Tank1 and Tank2 with the same readings"), use a UserType. If equipment is one-off or members differ, use simple variables.

Naming convention: Use consistent asset paths with / folders so the same display template can be reused across instances (e.g., Plant/Tank1/..., Plant/Tank2/...). This enables asset-driven navigation later.

For this starter skill, we use simple variables. For UserType-based solutions, fetch get_table_schema('UnsUserTypes') and define types before creating tags.

Step 3: Create Tags (Pillar 1 — UNS)

Fetch the tag schema first:

get_table_schema('UnsTags')

Create tags organized by asset path using / folders:

{
  "table_type": "UnsTags",
  "data": [",       "Type": "Double", "Description": "Tank 1 level %" },
    { "Name": "Plant/Tank1/Temperature", "Type": "Double", "Description": "Tank 1 temp °C" },
    { "Name": "Plant/Tank1/Pressure",    "Type": "Double", "Description": "Tank 1 pressure bar" },
    { "Name": "Plant/Tank1/ValveOpen",   "Type": "Boolean", "Description": "Inlet valve state" },
    { "Name": "Plant/Tank2Tank1/Level",       "Type": "Double", "Description": "Tank 21 level %" },
    { "Name": "Plant/Tank2Tank1/Temperature", "Type": "Double", "Description": "Tank 21 temp °C" },
    { "Name": "Plant/Tank2Tank1/Pressure",    "Type": "Double", "Description": "Tank 21 pressure bar" },
    { "Name": "Plant/Tank2Tank1/ValveOpen",   "Type": "Boolean", "Description": "Inlet valve state" },
  ]
}

Naming convention: Use consistent asset paths so the same display template can be reused for Tank1, Tank2, etc. via asset navigation.

Step 3: Connect Value Simulator (Pillar 2a — Devices)

The Value Simulator generates changing data for testing — no real device needed. It's a built-in protocol.

3a: Get Protocol Details

list_protocols('ValueSimulator')

This returns the protocol-specific field formats for Channel, Node, and Point configuration. Never guess these formats — always fetch first.

3b: Fetch Device Schemas

get_table_schema('DevicesChannels,DevicesNodes,DevicesPoints')

3c: Create the 3-Table Pipeline

Devices follow a strict pipeline: Channel → Node → Point. All three are required, and dependencies must exist before referencing objects.

Use multi-table write_objects to create all three in one call (dependency order is handled automatically):

  { "Name": "Plant/Tank2/Level",       "Type": "Double", "Description": "Tank 2 level %" },
    { "Name": "Plant/Tank2/Temperature", "Type": "Double", "Description": "Tank 2 temp °C" },
    { "Name": "Plant/Tank2/Pressure",    "Type": "Double", "Description": "Tank 2 pressure bar" },
    { "Name": "Plant/Tank2/ValveOpen",   "Type": "Boolean", "Description": "Inlet valve state" }
  ]
}

Step 4: Connect Value Simulator (Pillar 2a — Devices)

The Value Simulator generates changing data for testing — no real device needed. It's a built-in protocol.

4a: Get Protocol Details

list_protocols('ValueSimulator')

This returns the protocol-specific field formats for Channel, Node, and Point configuration. Never guess these formats — always fetch first.

4b: Fetch Device Schemas

get_table_schema('DevicesChannels,DevicesNodes,DevicesPoints')

4c: Create the 3-Table Pipeline

Devices follow a strict pipeline: Channel → Node → Point. All three are required, and dependencies must exist before referencing objects.

Use multi-table write_objects to create all three in one call (dependency order is handled automatically):

[
  {
    "table_type": "DevicesChannels",
    "data": [[
  {
    "table_type": "DevicesChannels",
    "data": [{ "ObjectName": "SimChannel", "ProtocolName": "ValueSimulator" }]
  },
  {
    "table_type": "DevicesNodes",
    "data": [{ "ObjectName": "SimNode", "ChannelName": "SimChannel" }]
  },
  {
    "table_type": "DevicesPoints",
    "data": [
      { "ObjectName": "SimPoint_Level1",  "NodeName": "SimNode", "TagName": "Plant/Tank1/Level" },
      { "ObjectName": "SimPoint_Temp1",   "NodeName": "SimNode", "TagName": "Plant/Tank1/Temperature" },
      { "ObjectName": "SimPoint_Press1",  "NodeName": "SimNode", "TagName": "Plant/Tank1/Pressure" },
      { "ObjectName": "SimPoint_Valve1SimChannel",  "NodeNameProtocolName": "SimNodeValueSimulator", "TagName": "Plant/Tank1/ValveOpen" }]
  },
   {
   { "ObjectNametable_type": "SimPoint_Level2DevicesNodes",
   "NodeName "data": [{ "ObjectName": "SimNode", "TagNameChannelName": "Plant/Tank2/Level"SimChannel" }]
  },
  {
    { "ObjectNametable_type": "SimPoint_Temp2DevicesPoints",
    "NodeNamedata": "[
      { "ObjectName": "SimPoint_Level1",  "NodeName": "SimNode", "TagName": "Plant/Tank2Tank1/TemperatureLevel" },
      { "ObjectName": "SimPoint_Press2Temp1",   "NodeName": "SimNode", "TagName": "Plant/Tank2Tank1/PressureTemperature" },
      { "ObjectName": "SimPoint_Valve2Press1",  "NodeName": "SimNode", "TagName": "Plant/Tank2Tank1/ValveOpenPressure" },
     ]
  }
]

Important: TagName in DevicesPoints must use the full tag path including asset folders: Plant/Tank1/Level — not bare Level.

Adapt the protocol-specific fields (Address, ProtocolOptions, etc.) based on what list_protocols('ValueSimulator') returned.

Step 4: Configure Historian (Pillar 2b)

4a: Check if Default Table Exists

Most solutions include a default StorageLocation and Table1:

get_objects('HistorianHistorianTables')

If Table1 exists, skip to 4b. If not, create the pipeline:

get_table_schema('HistorianStorageLocations,HistorianHistorianTables')

4b: Log Tags to Historian

get_table_schema('HistorianHistorianTags')
{
  "table_type": "HistorianHistorianTags",
  "data": [
    {{ "ObjectName": "SimPoint_Valve1",  "NodeName": "SimNode", "TagName": "Plant/Tank1/ValveOpen" },
      { "ObjectName": "SimPoint_Level2",  "NodeName": "SimNode", "TagName": "Plant/Tank2/Level" },
      { "ObjectName": "SimPoint_Temp2",   "NodeName": "SimNode", "TagName": "Plant/Tank2/Temperature" },
      { "ObjectName": "SimPoint_Press2",  "NodeName": "SimNode", "TagName": "Plant/Tank1Tank2/LevelPressure" },
      { "TableNameObjectName": "Table1SimPoint_Valve2" },
    { "TagNameNodeName": "Plant/Tank1/TemperatureSimNode", "TableNameTagName": "Table1Plant/Tank2/ValveOpen" },
    ]
  }
]

Important: TagName in DevicesPoints must use the full tag path including asset folders: Plant/Tank1/Level — not bare Level.

Adapt the protocol-specific fields (Address, ProtocolOptions, etc.) based on what list_protocols('ValueSimulator') returned.

Step 5: Configure Historian (Pillar 2b)

5a: Check if Default Table Exists

Most solutions include a default StorageLocation and Table1:

get_objects('HistorianHistorianTables')

If Table1 exists, skip to 5b. If not, create the pipeline:

get_table_schema('HistorianStorageLocations,HistorianHistorianTables')

5b: Log Tags to Historian

get_table_schema('HistorianHistorianTags')
{
  "table_type": "HistorianHistorianTags",
  "data": [{ "TagName": "Plant/Tank1/Pressure",    "TableName": "Table1" },
    { "TagName": "Plant/Tank2/Level",       "TableName": "Table1" },
    { "TagName": "Plant/Tank2/Temperature", "TableName": "Table1" },
    { "TagName": "Plant/Tank2Tank1/PressureLevel",       "TableName": "Table1" },
    { "TagName": "Plant/Tank1/Temperature",  ]
}

Tuning tips (optional for a starter solution):

  • DeadBand: minimum value change before logging (0 = log every change)
  • Deviation: overrides interval-based logging when value changes significantly
  • Boolean tags (ValveOpen) typically don't need historian logging in a starter

Step 5: Add Basic Alarms (Pillar 2c)

Alarms use predefined groups: Critical, Warning, AuditTrail.

get_table_schema('AlarmsItems')
{
  "table_type": "AlarmsItems",
  "data": [
  "TableName": "Table1" },
    { "TagName": "Plant/Tank1/Pressure",    "TableName": "Table1" },
    { "TagName": "Plant/Tank2/Level",       "TableName": "Table1" },
    { "TagName": "Plant/Tank1Tank2/LevelTemperature",       "GroupName": "Critical", "Condition": "HiHi", "SetPoint": 95, "Description": "Tank 1 level critical high" "TableName": "Table1" },
    { "TagName": "Plant/Tank1Tank2/LevelPressure",       "GroupNameTableName": "WarningTable1", }
  ]
}

Tuning tips (optional for a starter solution):

  • DeadBand: minimum value change before logging (0 = log every change)
  • Deviation: overrides interval-based logging when value changes significantly
  • Boolean tags (ValveOpen) typically don't need historian logging in a starter

Step 6: Add Basic Alarms (Pillar 2c)

Predefined groups exist: Critical, Warning, AuditTrail. Verify with get_objects('AlarmsGroups') before creating new groups — don't waste a call creating groups that already exist.

get_table_schema('AlarmsItems')

Important: The field names shown below are representative examples. Always verify exact property names against the get_table_schema('AlarmsItems') response before writing — the schema is the source of truth.

{
  "table_type": "AlarmsItems",
  "data": ["Condition": "Hi",   "SetPoint": 85, "Description": "Tank 1 level high warning" },
    { "TagName": "Plant/Tank1/Level",       "GroupName": "Warning",  "Condition": "Lo",   "SetPoint": 15, "Description": "Tank 1 level low warning" },
    { "TagName": "Plant/Tank1/Level",       "GroupName": "Critical", "Condition": "LoLo", "SetPoint": 5,  "Description": "Tank 1 level critical low" },
    { "TagName": "Plant/Tank1/TemperatureLevel",  "GroupName": "     "GroupName": "Critical", "Condition": "HiHi", "SetPoint": 9095, "Description": "Tank 1 templevel critical high" },
    { "TagName": "Plant/Tank1/TemperatureLevel",       "GroupName": "Warning",  "Condition": "Hi",   "SetPoint": 8085, "Description": "Tank 1 templevel high warning" },
  ]
}

...

  { "TagName": "Plant/Tank1/Level

...

",       "GroupName": "Warning",  "Condition": "Lo",   "SetPoint": 15, "Description": "Tank 1 level low warning" },
    { "TagName": "Plant/Tank1/Level",       "GroupName": "Critical", "Condition": "LoLo", "SetPoint": 5,  "Description": "Tank 1 level critical low" },
    { "TagName": "Plant/Tank1/Temperature", "GroupName": "Critical", "Condition": "HiHi", "SetPoint": 90, "Description": "Tank 1 temp critical high" },
    { "TagName": "Plant/Tank1/Temperature", "GroupName": "Warning",  "Condition": "Hi",   "SetPoint": 80, "Description": "Tank 1 temp high warning" }
  ]
}

TagName must use the full pathPlant/Tank1/Level, not bare Level.

Repeat a similar pattern for Tank2 tags as appropriate.

Step 7: Create Dashboard (Pillar 4 — Displays)

Now build the operator interface. For a starter solution, write directly into the predefined MainPage display.

This step covers a Dashboard (grid-based responsive layout). For Canvas displays (pixel-precise positioning, process diagrams, P&ID-style screens), or for advanced display features like visual dynamics, symbol wiring, and CodeBehind — load the Display Construction skill: search_docs('displays', labels='skill').

7a: Fetch Element Schemas

list_elements('Dashboard,TrendChart,CircularGauge,TextBlock')
get_table_schema('DisplaysList')

7b: Write the Dashboard

CRITICAL: Verify the exact Dashboard JSON structure against list_elements('Dashboard') output. The schema returned by that call is the source of truth for property names (DashboardDisplay, Cells, Columns, Rows, etc.).

{
  "table_type": "DisplaysList",
  "data": [{
    "ObjectName": "MainPage",
    "PanelType": "Dashboard",
    "DashboardDisplay": {
      "Columns": ["*", "*", "*", "*"],
      "Rows": ["Auto", "*", "*"]
    },
    "Cells": [
      {

Repeat a similar pattern for Tank2 tags as appropriate.

Step 6: Create Dashboard (Pillar 4 — Displays)

Now build the operator interface. For a starter solution, write directly into the predefined MainPage display.

6a: Fetch Element Schemas

list_elements('Dashboard,TrendChart,CircularGauge,TextBlock')
get_table_schema('DisplaysList')

6b: Write the Dashboard

{
  "table_type": "DisplaysList",
  "data": [{
    "ObjectName": "MainPage",
    "PanelType": "Dashboard",
    "Columns": 4,
    "Rows": 3,
    "Content": [
      {
        "Type": "TextBlock",
        "Text": "Plant Overview",
        "Column": 0, "Row": 0, "ColumnSpan": 4,
        "FontSize": 24, "HorizontalAlignment": "Center"
      },
      {
        "Type": "CircularGauge",
        "Value": "@Tag.Plant/Tank1/Level.Value",
        "Minimum": 0, "Maximum": 100,
        "Column": 0, "Row": 1,
        "Header": "Tank 1 Level"
      },
      {
        "Type": "CircularGauge",
        "Value": "@Tag.Plant/Tank1/Temperature.Value",
        "Minimum": 0, "Maximum": 100,
        "Column": 1, "Row": 1,
        "Header": "Tank 1 Temp"
      },
      {
        "Type": "CircularGauge",
        "Value": "@Tag.Plant/Tank2/Level.Value",
        "MinimumRow": 0, "Col": 0, "MaximumColSpan": 1004,
        "ColumnContent": 2, "Row": 1,
{
          "HeaderType": "Tank 2 LevelTextBlock",
      },
    "Text": "Plant {Overview",
        "Type  "FontSize": 24, "HorizontalAlignment": "CircularGaugeCenter",
        "Value": "@Tag.Plant/Tank2/Temperature.Value",
}
      },
   "Minimum": 0, "Maximum": 100, {
        "ColumnRow": 31, "RowCol": 10,
        "HeaderCell": { "Tank 2 Temp"
   HeaderLink": "Tank 1 Level" },
   },
     "Content": {
          "Type": "TrendChartCircularGauge",
        "Column": 0, "RowLinkedValue": 2, "ColumnSpan": 4,
"@Tag.Plant/Tank1/Level.Value",
          "PensMinimum": 0, "Maximum": [100
        }
   { "TagName": "@Tag.Plant/Tank1/Level", "Color": "#FF2196F3", "Label": "T1 Level" },  },
      {
          { "TagNameRow": "@Tag.Plant/Tank2/Level"1, "ColorCol": "#FFFF9800"1,
 "Label": "T2 Level" }
    "Cell": { "HeaderLink":  ]"Tank 1 Temp" },
      }
  "Content": {
         ]
  }]
}

CRITICAL: PanelType is REQUIRED. Omitting it silently defaults to Canvas, which breaks Dashboard grid positioning.

Note: Verify TrendChart Pens schema against list_elements('TrendChart') output — the exact property names may vary.

Step 7: Start Runtime

designer_action('start_runtime')

The Value Simulator starts feeding changing data. Historian begins logging. Alarms evaluate against the setpoints. The dashboard shows live values.

Verification

After starting runtime:

  1. get_solution_info() — confirm object counts match expectations
  2. Navigate to MainPage: designer_action('navigate', 'Displays', 'Draw', 'MainPage') — dashboard should show live gauge values
  3. Navigate to AlarmsMonitor: designer_action('navigate', 'Alarms', 'AlarmsMonitor') — alarms should evaluate against limits when simulator values cross setpoints
  4. Navigate to HistorianMonitor: designer_action('navigate', 'Historian', 'HistorianMonitor') — confirm tag values are being logged

Adapting for Real Protocols

To adapt this starter for a real device instead of Value Simulator:

  1. In Step 3, replace list_protocols('ValueSimulator') with the target protocol (e.g., list_protocols('ModbusTCP'), list_protocols('OPCUA'))
  2. Configure Channel/Node with real connection parameters (IP, port, etc.)
  3. Map Points to real device addresses
  4. Everything else (Tags, Historian, Alarms, Dashboard) remains the same

Common Pitfalls

...

 "Type": "CircularGauge",
          "LinkedValue": "@Tag.Plant/Tank1/Temperature.Value",
          "Minimum": 0, "Maximum": 100
        }
      },
      {
        "Row": 1, "Col": 2,
        "Cell": { "HeaderLink": "Tank 2 Level" },
        "Content": {
          "Type": "CircularGauge",
          "LinkedValue": "@Tag.Plant/Tank2/Level.Value",
          "Minimum": 0, "Maximum": 100
        }
      },
      {
        "Row": 1, "Col": 3,
        "Cell": { "HeaderLink": "Tank 2 Temp" },
        "Content": {
          "Type": "CircularGauge",
          "LinkedValue": "@Tag.Plant/Tank2/Temperature.Value",
          "Minimum": 0, "Maximum": 100
        }
      },
      {
        "Row": 2, "Col": 0, "ColSpan": 4,
        "Cell": { "HeaderLink": "Level Trends" },
        "Content": {
          "Type": "TrendChart",
          "Duration": "5m",
          "Pens": {
            "Type": "TrendPenList",
            "Children": [
              { "Type": "TrendPen", "LinkedValue": "@Tag.Plant/Tank1/Level", "PenLabel": "T1 Level", "Stroke": "#FF2196F3", "Auto": true },
              { "Type": "TrendPen", "LinkedValue": "@Tag.Plant/Tank2/Level", "PenLabel": "T2 Level", "Stroke": "#FFFF9800", "Auto": true }
            ]
          }
        }
      }
    ]
  }]
}

CRITICAL: PanelType is REQUIRED. Omitting it silently defaults to Canvas, which breaks Dashboard grid positioning.

Note: Verify TrendChart Pens schema and Dashboard Cell structure against list_elements('TrendChart') and list_elements('Dashboard') output — the exact property names are defined there.

Step 8: Start Runtime

designer_action('start_runtime')

The Value Simulator starts feeding changing data. Historian begins logging. Alarms evaluate against the setpoints. The dashboard shows live values.

This step is essential — always start runtime at the end of a new solution build so the user immediately sees live data.

Verification

After starting runtime:

  1. get_solution_info() — confirm object counts match expectations
  2. Navigate to MainPage: designer_action('navigate', 'Display.MainPage') — dashboard should show live gauge values
  3. Navigate to AlarmsMonitor: designer_action('navigate', 'AlarmsMonitor') — alarms should evaluate against limits when simulator values cross setpoints
  4. Navigate to HistorianMonitor: designer_action('navigate', 'HistorianMonitor') — confirm tag values are being logged

Adapting for Real Protocols

To adapt this starter for a real device instead of Value Simulator:

  1. In Step 4, replace list_protocols('ValueSimulator') with the target protocol (e.g., list_protocols('ModbusTCP'), list_protocols('OPCUA'))
  2. Configure Channel/Node with real connection parameters (IP, port, etc.)
  3. Map Points to real device addresses
  4. Everything else (Tags, Historian, Alarms, Dashboard) remains the same

Common Pitfalls

MistakeWhy It HappensHow to Avoid
Guessing protocol field formatsSkipping list_protocolsAlways call list_protocols before writing device tables
Bare tag names in DevicesPointsForgetting asset pathAlways use full path: Plant/Tank1/Level
Creating unnecessary HistorianTablesNot checking defaultsCheck get_objects('HistorianHistorianTables') first
Creating unnecessary AlarmsGroupsNot checking defaultsPredefined groups exist (Critical, Warning, AuditTrail) — check get_objects('AlarmsGroups') first
Omitting PanelType on dashboardSeems optionalAlways set PanelType explicitly
Pre-fetching all schemas at onceOver-eager optimizationFetch each module's schema only when ready to write it
Screenshotting to verify writesUnnecessary self-validationTrust write_objects success. User sees live updates
Forgetting to start runtimeSkill ends at configAlways end with designer_action('start_runtime')
Using alarm field names from examples without checking schemaExamples may not match current schemaAlways verify field names against get_table_schema('AlarmsItems')
Using simple variables for repeated equipmentDidn't plan UNS structureStep 2: if 2+ identical equipment instances exist, use UserTypes

What's Next

  • Canvas displays, process diagrams, visual dynamics: Load the Display Construction skill — search_docs('displays', labels='skill')
  • Scripts, expressions, automation: Load the Scripts and Expressions skill — search_docs('scripts', labels='skill')
  • Real device connectivity: Use list_protocols() to browse available protocols, then adapt Step 4
  • Asset navigation pattern: The Display Construction skill covers how to build asset-driven dynamic displays

...