title: "
...
Scripts — Tasks, Classes, Expressions, and CodeBehind" tags: [
...
script, expression, code, task, class, codebehind, automation, python, csharp] description: "Server-side automation with Script Tasks and Expressions, reusable Script Classes, and client-side CodeBehind" version: "1.0" author: "Tatsoft"
...
Build a complete starter solution from scratch: create tags organized by asset, connect a Value Simulator for testing, configure historian logging and basic alarms, and create a dashboard to visualize everything.
This skill follows the Four Pillars build order:
Use when:
...
Implement automation logic in FrameworX: server-side Script Tasks for procedural logic, Expressions for lightweight tag-to-tag calculations, reusable Script Classes for shared methods, and client-side CodeBehind for display event handling.
Use when:
...
Do NOT use when:
...
| Category | Items |
|---|---|
| Tools | open_solution, get_table_schema, write_objects, get_objects, list_protocols, list_elements, designer_action, get_solution_info |
| Tables | UnsTags, DevicesChannels, DevicesNodes, DevicesPoints, HistorianHistorianTags, HistorianHistorianTables, AlarmsItems, DisplaysList |
open_solution('MyFirstSolution', template='Blank')
Templates available:
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:
{
"table_type": "UnsTags",
"data": [
{ "Name": "Plant/Tank1/Level", "DataType": "Double", "Description": "Tank 1 level %" },
{ "Name": "Plant/Tank1/Temperature", "DataType": "Double", "Description": "Tank 1 temp °C" },
{ "Name": "Plant/Tank1/Pressure", "DataType": "Double", "Description": "Tank 1 pressure bar" },
{ "Name": "Plant/Tank1/ValveOpen", "DataType": "Boolean", "Description": "Inlet valve state" },
{ "Name": "Plant/Tank2/Level", "DataType": "Double", "Description": "Tank 2 level %" },
{ "Name": "Plant/Tank2/Temperature", "DataType": "Double", "Description": "Tank 2 temp °C" },
{ "Name": "Plant/Tank2/Pressure", "DataType": "Double", "Description": "Tank 2 pressure bar" },
{ "Name": "Plant/Tank2/ValveOpen", "DataType": "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):
[
{
"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_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/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 4b. If not, create the pipeline:
get_table_schema('HistorianStorageLocations,HistorianHistorianTables')
get_table_schema('HistorianHistorianTags')
{
"table_type": "HistorianHistorianTags",
"data": [
{ "TagName": "Plant/Tank1/Level", "TableName": "Table1" },
{ "TagName": "Plant/Tank1/Temperature", "TableName": "Table1" },
{ "TagName": "Plant/Tank1/Pressure", "TableName": "Table1" },
{ "TagName": "Plant/Tank2/Level", "TableName": "Table1" },
{ "TagName": "Plant/Tank2/Temperature", "TableName": "Table1" },
{ "TagName": "Plant/Tank2/Pressure", "TableName": "Table1" }
]
}
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 significantlyAlarms use predefined groups: Critical, Warning, AuditTrail.
get_table_schema('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" }
]
}
TagName must use the full path — Plant/Tank1/Level, not bare Level.
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.
list_elements('Dashboard,TrendChart,CircularGauge,TextBlock')
get_table_schema('DisplaysList')
{
"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",
"Minimum": 0, "Maximum": 100,
"Column": 2, "Row": 1,
"Header": "Tank 2 Level"
},
{
"Type": "CircularGauge",
"Value": "@Tag.Plant/Tank2/Temperature.Value",
"Minimum": 0, "Maximum": 100,
"Column": 3, "Row": 1,
"Header": "Tank 2 Temp"
},
{
"Type": "TrendChart",
"Column": 0, "Row": 2, "ColumnSpan": 4,
"Pens": [
{ "TagName": "@Tag.Plant/Tank1/Level", "Color": "#FF2196F3", "Label": "T1 Level" },
{ "TagName": "@Tag.Plant/Tank2/Level", "Color": "#FFFF9800", "Label": "T2 Level" }
]
}
]
}]
}
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'))...
Is the code triggered by a display event (button click, display opening)?
YES → CodeBehind (client-side, embedded in the display)
NO ↓
Is it a one-liner on many tags (scaling, unit conversion, status mapping)?
YES → Expression (server-side, one row per tag)
NO ↓
Is it procedural logic (sequences, loops, conditionals, multi-step)?
YES → Task (server-side, full method body)
Do you need shared utility methods callable from Tasks, Expressions, or CodeBehind?
→ Class (server-side or client-side, plain .NET/Python methods)
| Type | Runs On | Document Object? | Table |
|---|---|---|---|
| Task | Server | YES — read-modify-write | ScriptsTasks |
| Class | Server or Client | YES — read-modify-write | ScriptsClasses |
| Expression | Server | NO — simple merge | ScriptsExpressions |
| CodeBehind | Client | Embedded in DisplaysList | (part of display content) |
Key distinction: Tasks and Classes are document objects — they contain code as structured content. You must read the existing object, modify it, and write the full object back. Sending partial content replaces the entire document.
Expressions are simple objects — send only the fields you want to set or change.
| Category | Items |
|---|---|
| Tools | get_table_schema, write_objects, get_objects |
| Tables | ScriptsTasks, ScriptsClasses, ScriptsExpressions, ScriptsReferences |
get_table_schema('ScriptsTasks')
A Task is a method body that executes on triggers. Key properties:
{
"table_type": "ScriptsTasks",
"data": [{
"ObjectName": "CalculateEfficiency",
"Language": "CSharp",
"Execution": "Periodic",
"Period": 5000,
"Code": "double input = @Tag.Plant/Pump1/InputPower.Value;\ndouble output = @Tag.Plant/Pump1/OutputFlow.Value;\nif (input > 0)\n @Tag.Plant/Pump1/Efficiency.Value = (output / input) * 100;\nelse\n @Tag.Plant/Pump1/Efficiency.Value = 0;"
}]
}
Both are pre-created in new solutions and MCP-labeled. Modify with read-modify-write.
Tag access in C#:
// Read
double level = @Tag.Plant/Tank1/Level.Value;
bool isRunning = @Tag.Plant/Pump1/Running.Value;
// Write
@Tag.Plant/Tank1/SetPoint.Value = 75.0;
@Tag.Plant/Pump1/Running.Value = true;
// Quality and Timestamp
int quality = @Tag.Plant/Tank1/Level.Quality;
DateTime ts = @Tag.Plant/Tank1/Level.Timestamp;
Tag access in Python:
level = Tag.Plant_Tank1_Level.Value # Python uses _ instead of /
Tag.Plant_Tank1_SetPoint.Value = 75.0
Calling a Class method:
double result = @Script.Class.MathUtils.Clamp(value, 0, 100);
get_table_schema('ScriptsClasses')
A Class is a plain .NET or Python class with static methods. Key properties:
{
"table_type": "ScriptsClasses",
"data": [{
"ObjectName": "MathUtils",
"Language": "CSharp",
"Domain": "Server",
"Code": "public static double Clamp(double value, double min, double max)\n{\n if (value < min) return min;\n if (value > max) return max;\n return value;\n}\n\npublic static double Scale(double raw, double rawMin, double rawMax, double engMin, double engMax)\n{\n return engMin + (raw - rawMin) * (engMax - engMin) / (rawMax - rawMin);\n}"
}]
}
@Script.Class.MathUtils.Clamp(value, 0, 100)get_table_schema('ScriptsExpressions')
An Expression is a one-line server-side calculation bound to a tag. Key properties:
Tag.Path/Name (REQUIRED format)Use Expressions when many tags each need a simple computation — avoids creating a full Task document per tag.
{
"table_type": "ScriptsExpressions",
"data": [
{
"ObjectName": "Tag.Plant/Tank1/LevelPercent",
"Expression": "@Tag.Plant/Tank1/LevelRaw * 100 / 4095",
"Execution": "OnChange"
},
{
"ObjectName": "Tag.Plant/Tank1/TempFahrenheit",
"Expression": "@Tag.Plant/Tank1/TempCelsius * 9 / 5 + 32",
"Execution": "OnChange"
},
{
"ObjectName": "Tag.Plant/Tank1/Status",
"Expression": "@Script.Class.StatusLogic.EvaluateStatus(@Tag.Plant/Tank1/Level, @Tag.Plant/Tank1/Pressure)",
"Execution": "Periodic",
"Period": 1000
}
]
}
Critical: ObjectName must include the Tag. namespace prefix. The tag itself (in UnsTags) does NOT have the prefix — but the Expression binding DOES.
CodeBehind is NOT a separate table — it's embedded in the display content (DisplaysList). It runs client-side in the display's local runtime.
DisplayOpening() — runs once when the display loadsDisplayIsOpen() — runs cyclically while the display is visibleDisplayClosing() — runs once when the display closesCodeBehind is part of the display's document content. When writing a display via write_objects('DisplaysList', ...), include the CodeBehind in the content structure. Fetch the display first (document object — read-modify-write), then add or modify the CodeBehind section.
See the Display Construction skill for the full display writing workflow.
If your code needs external .NET assemblies:
get_table_schema('ScriptsReferences')
Add assembly references so Tasks and Classes can use additional libraries.
| Mistake | Why It Happens | How to Avoid |
|---|---|---|
| Sending partial Task code | Forget it's a document object | Always read-modify-write for Tasks and Classes |
Expression ObjectName without Tag. prefix | Confusing tag path vs expression binding | Always Tag.Path/Name for Expression ObjectName |
Using @Label. in server Tasks | Confusing with display symbol syntax | @Label. is only for symbol definitions. Use @Tag. in scripts |
Python tag paths with / | Python doesn't support / in identifiers | Use _ instead of / in Python tag access |
| CodeBehind accessing server objects | Assumes it runs on server | CodeBehind runs client-side. Use @Tag. for server tag access, but local variables are client-only |
| Creating a Task for each tag's scaling | Over-engineering | Use Expressions for bulk one-liner calculations |
| Forgetting hot_reload after Task changes | Code changes don't apply automatically | Call designer_action('hot_reload') or restart runtime |
Modify the predefined ServerStartup task:
get_objects('ScriptsTasks', names=['ServerStartup'])
Add initialization code, write back the full document.
When 50 analog tags each need raw-to-engineering conversion:
For complex logic applied to many tags:
...