product: maestro audience: test-developer authority: normative
.NET Runner SDK
The Maestro runner DOES NOT build your project. You MUST run
dotnet publishand commit the output toassemblies/before the runner can load your DLLs. Seereference/local-dev-commands.mdfor the exact command.
Project setup
Target framework: net10.0.
To return MeasurementPointBase objects (recommended for structured pass/fail limits),
reference WorkflowEngine.Contracts. When you create a .NET or Mixed package through
the TAT UI, WorkflowEngine.Contracts.dll IS automatically copied into assemblies/.
<Reference Include="WorkflowEngine.Contracts">
<HintPath>..\..\assemblies\WorkflowEngine.Contracts.dll</HintPath>
</Reference>
Class and method requirements
- The class MUST be
static, or a regular class with a public parameterless constructor. - The method MUST be
public. - Both
staticand instance methods are supported. async Task<T>andasync Taskare fully supported.
Parameter passing
The runner matches YAML parameters: keys to C# parameter names using reflection.
All values arrive as strings and are automatically converted to the declared type.
| C# type | Conversion |
|---|---|
string |
No conversion |
int |
int.Parse |
double |
double.Parse |
bool |
bool.Parse |
long |
long.Parse |
decimal |
decimal.Parse |
Parameters not present in parameters: are resolved from step-level variables.
Parameters with default values are optional in YAML.
Return types
| Situation | Recommended return type |
|---|---|
| Single measurement, limits in YAML | Scalar (double, bool, string) |
| Multiple output variables, limits in YAML | Dictionary<string, object> |
| Multiple measurements, limits in code | List<MeasurementPointBase> |
| Setup / teardown, no measurement | void / Task |
| Return type | Effect |
|---|---|
| Any scalar | Stored as output variable result — use {{result}} in YAML |
Dictionary<string, object> |
Keys become output variables |
void / Task |
Success, no outputs |
MeasurementPointBase subclass |
Single measurement point recorded |
IEnumerable<MeasurementPointBase> |
Multiple measurement points recorded |
Task<T> |
Async equivalent |
{{result}}IS the implicit variable name for scalar returns. Do NOT confuse{{result}}with{{value}}— the latter looks for a variable named "value".
Examples
using WorkflowEngine.Contracts.MeasurementPoints;
using static WorkflowEngine.Contracts.Types;
// Scalar return — use {{result}} in YAML
public static double MeasureVoltage(string rail)
=> ReadRail(rail);
// Output variables
public static Dictionary<string, object> MeasureVoltage(string channel)
=> new() { ["voltage"] = ReadChannel(channel) };
// No output (setup/teardown)
public static void PowerOn(string rail) => ApplyPower(rail);
// Single numeric measurement (limits in code)
public static NumericMeasurementPoint MeasureVoltage(string channel)
=> new NumericMeasurementPoint
{
Name = "VBOARD",
Value = ReadChannel(channel),
LowerLimit = 4.75,
UpperLimit = 5.25,
Units = "V",
ComparisonOperator = ComparisonOperatorType.GreaterThanOrEqual
};
// Multiple measurements
public static List<MeasurementPointBase> PowerTest(double voltage, double current)
=>
[
new NumericMeasurementPoint { Name = "VOLTAGE", Value = voltage,
LowerLimit = 4.5, UpperLimit = 5.5, Units = "V",
ComparisonOperator = ComparisonOperatorType.GreaterThanOrEqual },
new NumericMeasurementPoint { Name = "CURRENT", Value = current,
LowerLimit = 0.8, UpperLimit = 1.2, Units = "A",
ComparisonOperator = ComparisonOperatorType.GreaterThanOrEqual },
];
// Async
public static async Task<Dictionary<string, object>> MeasureAsync(string channel)
{
double voltage = await ReadChannelAsync(channel);
return new() { ["voltage"] = voltage };
}
ComparisonOperator values
| Value | Meaning |
|---|---|
Equal |
value == limit |
NotEqual |
value != expected |
GreaterThan |
value > LowerLimit |
GreaterThanOrEqual |
value >= LowerLimit |
LessThan |
value < UpperLimit |
LessThanOrEqual |
value <= UpperLimit |
Log |
Informational only — always PASS |
For a range check (low_limit ≤ value ≤ high_limit) use the YAML measurement:
block — the engine handles range comparison automatically.
Emitting Artifacts
A step can attach files (images, Plotly charts, raw data) to the execution report so they are visible in the Maestro UI without the operator downloading anything.
EmitArtifact(string path, string description, string? contentType = null)
| Parameter | Type | Description |
|---|---|---|
path |
string |
Absolute path to the file to attach |
description |
string |
Human-readable label shown in the report |
contentType |
string? |
MIME type. Inferred from extension if null. |
The method POSTs the file to the API under the current execution and step. Artifacts are shown inline (images and Plotly JSON) or as download links.
Inline image example:
using System.IO;
public static async Task PlotBode(string frequencies, string gains)
{
// ... generate PNG with a charting library ...
string path = Path.Combine(Path.GetTempPath(), "bode.png");
SavePngTo(path);
EmitArtifact(path, "Bode plot");
}
The PNG is rendered inline as a thumbnail in the step row of the test report.
EmitPlotly(string plotlyJson, string description)
Emits a Plotly figure JSON string as a step artifact with
content_type = "application/vnd.plotly.v1+json".
The Maestro UI renders it as a fully interactive Plotly chart.
public static async Task BodeInteractive(string frequencies, string gains)
{
var freq = frequencies.Split(',').Select(double.Parse).ToArray();
var gain = gains.Split(',').Select(double.Parse).ToArray();
// Build a minimal Plotly figure using anonymous types and System.Text.Json
var figure = new
{
data = new[]
{
new { type = "scatter", x = freq, y = gain, mode = "lines", name = "Gain (dB)" }
},
layout = new { xaxis = new { type = "log", title = "Hz" }, yaxis = new { title = "dB" } }
};
string json = System.Text.Json.JsonSerializer.Serialize(figure);
EmitPlotly(json, "Bode plot (interactive)");
}
Limit band convention — encode pass/fail limits as filled
scattertraces withfill: "tozeroy"orfill: "tonexty"so all Bode plots share a consistent look.
Error signalling
Throw any exception → runner catches it → ABORTED verdict. The full exception
message IS stored and displayed in the UI.
Returning null from a Dictionary<string, object> method DOES NOT fail the step —
it is treated as an empty output set.
Logging from test code
Write to Console.Out or Console.Error. Output IS captured and displayed in the
step log. Optional [LEVEL] prefixes are parsed:
Console.WriteLine("Connecting to " + address); // Information
Console.WriteLine("[DEBUG] Raw response: " + raw); // Debug
Console.Error.WriteLine("[WARNING] Retry 1 of 3"); // Warning
Supported prefixes: [INFO], [DEBUG], [WARNING] / [WARN], [ERROR] / [ERR], [TRACE].