Runs
Capture structured results from every test execution. Browse runs with filters, inspect measurements, phases, and attachments for each run.

Overview
A Run captures everything from test execution: the unit being tested, procedure metadata, phases, measurements, logs, and attachments.
Create Runs
You can create runs by linking your test script to a procedure. Create a procedure first, then reference it in your script.
Go to Procedures, click Create Procedure, and copy the
procedure_id (e.g., "FVT1").
Add the procedure_id to your test script to link execution to your
procedure.
Define the unit under test with serial_number and part_number.
Required Metadata
All runs require these fields:
- OpenHTF: Add
procedure_idandpart_numberto your Test, then provideserial_numberduring execution. TofuPilot automatically determines test outcome. - Python: Define
procedure_id,unit_under_test, andrun_passed.
| Prop | Type | Default |
|---|---|---|
procedure_id? | str | – |
serial_number? | str | – |
part_number? | str | – |
run_passed? | bool | – |
import openhtf as htf
from tofupilot.openhtf import TofuPilot
def main():
test = htf.Test(
procedure_id="FVT1", # Link to the Procedure created in the Dashboard
part_number="PCB01", # Part number (required)
)
with TofuPilot(test):
test.execute(lambda: "SN-0001") # UUT serial number (required)
if __name__ == "__main__":
main()from datetime import datetime, timezone, timedelta
from tofupilot.v2 import TofuPilot
client = TofuPilot()
started_at = datetime.now(timezone.utc)
ended_at = started_at + timedelta(seconds=30)
run = client.runs.create(
procedure_id="FVT1", # Link to the Procedure created in the Dashboard
serial_number="SN-0001", # Serial number (required)
part_number="PCB01", # Part number (required)
outcome="PASS", # PASS | FAIL | ERROR | TIMEOUT | ABORTED
started_at=started_at, # ISO 8601 timestamp (required)
ended_at=ended_at, # ISO 8601 timestamp (required)
)
print(f"Run created: {run.id}")using TofuPilot;
using TofuPilot.Models.Requests;
var client = new TofuPilot();
var startedAt = DateTime.UtcNow;
var endedAt = startedAt.AddSeconds(30);
var run = await client.Runs.CreateAsync(new RunCreateRequest
{
ProcedureId = "FVT1",
SerialNumber = "SN-0001",
PartNumber = "PCB01",
Outcome = RunCreateOutcome.Pass,
StartedAt = startedAt,
EndedAt = endedAt,
});
Console.WriteLine($"Run created: {run.Id}");Optional Metadata
Include additional metadata for better tracking:
- OpenHTF: Duration is calculated automatically from phase timestamps.
- Python: Set all metadata including duration manually.
| Prop | Type | Default |
|---|---|---|
procedure_version? | str | – |
duration? | timedelta | – |
batch_number? | str | – |
revision? | str | – |
import openhtf as htf
from tofupilot.openhtf import TofuPilot
def main():
test = htf.Test(
procedure_id="FVT1",
part_number="PCB01",
procedure_version="v2.1.0", # Track test procedure version
batch_number="2024-001",
revision="B",
)
with TofuPilot(test):
test.execute(lambda: "SN-0001")
if __name__ == "__main__":
main()from datetime import datetime, timezone, timedelta
from tofupilot.v2 import TofuPilot
client = TofuPilot()
started_at = datetime.now(timezone.utc)
ended_at = started_at + timedelta(minutes=2, seconds=34)
run = client.runs.create(
procedure_id="FVT1",
serial_number="SN-0001",
part_number="PCB01",
revision_number="B", # Hardware revision
batch_number="2024-001", # Production batch
procedure_version="v2.1.0", # Track test procedure version
outcome="PASS",
started_at=started_at,
ended_at=ended_at,
)using TofuPilot;
using TofuPilot.Models.Requests;
var client = new TofuPilot();
var startedAt = DateTime.UtcNow;
var endedAt = startedAt.AddMinutes(2).AddSeconds(34);
var run = await client.Runs.CreateAsync(new RunCreateRequest
{
ProcedureId = "FVT1",
SerialNumber = "SN-0001",
PartNumber = "PCB01",
RevisionNumber = "B",
BatchNumber = "2024-001",
ProcedureVersion = "v2.1.0",
Outcome = RunCreateOutcome.Pass,
StartedAt = startedAt,
EndedAt = endedAt,
});Phases
Phases organize tests into logical sections for easier debugging and analysis.
- OpenHTF: Define phase functions in your script. TofuPilot captures name, outcome, and timing automatically.
- Python: Create phase dictionaries with timing, outcome, and measurements.
| Prop | Type | Default |
|---|---|---|
name? | str | – |
outcome? | "PASS" | "FAIL" | "ERROR" | "SKIP" | – |
start_time_millis? | number | – |
end_time_millis? | number | – |
measurements? | array | – |
import openhtf as htf
from tofupilot.openhtf import TofuPilot
def phase_one(test): # Phase name is taken from the function name
return htf.PhaseResult.CONTINUE # Pass outcome
def main():
test = htf.Test(
phase_one,
procedure_id="FVT1",
part_number="PCB1",
)
with TofuPilot(test):
# Duration and start time are set automatically
test.execute(lambda: "SN-0001")
if __name__ == "__main__":
main()from datetime import datetime, timezone, timedelta
from tofupilot.v2 import TofuPilot
client = TofuPilot()
started_at = datetime.now(timezone.utc)
ended_at = started_at + timedelta(seconds=30)
run = client.runs.create(
procedure_id="FVT1",
serial_number="PCB1A001",
part_number="PCB01",
outcome="PASS",
started_at=started_at,
ended_at=ended_at,
phases=[
{
"name": "phase_one",
"outcome": "PASS",
"started_at": started_at,
"ended_at": ended_at,
}
],
)using TofuPilot;
using TofuPilot.Models.Requests;
var client = new TofuPilot();
var startedAt = DateTime.UtcNow;
var endedAt = startedAt.AddSeconds(30);
var run = await client.Runs.CreateAsync(new RunCreateRequest
{
ProcedureId = "FVT1",
SerialNumber = "PCB1A001",
PartNumber = "PCB01",
Outcome = RunCreateOutcome.Pass,
StartedAt = startedAt,
EndedAt = endedAt,
Phases = new List<RunCreatePhases>
{
new RunCreatePhases
{
Name = "phase_one",
Outcome = RunCreatePhasesOutcome.Pass,
StartedAt = startedAt,
EndedAt = endedAt,
}
},
});Measurements
Measurements capture test data like voltage, temperature, or pass/fail conditions. Support includes simple values and multi-dimensional arrays.
- OpenHTF: Use measurement decorators to define values, ranges, and units. TofuPilot handles collection and validation automatically.
- Python: Build measurement dictionaries with values, units, validators, and outcomes.
| Prop | Type | Default |
|---|---|---|
name? | string | – |
measured_value? | number | string | boolean | dict | – |
outcome? | "PASS" | "FAIL" | "UNSET" | – |
units? | string | – |
validators? | array | – |
aggregations? | array | – |
import openhtf as htf
from openhtf.util import units
from tofupilot.openhtf import TofuPilot
# Decorator to set measurement name, unit and limits
@htf.measures(htf.Measurement("voltage").in_range(3.1, 3.5).with_units(units.VOLT))
# Phase returns a Pass status because measurement (3.3) passes all validators
def phase_voltage_measure(test):
test.measurements.voltage = 3.3
def main():
test = htf.Test(
phase_voltage_measure,
procedure_id="FVT1",
part_number="PCB1",
)
with TofuPilot(test):
test.execute(lambda: "SN-0001")
if __name__ == "__main__":
main()
from datetime import datetime, timezone, timedelta
from tofupilot.v2 import TofuPilot
client = TofuPilot()
started_at = datetime.now(timezone.utc)
ended_at = started_at + timedelta(seconds=30)
run = client.runs.create(
procedure_id="FVT1",
serial_number="PCB1A001",
part_number="PCB01",
outcome="PASS",
started_at=started_at,
ended_at=ended_at,
phases=[
{
"name": "voltage_measure_phase",
"outcome": "PASS",
"started_at": started_at,
"ended_at": ended_at,
"measurements": [
{
"name": "voltage",
"outcome": "PASS",
"measured_value": 3.3,
"units": "V",
"validators": [
{"operator": ">=", "expected_value": 3.1, "outcome": "PASS"},
{"operator": "<=", "expected_value": 3.5, "outcome": "PASS"},
],
}
],
}
],
)
using TofuPilot;
using TofuPilot.Models.Requests;
var client = new TofuPilot();
var startedAt = DateTime.UtcNow;
var endedAt = startedAt.AddSeconds(30);
var run = await client.Runs.CreateAsync(new RunCreateRequest
{
ProcedureId = "FVT1",
SerialNumber = "PCB1A001",
PartNumber = "PCB01",
Outcome = RunCreateOutcome.Pass,
StartedAt = startedAt,
EndedAt = endedAt,
Phases = new List<RunCreatePhases>
{
new RunCreatePhases
{
Name = "voltage_measure_phase",
Outcome = RunCreatePhasesOutcome.Pass,
StartedAt = startedAt,
EndedAt = endedAt,
Measurements = new List<RunCreateMeasurements>
{
new RunCreateMeasurements
{
Name = "voltage",
Outcome = RunCreateMeasurementsOutcome.Pass,
MeasuredValue = (object)3.3,
Units = RunCreateUnits.CreateStr("V"),
Validators = (object)new List<RunCreateValidators>
{
new RunCreateValidators { Operator = ">=", ExpectedValue = RunCreateExpectedValue.CreateNumber(3.1), Outcome = "PASS" },
new RunCreateValidators { Operator = "<=", ExpectedValue = RunCreateExpectedValue.CreateNumber(3.5), Outcome = "PASS" },
},
}
},
}
},
});Numerical
Record numeric values like voltage, resistance, or temperature:
import openhtf as htf
from openhtf.util import units
from tofupilot.openhtf import TofuPilot
@htf.measures(
htf.Measurement("temperature") # Declares the measurement name
.in_range(0, 100) # Defines the lower and upper limits
.with_units(units.DEGREE_CELSIUS) # Specifies the unit
)
def phase_temp(test): # Record numerical measurement - temperature value
test.measurements.temperature = 25.3
return htf.PhaseResult.CONTINUE
def main():
test = htf.Test(
phase_temp,
procedure_id="FVT1",
part_number="PCB1",
)
with TofuPilot(test):
test.execute(lambda: "SN-0001")
if __name__ == "__main__":
main()from datetime import datetime, timezone, timedelta
from tofupilot.v2 import TofuPilot
client = TofuPilot()
started_at = datetime.now(timezone.utc)
ended_at = started_at + timedelta(seconds=5)
run = client.runs.create(
procedure_id="FVT1",
serial_number="PCB1A001",
part_number="PCB01",
outcome="PASS",
started_at=started_at,
ended_at=ended_at,
phases=[
{
"name": "phase_temperature",
"outcome": "PASS",
"started_at": started_at,
"ended_at": ended_at,
"measurements": [
{
"name": "temperature",
"measured_value": 25.3,
"units": "C",
"outcome": "PASS",
"validators": [
{"operator": ">=", "expected_value": 0, "outcome": "PASS"},
{"operator": "<=", "expected_value": 100, "outcome": "PASS"},
],
}
],
}
],
)using TofuPilot;
using TofuPilot.Models.Requests;
var client = new TofuPilot();
var startedAt = DateTime.UtcNow;
var endedAt = startedAt.AddSeconds(5);
var run = await client.Runs.CreateAsync(new RunCreateRequest
{
ProcedureId = "FVT1",
SerialNumber = "PCB1A001",
PartNumber = "PCB01",
Outcome = RunCreateOutcome.Pass,
StartedAt = startedAt,
EndedAt = endedAt,
Phases = new List<RunCreatePhases>
{
new RunCreatePhases
{
Name = "phase_temperature",
Outcome = RunCreatePhasesOutcome.Pass,
StartedAt = startedAt,
EndedAt = endedAt,
Measurements = new List<RunCreateMeasurements>
{
new RunCreateMeasurements
{
Name = "temperature",
MeasuredValue = (object)25.3,
Units = RunCreateUnits.CreateStr("C"),
Outcome = RunCreateMeasurementsOutcome.Pass,
Validators = (object)new List<RunCreateValidators>
{
new RunCreateValidators { Operator = ">=", ExpectedValue = RunCreateExpectedValue.CreateNumber(0), Outcome = "PASS" },
new RunCreateValidators { Operator = "<=", ExpectedValue = RunCreateExpectedValue.CreateNumber(100), Outcome = "PASS" },
},
}
},
}
},
});String
Record text values like serial numbers, firmware versions, or responses:
import openhtf as htf
from tofupilot.openhtf import TofuPilot
@htf.measures(htf.Measurement("firmware_version").equals("1.2.4"))
def phase_firmware(test): # Record string measurement - firmware version
test.measurements.firmware_version = "1.2.4"
return htf.PhaseResult.CONTINUE
def main():
test = htf.Test(
phase_firmware,
procedure_id="FVT1",
part_number="PCB1",
)
with TofuPilot(test):
test.execute(lambda: "SN-0001")
if __name__ == "__main__":
main()from datetime import datetime, timezone, timedelta
from tofupilot.v2 import TofuPilot
client = TofuPilot()
started_at = datetime.now(timezone.utc)
ended_at = started_at + timedelta(seconds=5)
run = client.runs.create(
procedure_id="FVT1",
serial_number="SN-0001",
part_number="PCB01",
outcome="PASS",
started_at=started_at,
ended_at=ended_at,
phases=[
{
"name": "phase_firmware",
"outcome": "PASS",
"started_at": started_at,
"ended_at": ended_at,
"measurements": [
{
"name": "firmware_version",
"measured_value": "1.2.4",
"outcome": "PASS",
"validators": [
{"operator": "==", "expected_value": "1.2.4", "outcome": "PASS"},
],
}
],
}
],
)using TofuPilot;
using TofuPilot.Models.Requests;
var client = new TofuPilot();
var startedAt = DateTime.UtcNow;
var endedAt = startedAt.AddSeconds(5);
var run = await client.Runs.CreateAsync(new RunCreateRequest
{
ProcedureId = "FVT1",
SerialNumber = "SN-0001",
PartNumber = "PCB01",
Outcome = RunCreateOutcome.Pass,
StartedAt = startedAt,
EndedAt = endedAt,
Phases = new List<RunCreatePhases>
{
new RunCreatePhases
{
Name = "phase_firmware",
Outcome = RunCreatePhasesOutcome.Pass,
StartedAt = startedAt,
EndedAt = endedAt,
Measurements = new List<RunCreateMeasurements>
{
new RunCreateMeasurements
{
Name = "firmware_version",
MeasuredValue = (object)"1.2.4",
Outcome = RunCreateMeasurementsOutcome.Pass,
Validators = (object)new List<RunCreateValidators>
{
new RunCreateValidators { Operator = "==", ExpectedValue = RunCreateExpectedValue.CreateStr("1.2.4"), Outcome = "PASS" },
},
}
},
}
},
});Boolean
Record true/false values for conditions, flags, or states:
import openhtf as htf
from tofupilot.openhtf import TofuPilot
@htf.measures(htf.Measurement("is_led_switch_on").equals(True))
def phase_led(test): # Record boolean measurement - LED switch state
test.measurements.is_led_switch_on = True
return htf.PhaseResult.CONTINUE
def main():
test = htf.Test(
phase_led,
procedure_id="FVT1",
part_number="PCB1",
)
with TofuPilot(test):
test.execute(lambda: "SN-0001")
if __name__ == "__main__":
main()from datetime import datetime, timezone, timedelta
from tofupilot.v2 import TofuPilot
client = TofuPilot()
started_at = datetime.now(timezone.utc)
ended_at = started_at + timedelta(seconds=5)
run = client.runs.create(
procedure_id="FVT1",
serial_number="SN-0001",
part_number="PCB01",
outcome="PASS",
started_at=started_at,
ended_at=ended_at,
phases=[
{
"name": "phase_led",
"outcome": "PASS",
"started_at": started_at,
"ended_at": ended_at,
"measurements": [
{
"name": "is_led_on",
"measured_value": True,
"outcome": "PASS",
"validators": [
{"operator": "==", "expected_value": True, "outcome": "PASS"},
],
}
],
}
],
)using TofuPilot;
using TofuPilot.Models.Requests;
var client = new TofuPilot();
var startedAt = DateTime.UtcNow;
var endedAt = startedAt.AddSeconds(5);
var run = await client.Runs.CreateAsync(new RunCreateRequest
{
ProcedureId = "FVT1",
SerialNumber = "SN-0001",
PartNumber = "PCB01",
Outcome = RunCreateOutcome.Pass,
StartedAt = startedAt,
EndedAt = endedAt,
Phases = new List<RunCreatePhases>
{
new RunCreatePhases
{
Name = "phase_led",
Outcome = RunCreatePhasesOutcome.Pass,
StartedAt = startedAt,
EndedAt = endedAt,
Measurements = new List<RunCreateMeasurements>
{
new RunCreateMeasurements
{
Name = "is_led_on",
MeasuredValue = (object)true,
Outcome = RunCreateMeasurementsOutcome.Pass,
Validators = (object)new List<RunCreateValidators>
{
new RunCreateValidators { Operator = "==", ExpectedValue = RunCreateExpectedValue.CreateBoolean(true), Outcome = "PASS" },
},
}
},
}
},
});Multi-dimensional
Record time-series data with multiple dimensions. The Python client supports validators and aggregations at two levels:
- Measurement level: applies to the entire measurement
- Data series level: applies to a specific axis
Data series object:
Python client only. OpenHTF supports multi-dimensional data via
.with_dimensions() but not validators or aggregations.
| Property | Type | Description |
|---|---|---|
data | array | Array of numeric data points |
units | string | Unit for this axis |
validators | array | Validator objects for this axis |
aggregations | array | Aggregation objects computed over this axis data |
import random
import openhtf as htf
from openhtf.util import units
from tofupilot.openhtf import TofuPilot
@htf.measures(
htf.Measurement("current_voltage_resistance_over_time")
.with_dimensions(
units.SECOND, units.VOLT, units.AMPERE
) # Input axes: time, voltage, current
.with_units(units.OHM) # Output unit: resistance in ohms
)
def power_phase(test): # Record multi-dimensional measurement - time series data
for t in range(100):
timestamp = t / 100
voltage = round(random.uniform(3.3, 3.5), 2)
current = round(random.uniform(0.3, 0.8), 3)
resistance = voltage / current
test.measurements.current_voltage_resistance_over_time[
timestamp, voltage, current
] = resistance
return htf.PhaseResult.CONTINUE
def main():
test = htf.Test(
power_phase,
procedure_id="FVT1",
part_number="PCB1",
)
with TofuPilot(test):
test.execute(lambda: "SN-0001")
if __name__ == "__main__":
main()import random
from datetime import datetime, timezone, timedelta
from tofupilot.v2 import TofuPilot
client = TofuPilot()
started_at = datetime.now(timezone.utc)
ended_at = started_at + timedelta(seconds=5)
temperatures = [round(random.uniform(24.5, 25.5), 2) for _ in range(10)]
timestamps_ms = [i * 1000 for i in range(10)]
run = client.runs.create(
procedure_id="FVT1",
serial_number="SN-0001",
part_number="PCB01",
outcome="PASS",
started_at=started_at,
ended_at=ended_at,
phases=[
{
"name": "phase_temp_sweep",
"outcome": "PASS",
"started_at": started_at,
"ended_at": ended_at,
"measurements": [
{
"name": "temperature_sweep",
"outcome": "PASS",
"measured_value": {
"x": {"data": timestamps_ms, "units": "ms"},
"y": {
"data": temperatures,
"units": "C",
"validators": [
{"operator": ">=", "expected_value": 20, "outcome": "PASS"},
{"operator": "<=", "expected_value": 30, "outcome": "PASS"},
],
},
},
}
],
}
],
)using TofuPilot;
using TofuPilot.Models.Requests;
var client = new TofuPilot();
var startedAt = DateTime.UtcNow;
var endedAt = startedAt.AddSeconds(5);
var temperatures = Enumerable.Range(0, 10).Select(_ => Math.Round(24.5 + new Random().NextDouble(), 2)).ToList();
var timestampsMs = Enumerable.Range(0, 10).Select(i => (object)(i * 1000)).ToList();
var run = await client.Runs.CreateAsync(new RunCreateRequest
{
ProcedureId = "FVT1",
SerialNumber = "SN-0001",
PartNumber = "PCB01",
Outcome = RunCreateOutcome.Pass,
StartedAt = startedAt,
EndedAt = endedAt,
Phases = new List<RunCreatePhases>
{
new RunCreatePhases
{
Name = "phase_temp_sweep",
Outcome = RunCreatePhasesOutcome.Pass,
StartedAt = startedAt,
EndedAt = endedAt,
Measurements = new List<RunCreateMeasurements>
{
new RunCreateMeasurements
{
Name = "temperature_sweep",
Outcome = RunCreateMeasurementsOutcome.Pass,
MeasuredValue = (object)new Dictionary<string, object>
{
["x"] = new { data = timestampsMs, units = "ms" },
["y"] = new { data = temperatures.Cast<object>().ToList(), units = "C",
validators = new[] {
new { @operator = ">=", expected_value = 20, outcome = "PASS" },
new { @operator = "<=", expected_value = 30, outcome = "PASS" },
}
},
},
}
},
}
},
});Click the Chart icon in Run > Phases to visualize multi-dimensional data.

