Create machine learning models that run inside FrameworX using Script Classes with ML.NET. The AI writes the C# code, connects it to live tags, creates output tags for predictions, and configures model persistence — all within the FrameworX scripting engine.
Always generate the full production-ready implementation. Every ML Script Class includes model persistence (SaveModel), startup reload (LoadModel), and the ServerStartup wiring — no stripped-down versions.
One model per interaction. Always create exactly one Script Class ML model per session, targeting one sensor or one prediction goal. Do NOT create multiple ML classes unprompted — even if the solution has many tags. If the user wants additional models, they will ask in follow-up.
Build ML.NET models as FrameworX Script Classes. The AI generates the full C# ML pipeline (data classes, training, prediction, tag integration) based on the user's requirements. Models run server-side, read from input tags, and write predictions to output tags.
Architecture
Input Tags ??? Script Class (ML.NET) ??? Output Tags
? ? ?
Live data Train / Predict Predictions, scores,
from UNS Model persisted to anomaly flags, forecasts
solution folder ??? Alarms / Dashboard
|
Use when:
Do NOT use when:
Category | Items |
|---|---|
Tools |
|
Tables |
|
HARD STOP — Do not create any tags, classes, tasks, or expressions until Step 0 is complete. |
Before writing any code, the AI must always ask the user the following questions — no exceptions, regardless of how much context is available. Do not silently choose for the user.
1. Which ML algorithm do you want to use?
- Anomaly Detection — SSA Spike — detects sudden outliers, spikes, or abnormal readings (e.g., pressure spikes, vibration bursts, temperature jumps)
- Anomaly Detection — SSA ChangePoint — detects gradual drift or regime shifts (e.g., slow pressure decay, process baseline change)
- Time-Series Forecasting — SSA — predicts future values from historical patterns (e.g., tank level in 30 min, production rate tomorrow)
- Regression — FastTree — predicts a continuous value from multiple inputs (e.g., energy consumption from temperature + speed + load)
- Binary Classification — FastTree — predicts yes/no outcomes from multiple inputs (e.g., will this motor fault? will this batch pass QC?)
Not sure which to pick? Describe what you want to achieve and I'll recommend the best fit.
After Q1 is answered, adapt Q2 and Q3 based on the chosen algorithm:
Anomaly Detection — SSA Spike or ChangePoint:
2. Which single tag member should be monitored for anomalies?
(e.g.,OilGas_Co/WestTexas_Field/WellPad_A/Well_A01.TubingPressure— full path + member name)3. The output will be
AnomalyScore,IsAnomaly, andLastPredictiontags under<AssetPath>/ML/. Confirm the asset path prefix, or suggest a different output folder.
Time-Series Forecasting — SSA:
2. Which single tag member should be forecast?
(e.g.,OilGas_Co/.../Tank_01.Level— full path + member name)3. How many steps ahead should the forecast horizon be? What does the value represent (unit/context)? The output will be
Forecast,ForecastLower,ForecastUpper, andLastPredictionunder<AssetPath>/ML/.
Regression — FastTree:
2. Which 2–5 feature tags are the inputs, and which tag is the label (the value to predict)?
(provide full paths for all — e.g., features: Temperature, Pressure, FlowRate; label: EnergyConsumption)3. What does the predicted value represent (unit/context, e.g., "energy consumption in kW")?
Binary Classification — FastTree:
2. Which 2–5 feature tags are the inputs, and which tag is the boolean label (historical fault flag)?
(provide full paths for all — e.g., features: VibrationX, VibrationY, Temperature; label: DidFault)3. What does the yes/no outcome represent? (e.g., "will this compressor fault in the next hour")
Do not proceed past Step 0 until all three questions are answered.
User Goal | Suggested Algorithm |
|---|---|
Predictive maintenance — single sensor | Anomaly Detection (Spike) |
Predictive maintenance — multiple sensors | Binary Classification |
Detect sensor failures / outliers | Anomaly Detection (Spike) |
Detect gradual drift or process shift | Anomaly Detection (ChangePoint) |
Predict future values | Time-Series Forecasting (SSA) |
Energy / consumption modeling | Regression |
Quality control pass/fail | Binary Classification |
Fault prediction yes/no | Binary Classification |
Production / demand forecasting | Time-Series Forecasting (SSA) |
Process output from multiple inputs | Regression |
Information | Why |
|---|---|
Input tag path(s) | The model reads from these tags |
ML algorithm | Determines the ML.NET pipeline to generate |
Output semantics | What the predictions mean (anomaly score, forecast value, etc.) |
Create tags to receive the model's predictions. Place them under a /ML/ subfolder relative to the input tag's asset path for clean separation.
get_table_schema('UnsTags')
|
Output tag patterns by ML task:
{
"table_type": "UnsTags",
"data": [
{ "Name": "<AssetPath>/ML/AnomalyScore", "DataType": "Double", "Description": "Anomaly score (0=normal, higher=anomalous)" },
{ "Name": "<AssetPath>/ML/IsAnomaly", "DataType": "Boolean", "Description": "True when anomaly detected" },
{ "Name": "<AssetPath>/ML/LastPrediction", "DataType": "DateTime", "Description": "Timestamp of last prediction" }
]
}
|
{
"table_type": "UnsTags",
"data": [
{ "Name": "<AssetPath>/ML/Forecast", "DataType": "Double", "Description": "Forecasted value" },
{ "Name": "<AssetPath>/ML/ForecastLower", "DataType": "Double", "Description": "Lower confidence bound" },
{ "Name": "<AssetPath>/ML/ForecastUpper", "DataType": "Double", "Description": "Upper confidence bound" },
{ "Name": "<AssetPath>/ML/LastPrediction", "DataType": "DateTime", "Description": "Timestamp of last prediction" }
]
}
|
{
"table_type": "UnsTags",
"data": [
{ "Name": "<AssetPath>/ML/PredictedValue", "DataType": "Double", "Description": "Model predicted value" },
{ "Name": "<AssetPath>/ML/LastPrediction", "DataType": "DateTime", "Description": "Timestamp of last prediction" }
]
}
|
{
"table_type": "UnsTags",
"data": [
{ "Name": "<AssetPath>/ML/PredictedLabel", "DataType": "Boolean", "Description": "Predicted outcome (true/false)" },
{ "Name": "<AssetPath>/ML/Probability", "DataType": "Double", "Description": "Prediction probability (0-1)" },
{ "Name": "<AssetPath>/ML/LastPrediction", "DataType": "DateTime", "Description": "Timestamp of last prediction" }
]
}
|
Replace <AssetPath> with the actual asset folder path (e.g., Plant/Reactor1).
Script Classes using ML.NET require the NamespaceDeclarations field set in write_objects. ML.NET is pre-installed with FrameworX — no NuGet packages or external DLL references are needed.
Critical pitfall: The field |
The standard ML.NET namespace set to use:
"NamespaceDeclarations": "Microsoft.ML;Microsoft.ML.Data;Microsoft.ML.Transforms;Microsoft.ML.Transforms.TimeSeries;Microsoft.ML.Transforms.Text;Microsoft.ML.Trainers;Microsoft.ML.TimeSeries" |
Use this exact string for all ML task types. If the class uses VectorType, it is already included via Microsoft.ML.Data.
get_table_schema('ScriptsClasses')
|
Every ML Script Class follows this structure — always include persistence and LoadModel. No stripped-down versions.
// 1. Data classes — define input/output schemas for ML.NET
public class SensorData
{
public float Value { get; set; }
}
public class PredictionResult
{
// Fields vary by ML task (see task-specific examples below)
}
// 2. Static fields — MLContext and model persist across calls
private static MLContext mlContext = new MLContext(seed: 0);
private static ITransformer model;
private static IDataView lastTrainingDataView;
private static bool modelTrained = false;
// 3. Training buffer — collects data until enough for training
private static List<SensorData> trainingBuffer = new List<SensorData>();
private const int MinTrainingSize = 100; // adjust per task
// 4. Model path — persisted to solution execution folder
private static readonly string ModelPath = Path.Combine(@Info.GetExecutionPath(), "<ClassName>.mlnet");
// 5. Public entry method — called from Expression or Task
public void Predict(double inputValue)
{
trainingBuffer.Add(new SensorData { Value = (float)inputValue });
if (!modelTrained && trainingBuffer.Count >= MinTrainingSize)
TrainModel();
if (modelTrained)
RunPrediction(inputValue);
}
// 6. LoadModel — called from ServerStartup to reload persisted model
public void LoadModel()
{
if (File.Exists(ModelPath))
{
model = mlContext.Model.Load(ModelPath, out _);
modelTrained = true;
}
}
// 7. TrainModel — build, fit, and persist the ML pipeline
private void TrainModel()
{
lastTrainingDataView = mlContext.Data.LoadFromEnumerable(trainingBuffer);
// pipeline.Fit() call here — see task-specific examples below
model = pipeline.Fit(lastTrainingDataView);
modelTrained = true;
SaveModel();
}
// 8. SaveModel — persist to disk after training
private void SaveModel()
{
mlContext.Model.Save(model, lastTrainingDataView.Schema, ModelPath);
}
// 9. RunPrediction — transform input and write to output tags
private void RunPrediction(double inputValue) { /* ... */ }
|
Always use the @Tag. prefix to read or write tag values:
// Read from a tag double temp = @Tag.Plant/Reactor1/Temperature.Value; // Write to a tag @Tag.Plant/Reactor1/ML/AnomalyScore.Value = score; @Tag.Plant/Reactor1/ML/IsAnomaly.Value = true; @Tag.Plant/Reactor1/ML/LastPrediction.Value = DateTime.Now; |
Important: ML.NET expects float but FrameworX tags use double. Always cast with (float) when feeding ML.NET and cast back to double when writing to tags.
Write the complete class with write_objects. The AI generates the full C# code based on the ML task chosen in Step 0.
{
"table_type": "ScriptsClasses",
"data": [
{
"Name": "<ClassName>",
"Code": "CSharp",
"Domain": "Server",
"ClassContent": "Methods",
"NamespaceDeclarations": "Microsoft.ML;Microsoft.ML.Data;Microsoft.ML.Transforms;Microsoft.ML.Transforms.TimeSeries;Microsoft.ML.Transforms.Text;Microsoft.ML.Trainers;Microsoft.ML.TimeSeries",
"Contents": "<AI-generated C# code following the class structure pattern>"
}
]
}
|
Field names matter: the language field is |
The AI must generate the Code field dynamically based on:
SaveModel, LoadModel, and ModelPath — persistence is standardUse these as the basis for generating the Code. Adapt parameters to the user's data characteristics.
The AI chooses the appropriate variant based on the user's description:
Default: SSA Spike Detection (most common industrial use case).
var pipeline = mlContext.Transforms.DetectSpikeBySsa(
outputColumnName: "Prediction",
inputColumnName: nameof(SensorData.Value),
confidence: 95.0,
pvalueHistoryLength: 10,
trainingWindowSize: 100,
seasonalityWindowSize: 10);
|
Output: double[] Prediction with 0=isAnomaly, 1=score, 2=pValue
var pipeline = mlContext.Transforms.DetectChangePointBySsa(
outputColumnName: "Prediction",
inputColumnName: nameof(SensorData.Value),
confidence: 95.0,
changeHistoryLength: 10,
trainingWindowSize: 100,
seasonalityWindowSize: 10);
|
Output: double[] Prediction with 0=alert, 1=score, 2=pValue, 3=martingaleValue
public class SensorData
{
public float Value { get; set; }
}
public class SpikePrediction
{
[VectorType(3)]
public double[] Prediction { get; set; }
}
private static MLContext mlContext = new MLContext(seed: 0);
private static ITransformer model;
private static IDataView lastTrainingDataView;
private static bool modelTrained = false;
private static List<SensorData> trainingBuffer = new List<SensorData>();
private const int MinTrainingSize = 100;
private static readonly string ModelPath = Path.Combine(@Info.GetExecutionPath(), "<ClassName>.mlnet");
public void Predict(double inputValue)
{
trainingBuffer.Add(new SensorData { Value = (float)inputValue });
if (!modelTrained && trainingBuffer.Count >= MinTrainingSize)
TrainModel();
if (modelTrained)
RunPrediction();
}
public void LoadModel()
{
if (File.Exists(ModelPath))
{
model = mlContext.Model.Load(ModelPath, out _);
modelTrained = true;
}
}
private void TrainModel()
{
lastTrainingDataView = mlContext.Data.LoadFromEnumerable(trainingBuffer);
var pipeline = mlContext.Transforms.DetectSpikeBySsa(
outputColumnName: "Prediction",
inputColumnName: nameof(SensorData.Value),
confidence: 95.0,
pvalueHistoryLength: 10,
trainingWindowSize: 100,
seasonalityWindowSize: 10);
model = pipeline.Fit(lastTrainingDataView);
modelTrained = true;
SaveModel();
}
private void SaveModel()
{
mlContext.Model.Save(model, lastTrainingDataView.Schema, ModelPath);
}
private void RunPrediction()
{
var dataView = mlContext.Data.LoadFromEnumerable(trainingBuffer);
var transformed = model.Transform(dataView);
var predictions = mlContext.Data.CreateEnumerable<SpikePrediction>(transformed, reuseRowObject: false).ToList();
var latest = predictions.Last();
@Tag.<AssetPath>/ML/IsAnomaly.Value = latest.Prediction[0] == 1;
@Tag.<AssetPath>/ML/AnomalyScore.Value = latest.Prediction[1];
@Tag.<AssetPath>/ML/LastPrediction.Value = DateTime.Now;
}
|
var pipeline = mlContext.Forecasting.ForecastBySsa(
outputColumnName: "ForecastedValues",
inputColumnName: nameof(SensorData.Value),
windowSize: 10,
seriesLength: 100,
trainSize: trainingBuffer.Count,
horizon: 5,
confidenceLevel: 0.95f,
confidenceLowerBoundColumn: "LowerBound",
confidenceUpperBoundColumn: "UpperBound");
|
Output: float[] ForecastedValues, float[] LowerBound, float[] UpperBound
public class SensorData
{
public float Value { get; set; }
}
public class ForecastOutput
{
public float[] ForecastedValues { get; set; }
public float[] LowerBound { get; set; }
public float[] UpperBound { get; set; }
}
private static MLContext mlContext = new MLContext(seed: 0);
private static TimeSeriesPredictionEngine<SensorData, ForecastOutput> forecastEngine;
private static ITransformer model;
private static IDataView lastTrainingDataView;
private static bool modelTrained = false;
private static List<SensorData> trainingBuffer = new List<SensorData>();
private const int MinTrainingSize = 100;
private static readonly string ModelPath = Path.Combine(@Info.GetExecutionPath(), "<ClassName>.mlnet");
public void Predict(double inputValue)
{
trainingBuffer.Add(new SensorData { Value = (float)inputValue });
if (!modelTrained && trainingBuffer.Count >= MinTrainingSize)
TrainModel();
if (modelTrained)
RunPrediction();
}
public void LoadModel()
{
if (File.Exists(ModelPath))
{
model = mlContext.Model.Load(ModelPath, out _);
forecastEngine = model.CreateTimeSeriesEngine<SensorData, ForecastOutput>(mlContext);
modelTrained = true;
}
}
private void TrainModel()
{
lastTrainingDataView = mlContext.Data.LoadFromEnumerable(trainingBuffer);
var pipeline = mlContext.Forecasting.ForecastBySsa(
outputColumnName: "ForecastedValues",
inputColumnName: nameof(SensorData.Value),
windowSize: 10,
seriesLength: 100,
trainSize: trainingBuffer.Count,
horizon: 5,
confidenceLevel: 0.95f,
confidenceLowerBoundColumn: "LowerBound",
confidenceUpperBoundColumn: "UpperBound");
model = pipeline.Fit(lastTrainingDataView);
forecastEngine = model.CreateTimeSeriesEngine<SensorData, ForecastOutput>(mlContext);
modelTrained = true;
SaveModel();
}
private void SaveModel()
{
mlContext.Model.Save(model, lastTrainingDataView.Schema, ModelPath);
}
private void RunPrediction()
{
var forecast = forecastEngine.Predict();
@Tag.<AssetPath>/ML/Forecast.Value = (double)forecast.ForecastedValues[0];
@Tag.<AssetPath>/ML/ForecastLower.Value = (double)forecast.LowerBound[0];
@Tag.<AssetPath>/ML/ForecastUpper.Value = (double)forecast.UpperBound[0];
@Tag.<AssetPath>/ML/LastPrediction.Value = DateTime.Now;
}
|
var pipeline = mlContext.Transforms.Concatenate("Features", "Feature1", "Feature2", "Feature3")
.Append(mlContext.Regression.Trainers.FastTree(
labelColumnName: "Label",
featureColumnName: "Features",
numberOfLeaves: 20,
numberOfTrees: 100,
minimumExampleCountPerLeaf: 10,
learningRate: 0.2));
|
Output: float Score (predicted continuous value)
public class ProcessData
{
public float Feature1 { get; set; } // e.g., Temperature
public float Feature2 { get; set; } // e.g., Pressure
public float Feature3 { get; set; } // e.g., Flow
public float Label { get; set; } // e.g., EnergyConsumption (what we predict)
}
public class RegressionPrediction
{
public float Score { get; set; }
}
private static MLContext mlContext = new MLContext(seed: 0);
private static ITransformer model;
private static IDataView lastTrainingDataView;
private static PredictionEngine<ProcessData, RegressionPrediction> predictionEngine;
private static bool modelTrained = false;
private static List<ProcessData> trainingBuffer = new List<ProcessData>();
private const int MinTrainingSize = 200;
private static readonly string ModelPath = Path.Combine(@Info.GetExecutionPath(), "<ClassName>.mlnet");
public void Predict(double input1, double input2, double input3, double label)
{
trainingBuffer.Add(new ProcessData
{
Feature1 = (float)input1,
Feature2 = (float)input2,
Feature3 = (float)input3,
Label = (float)label
});
if (!modelTrained && trainingBuffer.Count >= MinTrainingSize)
TrainModel();
if (modelTrained)
RunPrediction(input1, input2, input3);
}
public void LoadModel()
{
if (File.Exists(ModelPath))
{
model = mlContext.Model.Load(ModelPath, out _);
predictionEngine = mlContext.Model.CreatePredictionEngine<ProcessData, RegressionPrediction>(model);
modelTrained = true;
}
}
private void TrainModel()
{
lastTrainingDataView = mlContext.Data.LoadFromEnumerable(trainingBuffer);
var pipeline = mlContext.Transforms.Concatenate("Features",
nameof(ProcessData.Feature1),
nameof(ProcessData.Feature2),
nameof(ProcessData.Feature3))
.Append(mlContext.Regression.Trainers.FastTree(
labelColumnName: "Label",
featureColumnName: "Features",
numberOfLeaves: 20,
numberOfTrees: 100,
minimumExampleCountPerLeaf: 10,
learningRate: 0.2));
model = pipeline.Fit(lastTrainingDataView);
predictionEngine = mlContext.Model.CreatePredictionEngine<ProcessData, RegressionPrediction>(model);
modelTrained = true;
SaveModel();
}
private void SaveModel()
{
mlContext.Model.Save(model, lastTrainingDataView.Schema, ModelPath);
}
private void RunPrediction(double input1, double input2, double input3)
{
var input = new ProcessData
{
Feature1 = (float)input1,
Feature2 = (float)input2,
Feature3 = (float)input3
};
var result = predictionEngine.Predict(input);
@Tag.<AssetPath>/ML/PredictedValue.Value = (double)result.Score;
@Tag.<AssetPath>/ML/LastPrediction.Value = DateTime.Now;
}
|
Note on Regression training data: The Predict() method above accepts a label parameter during the training phase. This is the known actual value that the model learns to predict. During prediction-only mode (after training), the label is not needed. The AI should adapt the method signature based on whether the user has a label tag or wants to train from historical data.
var pipeline = mlContext.Transforms.Concatenate("Features", "Feature1", "Feature2", "Feature3")
.Append(mlContext.BinaryClassification.Trainers.FastTree(
labelColumnName: "Label",
featureColumnName: "Features"));
|
Output: bool PredictedLabel, float Score, float Probability
public class ProcessData
{
public float Feature1 { get; set; } // e.g., Vibration
public float Feature2 { get; set; } // e.g., Temperature
public float Feature3 { get; set; } // e.g., Current
public bool Label { get; set; } // e.g., DidFault (true/false)
}
public class ClassificationPrediction
{
public bool PredictedLabel { get; set; }
public float Score { get; set; }
public float Probability { get; set; }
}
private static MLContext mlContext = new MLContext(seed: 0);
private static ITransformer model;
private static IDataView lastTrainingDataView;
private static PredictionEngine<ProcessData, ClassificationPrediction> predictionEngine;
private static bool modelTrained = false;
private static List<ProcessData> trainingBuffer = new List<ProcessData>();
private const int MinTrainingSize = 200;
private static readonly string ModelPath = Path.Combine(@Info.GetExecutionPath(), "<ClassName>.mlnet");
public void Predict(double input1, double input2, double input3, bool label)
{
trainingBuffer.Add(new ProcessData
{
Feature1 = (float)input1,
Feature2 = (float)input2,
Feature3 = (float)input3,
Label = label
});
if (!modelTrained && trainingBuffer.Count >= MinTrainingSize)
TrainModel();
if (modelTrained)
RunPrediction(input1, input2, input3);
}
public void LoadModel()
{
if (File.Exists(ModelPath))
{
model = mlContext.Model.Load(ModelPath, out _);
predictionEngine = mlContext.Model.CreatePredictionEngine<ProcessData, ClassificationPrediction>(model);
modelTrained = true;
}
}
private void TrainModel()
{
lastTrainingDataView = mlContext.Data.LoadFromEnumerable(trainingBuffer);
var pipeline = mlContext.Transforms.Concatenate("Features",
nameof(ProcessData.Feature1),
nameof(ProcessData.Feature2),
nameof(ProcessData.Feature3))
.Append(mlContext.BinaryClassification.Trainers.FastTree(
labelColumnName: "Label",
featureColumnName: "Features"));
model = pipeline.Fit(lastTrainingDataView);
predictionEngine = mlContext.Model.CreatePredictionEngine<ProcessData, ClassificationPrediction>(model);
modelTrained = true;
SaveModel();
}
private void SaveModel()
{
mlContext.Model.Save(model, lastTrainingDataView.Schema, ModelPath);
}
private void RunPrediction(double input1, double input2, double input3)
{
var input = new ProcessData
{
Feature1 = (float)input1,
Feature2 = (float)input2,
Feature3 = (float)input3
};
var result = predictionEngine.Predict(input);
@Tag.<AssetPath>/ML/PredictedLabel.Value = result.PredictedLabel;
@Tag.<AssetPath>/ML/Probability.Value = (double)result.Probability;
@Tag.<AssetPath>/ML/LastPrediction.Value = DateTime.Now;
}
|
Note on Classification training data: Same as Regression — the label parameter is needed during training. The AI should adapt based on whether a label tag exists. For fault prediction, the label is typically a boolean tag that records when a fault occurred historically.
Connect the ML class to live tag changes so the model runs automatically.
Best for anomaly detection and forecasting on a single tag.
get_table_schema('ScriptsExpressions')
|
{
"table_type": "ScriptsExpressions",
"data": [
{
"Name": "ML_Predict_<SensorName>",
"ObjectName": "",
"Expression": "@Script.Class.<ClassName>.Predict(@Tag.<AssetPath>.<Member>)",
"Execution": "OnChange",
"Trigger": "<AssetPath>"
}
]
}
|
Use |
Best for regression and classification where multiple tags feed the model simultaneously.
get_table_schema('ScriptsTasks')
|
{
"table_type": "ScriptsTasks",
"data": [
{
"Name": "ML_Predict_Periodic",
"Language": "CSharp",
"Execution": "Periodic",
"Period": 5000,
"Code": "@Script.Class.<ClassName>.Predict(\n @Tag.Plant/Reactor1/Temperature.Value,\n @Tag.Plant/Reactor1/Pressure.Value,\n @Tag.Plant/Reactor1/Flow.Value,\n @Tag.Plant/Reactor1/EnergyConsumption.Value);"
}
]
}
|
The |
Always wire LoadModel() into ServerStartup. Read the existing task first (document object — read-modify-write), then append the LoadModel call.
Script.Class.<ClassName>.LoadModel(); |
Read the existing ServerStartup task first (document object — read-modify-write), then append the LoadModel call.
After creating all objects:
System.Math / CpuMath error. Instruct the user:Only proceed if the user confirms this is already set."Before starting the runtime, please confirm your solution is set to Multiplatform: Solution → Settings → Target Platform = Multiplatform, then Product → Modify."
designer_action('start_runtime') if the user explicitly requests it.MinTrainingSize data points before predictions beginLastPrediction timestamp is updatingMistake | Why It Happens | How to Avoid |
|---|---|---|
Missing ML.NET namespaces | Used | Always set |
| Called | Always use |
Tag reference without | Confusing with Expression syntax | Always |
Model lost on restart | SaveModel or LoadModel not wired up | Always include |
Training on every call | No guard for already-trained model | Use |
Wrong data types | ML.NET expects | Cast with |
Expression ObjectName missing | Confusing tag path vs expression binding | Expression ObjectName needs |
Non-empty | Expression tries to assign void return to a tag | Leave |
Used | Field does not exist — silently ignored, expression never fires | Use |
Class is document object | Partial write replaces entire class | Always read-modify-write for existing classes |
|
| Never assign a raw |
| Tasks and Classes compile in the same pass; if the Class isn't ordered first, Tasks that reference it fail | Set |
| Solution is targeting .NET 4.8 (Windows platform) — ML.NET CpuMath trainers (FastTree, SSA) are incompatible with .NET 4.8 | Go to Solution → Settings → Target Platform → Multiplatform, then Product → Modify to rebuild. This is required for all ML.NET solutions. |
Scenario | ML Task | Trigger | Notes |
|---|---|---|---|
Single sensor, detect outliers/spikes | Anomaly Detection (Spike) | Expression OnChange | Fast, one tag in / flags out |
Single sensor, detect gradual drift | Anomaly Detection (ChangePoint) | Expression OnChange | AI picks this variant when user mentions "drift" or "regime change" |
Single sensor, predict future values | Forecasting (SSA) | Expression OnChange or Periodic | Outputs forecast + confidence bounds |
Multiple sensors → one continuous value | Regression | Task Periodic | Energy prediction, process modeling |
Multiple sensors → yes/no | Binary Classification | Task Periodic | Fault prediction, quality pass/fail |
User says "predictive maintenance" + single sensor | Anomaly Detection | Expression OnChange | Most common PdM entry point |
User says "predictive maintenance" + multiple sensors | Binary Classification | Task Periodic | Predicts failure from combined inputs |
User says "quality control" | Binary Classification | Task Periodic | Pass/fail prediction |
User says "forecast" or "predict demand" | Forecasting (SSA) | Expression OnChange or Periodic | Time-series based |
User says "you decide" + single sensor | Anomaly Detection | Expression OnChange | Safest default for monitoring |
User says "you decide" + multiple sensors | Regression | Task Periodic | Most general multi-input approach |