title: "Getting Started — First Solution with Tags, Simulator, and DashboardNew Solution — Build 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"
...
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:
Use when:
...
| Category | Items |
|---|---|
| Tools | open_solution, get_table_schema, write_objects, get_objects, list_protocols, list_elements, list_dynamics, designer_action, get_solution_info |
| Tables | UnsTags, |
UnsUserTypes, UnsEnumerations, DevicesChannels, DevicesNodes, DevicesPoints, HistorianHistorianTags, HistorianHistorianTables, AlarmsItems, DisplaysList |
open_solution('MyFirstSolution', template='Blank')
...
After open_solution completes, the full engineering context is delivered. Proceed to the next step — do not pre-fetch schemas.
...
Fetch the tag schema first:
get_table_schema('UnsTags')
Create tags organized by asset path using / folders:
...
Before creating any tags, analyze the user's requirements and decide on the tag structure.
Key decision: Simple variables vs UserTypes (UDTs).
Plant/Tank1/Level, Plant/Tank1/Temperature.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.
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.
The Value Simulator generates changing data for testing — no real device needed. It's a built-in protocol.
list_protocols('ValueSimulator')
This returns the protocol-specific field formats for Channel, Node, and Point configuration. Never guess these formats — always fetch first.
get_table_schema('DevicesChannels,DevicesNodes,DevicesPoints')
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" }
]
}
The Value Simulator generates changing data for testing — no real device needed. It's a built-in protocol.
list_protocols('ValueSimulator')
This returns the protocol-specific field formats for Channel, Node, and Point configuration. Never guess these formats — always fetch first.
get_table_schema('DevicesChannels,DevicesNodes,DevicesPoints')
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": [{ "ObjectName": "SimChannel", "ProtocolName": "ValueSimulator" }]
},
{
"table_type": "DevicesNodes",
"data": [{ "ObjectName[
{
"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", "TagNameChannelName": "Plant/Tank1/Temperature"SimChannel" }]
},
{
{ "table_type": "DevicesPoints",
"data": [
{ "ObjectName": "SimPoint_Press1Level1", "NodeName": "SimNode", "TagName": "Plant/Tank1/PressureLevel" },
{ "ObjectName": "SimPoint_Valve1Temp1", "NodeName": "SimNode", "TagName": "Plant/Tank1/ValveOpenTemperature" },
{ "ObjectName": "SimPoint_Level2Press1", "NodeName": "SimNode", "TagName": "Plant/Tank2Tank1/LevelPressure" },
{ "ObjectName": "SimPoint_Temp2Valve1", "NodeName": "SimNode", "TagName": "Plant/Tank2Tank1/TemperatureValveOpen" },
{ "ObjectName": "SimPoint_Press2Level2", "NodeName": "SimNode", "TagName": "Plant/Tank2/PressureLevel" },
{ "ObjectName": "SimPoint_Valve2Temp2", "NodeName": "SimNode", "TagName": "Plant/Tank2/ValveOpenTemperature" },
]
}
]
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.
Most solutions include a default StorageLocation and Table1:
get_objects('HistorianHistorianTables')
{ "ObjectName": "SimPoint_Press2", "NodeName": "SimNode", "TagName": "Plant/Tank2/Pressure" },
{ "ObjectName": "SimPoint_Valve2", "NodeName": "SimNode", "TagName": "Plant/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.
Most solutions include a default StorageLocation and Table1:
get_objects('HistorianHistorianTables')
If Table1 exists, skip to 5bIf Table1 exists, skip to 4b. If not, create the pipeline:
get_table_schema('HistorianStorageLocations,HistorianHistorianTables')
...
get_table_schema('HistorianHistorianTags')
...
DeadBand: minimum value change before logging (0 = log every change)Deviation: overrides interval-based logging when value changes significantly...
Alarms use predefined 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"{
"table_type": "AlarmsItems",
"data": [
{ "TagName": "Plant/Tank1/Level", "GroupName": "Critical", "Condition": "HiHi", "SetPoint": 95, "Description": "Tank 1 level critical high" },
{ "TagName": "Plant/Tank1/Level", "GroupName": "Warning", "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/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" }
]
}
...
Repeat a similar pattern for Tank2 tags as appropriate.
...
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').
list_elements('Dashboard,TrendChart,CircularGauge,list_elements('Dashboard,TrendChart,CircularGauge,TextBlock')
get_table_schema('DisplaysList')
...
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",
"ColumnsDashboardDisplay": 4,{
"RowsColumns": 3,
["*", "Content*": [
{, "*", "*"],
"Rows": ["Auto", "Type*":, "TextBlock*",]
},
"TextCells": "Plant Overview",[
{
"ColumnRow": 0, "RowCol": 0, "ColumnSpanColSpan": 4,
"FontSizeContent": {
24, "HorizontalAlignment": "Center"
},
{
"Type": "CircularGaugeTextBlock",
"ValueText": "@Tag.Plant/Tank1/Level.ValuePlant Overview",
"MinimumFontSize": 024, "MaximumHorizontalAlignment": 100,"Center"
"Column": 0, "Row": 1}
},
{
"HeaderRow": 1, "Tank 1 Level"
Col": 0,
"Cell": { "HeaderLink": "Tank 1 Level" },
"Content": {
"Type": "CircularGauge",
"ValueLinkedValue": "@Tag.Plant/Tank1/TemperatureLevel.Value",
"Minimum": 0, "Maximum": 100,
"Column}
},
{
"Row": 1, "RowCol": 1,
"Header"Cell": { "HeaderLink": "Tank 1 Temp" },
},
"Content": {
{
"Type": "CircularGauge",
"ValueLinkedValue": "@Tag.Plant/Tank2Tank1/LevelTemperature.Value",
"Minimum": 0, "Maximum": 100,
"Column}
},
{
"Row": 21, "RowCol": 12,
"HeaderCell": { "HeaderLink": "Tank 2 Level" },
},
"Content": {
{
"Type": "CircularGauge",
"ValueLinkedValue": "@Tag.Plant/Tank2/TemperatureLevel.Value",
"Minimum": 0, "Maximum": 100
}
},
{
"Column "Row": 31, "RowCol": 13,
"HeaderCell": { "HeaderLink": "Tank 2 Temp" },
},
"Content": {
{
"Type": "TrendChartCircularGauge",
"ColumnLinkedValue": 0, "Row "@Tag.Plant/Tank2/Temperature.Value",
"Minimum": 20, "ColumnSpanMaximum": 4,100
"Pens": [
}
},
{
{ "TagNameRow": "@Tag.Plant/Tank1/Level"2, "ColorCol": "#FF2196F3"0, "LabelColSpan": "T1 Level" }4,
"Cell": { "TagNameHeaderLink": "@Tag.Plant/Tank2/Level", "Color": "#FFFF9800", "Label": "T2 Level" }
Level Trends" },
"Content": {
]
"Type": "TrendChart",
}
]
}]
}
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.
designer_action('start_runtime')
The Value Simulator starts feeding changing data. Historian begins logging. Alarms evaluate against the setpoints. The dashboard shows live values.
After starting runtime:
get_solution_info() — confirm object counts match expectationsdesigner_action('navigate', 'Displays', 'Draw', 'MainPage') — dashboard should show live gauge valuesdesigner_action('navigate', 'Alarms', 'AlarmsMonitor') — alarms should evaluate against limits when simulator values cross setpointsdesigner_action('navigate', 'Historian', 'HistorianMonitor') — confirm tag values are being loggedTo adapt this starter for a real device instead of Value Simulator:
list_protocols('ValueSimulator') with the target protocol (e.g., list_protocols('ModbusTCP'), list_protocols('OPCUA'))...
"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.
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.
After starting runtime:
get_solution_info() — confirm object counts match expectationsdesigner_action('navigate', 'AlarmsMonitor') — alarms should evaluate against limits when simulator values cross setpointsdesigner_action('navigate', 'HistorianMonitor') — confirm tag values are being loggedTo adapt this starter for a real device instead of Value Simulator:
list_protocols('ValueSimulator') with the target protocol (e.g., list_protocols('ModbusTCP'), list_protocols('OPCUA'))| Mistake | Why It Happens | How to Avoid |
|---|---|---|
| Guessing protocol field formats | Skipping list_protocols | Always call list_protocols before writing device tables |
| Bare tag names in DevicesPoints | Forgetting asset path | Always use full path: Plant/Tank1/Level |
| Creating unnecessary HistorianTables | Not checking defaults | Check get_objects('HistorianHistorianTables') first |
| Creating unnecessary AlarmsGroups | Not checking defaults | Predefined groups exist (Critical, Warning, AuditTrail) — check get_objects('AlarmsGroups') first |
| Omitting PanelType on dashboard | Seems optional | Always set PanelType explicitly |
| Pre-fetching all schemas at once | Over-eager optimization | Fetch each module's schema only when ready to write it |
| Screenshotting to verify writes | Unnecessary self-validation | Trust write_objects success. User sees live updates |
| Forgetting to start runtime | Skill ends at config | Always end with designer_action('start_runtime') |
| Using alarm field names from examples without checking schema | Examples may not match current schema | Always verify field names against get_table_schema('AlarmsItems') |
| Using simple variables for repeated equipment | Didn't plan UNS structure | Step 2: if 2+ identical equipment instances exist, use UserTypes |
search_docs('displays', labels='skill')search_docs('scripts', labels='skill')list_protocols() to browse available protocols, then adapt Step 4...