Validators
Validators define validation rules for measurements. Each validator specifies an operator, expected value, and outcome.
| Prop | Type | Default |
|---|---|---|
operator? | "≥" | "≤" | ">" | "<" | "==" | "!=" | "in" | "not in" | "matches" | – |
expected_value? | number | string | boolean | array | – |
outcome? | "PASS" | "FAIL" | "UNSET" | UNSET |
is_decisive? | boolean | true |
expression? | string | – |
Type matching rules:
The expected_value type must match the measurement's measured_value type:
| Measurement type | Valid expected_value types | Valid operators |
|---|---|---|
number | number, array[number] | >=, <=, >, <, ==, !=, in, not in |
string | string, array[string] | ==, !=, in, not in, matches |
boolean | boolean | ==, != |
data series | array[number] (same length as data) | ==, != |
from datetime import datetime, timezone, timedelta
from tofupilot.v2 import TofuPilot
client = TofuPilot()
started_at = datetime.now(timezone.utc)
ended_at = started_at + timedelta(seconds=5)
run = client.runs.create(
procedure_id="FVT1",
serial_number="SN-0001",
part_number="PCB01",
outcome="PASS",
started_at=started_at,
ended_at=ended_at,
phases=[
{
"name": "phase_voltage",
"outcome": "PASS",
"started_at": started_at,
"ended_at": ended_at,
"measurements": [
{
"name": "voltage",
"measured_value": 3.3,
"units": "V",
"outcome": "PASS",
"validators": [
{"operator": ">=", "expected_value": 3.0, "outcome": "PASS"},
{"operator": "<=", "expected_value": 3.6, "outcome": "PASS"},
],
}
],
}
],
)using TofuPilot;
using TofuPilot.Models.Requests;
var client = new TofuPilot();
var startedAt = DateTime.UtcNow;
var endedAt = startedAt.AddSeconds(5);
var run = await client.Runs.CreateAsync(new RunCreateRequest
{
ProcedureId = "FVT1",
SerialNumber = "SN-0001",
PartNumber = "PCB01",
Outcome = RunCreateOutcome.Pass,
StartedAt = startedAt,
EndedAt = endedAt,
Phases = new List<RunCreatePhases>
{
new RunCreatePhases
{
Name = "phase_voltage",
Outcome = RunCreatePhasesOutcome.Pass,
StartedAt = startedAt,
EndedAt = endedAt,
Measurements = new List<RunCreateMeasurements>
{
new RunCreateMeasurements
{
Name = "voltage",
MeasuredValue = (object)3.3,
Units = RunCreateUnits.CreateStr("V"),
Outcome = RunCreateMeasurementsOutcome.Pass,
Validators = (object)new List<RunCreateValidators>
{
new RunCreateValidators { Operator = ">=", ExpectedValue = RunCreateExpectedValue.CreateNumber(3.0), Outcome = "PASS" },
new RunCreateValidators { Operator = "<=", ExpectedValue = RunCreateExpectedValue.CreateNumber(3.6), Outcome = "PASS" },
},
}
},
}
},
});On type mismatch: If expected_value type doesn't match measurement type, the validator is stored as expression-only (structured fields cleared, data preserved in expression).
Expression usage:
The expression field serves two purposes:
- Custom display: Provide human-readable text for complex validations
- Fallback storage: On type mismatch, structured data is converted to expression
Some analytics features require structured validators with operator and
expected_value. Expression-only validators are for display purposes and
won't be included in these analytics.
Aggregations
Attach computed results (statistics, derived values) to measurements with optional validation.
Aggregations are only available with the Python client. OpenHTF does not support aggregations natively.
| Prop | Type | Default |
|---|---|---|
type | string | – |
value? | number | string | boolean | – |
unit? | string | – |
outcome? | "PASS" | "FAIL" | "UNSET" | UNSET |
validators? | array | – |
from datetime import datetime, timezone, timedelta
from tofupilot.v2 import TofuPilot
client = TofuPilot()
started_at = datetime.now(timezone.utc)
ended_at = started_at + timedelta(seconds=5)
values = [25.1, 25.3, 24.9, 25.0, 25.2]
run = client.runs.create(
procedure_id="FVT1",
serial_number="SN-0001",
part_number="PCB01",
outcome="PASS",
started_at=started_at,
ended_at=ended_at,
phases=[
{
"name": "phase_temp_stability",
"outcome": "PASS",
"started_at": started_at,
"ended_at": ended_at,
"measurements": [
{
"name": "temperature_stability",
"outcome": "PASS",
"measured_value": {
"x": {"data": list(range(len(values))), "units": "s"},
"y": {
"data": values,
"units": "C",
"aggregations": [
{
"function": "mean",
"value": sum(values) / len(values),
"outcome": "PASS",
"validators": [
{"operator": ">=", "expected_value": 24.0, "outcome": "PASS"},
{"operator": "<=", "expected_value": 26.0, "outcome": "PASS"},
],
},
],
},
},
}
],
}
],
)using TofuPilot;
using TofuPilot.Models.Requests;
var client = new TofuPilot();
var startedAt = DateTime.UtcNow;
var endedAt = startedAt.AddSeconds(5);
var values = new List<double> { 25.1, 25.3, 24.9, 25.0, 25.2 };
var run = await client.Runs.CreateAsync(new RunCreateRequest
{
ProcedureId = "FVT1",
SerialNumber = "SN-0001",
PartNumber = "PCB01",
Outcome = RunCreateOutcome.Pass,
StartedAt = startedAt,
EndedAt = endedAt,
Phases = new List<RunCreatePhases>
{
new RunCreatePhases
{
Name = "phase_temp_stability",
Outcome = RunCreatePhasesOutcome.Pass,
StartedAt = startedAt,
EndedAt = endedAt,
Measurements = new List<RunCreateMeasurements>
{
new RunCreateMeasurements
{
Name = "temperature_stability",
Outcome = RunCreateMeasurementsOutcome.Pass,
MeasuredValue = (object)new Dictionary<string, object>
{
["x"] = new { data = Enumerable.Range(0, values.Count).Cast<object>().ToList(), units = "s" },
["y"] = new { data = values.Cast<object>().ToList(), units = "C",
aggregations = new[] {
new { function_ = "mean", value = values.Average(), outcome = "PASS",
validators = new[] {
new { @operator = ">=", expected_value = 24.0, outcome = "PASS" },
new { @operator = "<=", expected_value = 26.0, outcome = "PASS" },
}
}
}
},
},
}
},
}
},
});Multiple Measurements
Record multiple measurements within one phase:
import random
import openhtf as htf
from openhtf.util import units
from tofupilot.openhtf import TofuPilot
@htf.measures(
htf.Measurement("is_connected").equals(True), # Boolean measure
htf.Measurement("firmware_version").equals("1.2.7"), # String measure
htf.Measurement("input_voltage").in_range(3.2, 3.4).with_units(units.VOLT),
htf.Measurement("input_current").in_range(maximum=1.5).with_units(units.AMPERE),
)
def phase_multi_measurements(test):
test.measurements.is_connected = True
test.measurements.firmware_version = "1.2.7"
test.measurements.input_voltage = round(random.uniform(3.29, 3.42), 2)
test.measurements.input_current = round(random.uniform(1.0, 1.55), 3)
def main():
test = htf.Test(
phase_multi_measurements,
procedure_id="FVT1",
part_number="PCB01",
)
with TofuPilot(test):
test.execute(lambda: "PCB1A004")
if __name__ == "__main__":
main()from datetime import datetime, timezone, timedelta
from tofupilot.v2 import TofuPilot
client = TofuPilot()
started_at = datetime.now(timezone.utc)
ended_at = started_at + timedelta(seconds=5)
run = client.runs.create(
procedure_id="FVT1",
serial_number="SN-0001",
part_number="PCB01",
outcome="PASS",
started_at=started_at,
ended_at=ended_at,
phases=[
{
"name": "phase_multi_measurements",
"outcome": "PASS",
"started_at": started_at,
"ended_at": ended_at,
"measurements": [
{
"name": "is_connected",
"measured_value": True,
"outcome": "PASS",
"validators": [
{"operator": "==", "expected_value": True, "outcome": "PASS"},
],
},
{
"name": "firmware_version",
"measured_value": "1.2.7",
"outcome": "PASS",
"validators": [
{"operator": "==", "expected_value": "1.2.7", "outcome": "PASS"},
],
},
{
"name": "input_voltage",
"units": "V",
"measured_value": 3.35,
"outcome": "PASS",
"validators": [
{"operator": ">=", "expected_value": 3.2, "outcome": "PASS"},
{"operator": "<=", "expected_value": 3.4, "outcome": "PASS"},
],
},
{
"name": "input_current",
"units": "A",
"measured_value": 1.2,
"outcome": "PASS",
"validators": [
{"operator": "<=", "expected_value": 1.5, "outcome": "PASS"},
],
},
],
}
],
)using TofuPilot;
using TofuPilot.Models.Requests;
var client = new TofuPilot();
var startedAt = DateTime.UtcNow;
var endedAt = startedAt.AddSeconds(5);
var run = await client.Runs.CreateAsync(new RunCreateRequest
{
ProcedureId = "FVT1",
SerialNumber = "SN-0001",
PartNumber = "PCB01",
Outcome = RunCreateOutcome.Pass,
StartedAt = startedAt,
EndedAt = endedAt,
Phases = new List<RunCreatePhases>
{
new RunCreatePhases
{
Name = "phase_multi_measurements",
Outcome = RunCreatePhasesOutcome.Pass,
StartedAt = startedAt,
EndedAt = endedAt,
Measurements = new List<RunCreateMeasurements>
{
new RunCreateMeasurements
{
Name = "is_connected",
MeasuredValue = (object)true,
Outcome = RunCreateMeasurementsOutcome.Pass,
Validators = (object)new List<RunCreateValidators>
{
new RunCreateValidators { Operator = "==", ExpectedValue = RunCreateExpectedValue.CreateBoolean(true), Outcome = "PASS" },
},
},
new RunCreateMeasurements
{
Name = "firmware_version",
MeasuredValue = (object)"1.2.7",
Outcome = RunCreateMeasurementsOutcome.Pass,
Validators = (object)new List<RunCreateValidators>
{
new RunCreateValidators { Operator = "==", ExpectedValue = RunCreateExpectedValue.CreateStr("1.2.7"), Outcome = "PASS" },
},
},
new RunCreateMeasurements
{
Name = "input_voltage",
Units = RunCreateUnits.CreateStr("V"),
MeasuredValue = (object)3.35,
Outcome = RunCreateMeasurementsOutcome.Pass,
Validators = (object)new List<RunCreateValidators>
{
new RunCreateValidators { Operator = ">=", ExpectedValue = RunCreateExpectedValue.CreateNumber(3.2), Outcome = "PASS" },
new RunCreateValidators { Operator = "<=", ExpectedValue = RunCreateExpectedValue.CreateNumber(3.4), Outcome = "PASS" },
},
},
new RunCreateMeasurements
{
Name = "input_current",
Units = RunCreateUnits.CreateStr("A"),
MeasuredValue = (object)1.2,
Outcome = RunCreateMeasurementsOutcome.Pass,
Validators = (object)new List<RunCreateValidators>
{
new RunCreateValidators { Operator = "<=", ExpectedValue = RunCreateExpectedValue.CreateNumber(1.5), Outcome = "PASS" },
},
},
},
}
},
});View detailed results on the run page after test completion.

