Live demo — data resets daily at 03:00 UTC. Nothing you enter is saved. Server UI →

product: maestro audience: test-developer authority: normative

.NET Runner SDK

The Maestro runner DOES NOT build your project. You MUST run dotnet publish and commit the output to assemblies/ before the runner can load your DLLs. See reference/local-dev-commands.md for 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 static and instance methods are supported.
  • async Task<T> and async Task are 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 scatter traces with fill: "tozeroy" or fill: "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].

An unhandled error has occurred. Reload 🗙

Rejoining the server...

Rejoin failed... trying again in seconds.

Failed to rejoin.
Please retry or reload the page.

The session has been paused by the server.

Failed to resume the session.
Please retry or reload the page.