Attachments
Attach files like screenshots, CSV data, or diagnostic images alongside test results.
- OpenHTF: Use
attachorattach_from_filefunctions to include files. - Python v2: Use
client.attachments.upload()then link withclient.runs.update().
import openhtf as htf
from tofupilot.openhtf import TofuPilot
def phase_file_attachment(test):
test.attach_from_file("data/temperature-map.png") # Replace with your file path
return htf.PhaseResult.CONTINUE
def main():
test = htf.Test(
phase_file_attachment,
procedure_id="FVT1", # Create the procedure first in the Dashboard
part_number="PCB01",
)
with TofuPilot(test):
test.execute(lambda: "SN-0001")
if __name__ == "__main__":
main()from tofupilot.v2 import TofuPilot
client = TofuPilot()
# Upload files
upload_id = client.attachments.upload("data/temperature-map.png")
# Link to a run
client.runs.update(id="your-run-id", attachments=[upload_id])using TofuPilot;
using TofuPilot.Models.Requests;
var client = new TofuPilot();
// Upload files
var uploadId = await client.Attachments.UploadAsync("data/temperature-map.png");
// Link to a run
await client.Runs.UpdateAsync("your-run-id", new RunUpdateRequestBody { Attachments = new List<string> { uploadId } });By default, the JSON report generated by OpenHTF is included as an attachment.
Logs
Logs help debug test execution and unusual behavior.
- OpenHTF: Use the OpenHTF logger from any phase. TofuPilot captures all log levels and makes them searchable.
- Python: Create your own logging handler to capture messages.
import openhtf as htf
from tofupilot.openhtf import TofuPilot
@htf.measures(htf.Measurement("boolean_measure").equals(True))
def phase_with_info_logger(test):
test.measurements.boolean_measure = True # You can log info, warning, error, and critical. By default, debug.
test.logger.info("Logging an information")
def main():
test = htf.Test(
phase_with_info_logger,
procedure_id="FVT1",
part_number="PCB01",
)
with TofuPilot(test):
test.execute(lambda: "SN-0001")
if __name__ == "__main__":
main()from datetime import datetime, timezone, timedelta
from tofupilot.v2 import TofuPilot
client = TofuPilot()
started_at = datetime.now(timezone.utc)
ended_at = started_at + timedelta(seconds=10)
run = client.runs.create(
procedure_id="FVT1",
serial_number="SN-0001",
part_number="PCB01",
outcome="PASS",
started_at=started_at,
ended_at=ended_at,
logs=[
{
"level": "INFO",
"timestamp": started_at.isoformat(),
"message": "Test started",
"source_file": "test_script.py",
"line_number": 42,
},
{
"level": "WARNING",
"timestamp": ended_at.isoformat(),
"message": "Voltage close to upper limit",
"source_file": "test_script.py",
"line_number": 58,
},
],
)using TofuPilot;
using TofuPilot.Models.Requests;
var client = new TofuPilot();
var startedAt = DateTime.UtcNow;
var endedAt = startedAt.AddSeconds(10);
var run = await client.Runs.CreateAsync(new RunCreateRequest
{
ProcedureId = "FVT1",
SerialNumber = "SN-0001",
PartNumber = "PCB01",
Outcome = RunCreateOutcome.Pass,
StartedAt = startedAt,
EndedAt = endedAt,
Logs = new List<RunCreateLogs>
{
new RunCreateLogs
{
Level = RunCreateLevel.Info,
Timestamp = startedAt,
Message = "Test started",
SourceFile = "TestScript.cs",
LineNumber = 42,
},
new RunCreateLogs
{
Level = RunCreateLevel.Warning,
Timestamp = endedAt,
Message = "Voltage close to upper limit",
SourceFile = "TestScript.cs",
LineNumber = 58,
},
},
});Browse & Filter Runs
You can browse and filter runs from the Dashboard or with the API client.
from tofupilot.v2 import TofuPilot
client = TofuPilot()
# List runs filtered by serial number
runs = client.runs.list(serial_numbers=["SN-0001"])
for r in runs.data:
print(f"{r.id}: {r.outcome}")
# Get a specific run by ID
run = client.runs.get(id="your-run-id")
print(f"Run {run.id}: {run.outcome}")
# Filter by procedure, outcome, date range
from datetime import datetime, timezone, timedelta
runs = client.runs.list(
procedure_ids=["FVT1"],
outcomes=["FAIL"],
started_after=datetime.now(timezone.utc) - timedelta(days=7),
)using TofuPilot;
var client = new TofuPilot();
// List runs filtered by serial number
var runs = await client.Runs.ListAsync(serialNumbers: new List<string> { "SN-0001" });
foreach (var r in runs.Data)
Console.WriteLine($"{r.Id}: {r.Outcome}");
// Get a specific run by ID
var run = await client.Runs.GetAsync("your-run-id");
Console.WriteLine($"Run {run.Id}: {run.Outcome}");
// Filter by procedure, outcome, date range
var filtered = await client.Runs.ListAsync(
procedureIds: new List<string> { "FVT1" },
outcomes: new List<string> { "FAIL" }
);Download Attachments
You can download attachments from a run.
from tofupilot.v2 import TofuPilot
client = TofuPilot()
run = client.runs.get(id="your-run-id")
for a in run.attachments:
client.attachments.download(a)
print(f"Downloaded {a.name}")using TofuPilot;
var client = new TofuPilot();
var run = await client.Runs.GetAsync("your-run-id");
foreach (var a in run.Attachments)
{
await client.Attachments.DownloadAsync(a.DownloadUrl, a.Name);
Console.WriteLine($"Downloaded {a.Name}");
}Update Runs
You can update runs from the Dashboard or with the API client. Currently, updates support linking file attachments to existing runs.
from tofupilot.v2 import TofuPilot
client = TofuPilot()
# Upload a file and link it to a run
upload_id = client.attachments.upload("data/report.pdf")
client.runs.update(id="your-run-id", attachments=[upload_id])using TofuPilot;
using TofuPilot.Models.Requests;
var client = new TofuPilot();
// Upload a file and link it to a run
var uploadId = await client.Attachments.UploadAsync("data/report.pdf");
await client.Runs.UpdateAsync("your-run-id", new RunUpdateRequestBody { Attachments = new List<string> { uploadId } });Delete Runs
You can delete runs from the Dashboard or with the API client. Deleting a run removes all associated phases, measurements, and attachments.
from tofupilot.v2 import TofuPilot
client = TofuPilot()
client.runs.delete(ids=["your-run-id"])using TofuPilot;
var client = new TofuPilot();
await client.Runs.DeleteAsync(new List<string> { "your-run-id" });Upload Runs Offline
Run tests offline and upload results when connectivity is available.
- OpenHTF: Save results as JSON files, then use
create_run_from_openhtf_report()to upload. See OpenHTF output callbacks documentation. - Python: Use
client.runs.create()withstarted_atto preserve test time. Without it, upload time is used.
from openhtf import PhaseResult, Test
from openhtf.output.callbacks import json_factory
from tofupilot import TofuPilotClient
def power_on_test(test):
return PhaseResult.CONTINUE
# Run test and save results to JSON
def execute_test(file_path):
test = Test(
power_on_test,
part_number="PCB01",
procedure_id="FVT1",
)
# Save results as JSON
test.add_output_callbacks(json_factory.OutputToJSON(file_path, indent=2))
test.execute(lambda: "SN-0001")
def main():
client = TofuPilotClient()
# Test results file path
file_path = "./test_result.json"
execute_test(file_path)
# Upload OpenHTF JSON report to TofuPilot
client.create_run_from_openhtf_report(file_path)
if __name__ == "__main__":
main()from datetime import datetime, timezone, timedelta
from tofupilot.v2 import TofuPilot
client = TofuPilot()
# Set timestamps to when the test actually ran (e.g. yesterday)
started_at = datetime.now(timezone.utc) - timedelta(days=1)
ended_at = started_at + timedelta(minutes=2)
run = client.runs.create(
procedure_id="FVT1",
serial_number="SN-0001",
part_number="PCB01",
outcome="PASS",
started_at=started_at,
ended_at=ended_at,
)using TofuPilot;
using TofuPilot.Models.Requests;
var client = new TofuPilot();
// Set timestamps to when the test actually ran (e.g. yesterday)
var startedAt = DateTime.UtcNow.AddDays(-1);
var endedAt = startedAt.AddMinutes(2);
var run = await client.Runs.CreateAsync(new RunCreateRequest
{
ProcedureId = "FVT1",
SerialNumber = "SN-0001",
PartNumber = "PCB01",
Outcome = RunCreateOutcome.Pass,
StartedAt = startedAt,
EndedAt = endedAt,
});How is this guide?