Compliance & Traceability

Track Serial Numbers and Sub-Assemblies

Record parent/child serial number relationships in your test data to trace sub-assemblies across boards, modules, and final products.

JJulien Buteau
intermediate7 min readMarch 14, 2026

Most hardware products contain sub-assemblies, each with its own serial number. The sub_units field lets you record which components went into which parent unit, giving you full BOM traceability from a single test script.

Why Sub-Assembly Tracking Matters

A finished product might contain a power supply board, a compute module, and a sensor array. Each has its own serial number and its own test history. When a field failure points to a specific component, you need to know which parent units contain that component.

This is standard practice for IPC-1782 traceability and required in regulated industries. Parent and child serial numbers link automatically when you include them in your test runs.

This is also the hardest thing to add retroactively. Reconstructing which board went into which assembly after the fact means reading build records by hand, if they exist at all. Model it from the first build even if you do not query it yet.

Recording Sub-Unit Serial Numbers

sub_units is a field on htf.Test(...), alongside procedure_id and part_number. It takes a list of {"serial_number": "..."} entries.

test_assembly.py
31 lines
# Record sub-assembly serial numbers during final assembly testimport openhtf as htffrom tofupilot.openhtf import upload@htf.measures(    htf.Measurement("system_power_on").in_range(minimum=1, maximum=1),    htf.Measurement("communication_check").in_range(minimum=1, maximum=1),)def system_integration_test(test):    test.measurements.system_power_on = 1    test.measurements.communication_check = 1def main():    test = htf.Test(        system_integration_test,        procedure_id="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",  # procedure UUID from the dashboard        part_number="ASSY-100",        sub_units=[            {"serial_number": "PSU-2026-0441"},            {"serial_number": "CPU-2026-1187"},            {"serial_number": "SNS-2026-0893"},        ],    )    test.add_output_callbacks(upload())    test.execute(lambda: "ASSY-2026-0072")if __name__ == "__main__":    main()

After this test uploads, the unit page for ASSY-2026-0072 shows three linked sub-assemblies. Click any sub-unit serial to see its own test history.

Scanning Sub-Unit Serials During Assembly

In practice, operators scan component serial numbers as they install them. Because sub_units is a constructor argument, collect the scans before building the test:

test_scan_subunits.py
34 lines
# Operator scans sub-assembly serials during assemblyimport openhtf as htffrom openhtf.util import unitsfrom tofupilot.openhtf import upload@htf.measures(    htf.Measurement("power_rail_5v").in_range(minimum=4.8, maximum=5.2).with_units(units.VOLT),    htf.Measurement("power_rail_3v3").in_range(minimum=3.1, maximum=3.5).with_units(units.VOLT),)def power_validation(test):    test.measurements.power_rail_5v = 5.02    test.measurements.power_rail_3v3 = 3.31def main():    psu = input("Scan PSU serial: ")    cpu = input("Scan CPU serial: ")    test = htf.Test(        power_validation,        procedure_id="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",        part_number="ASSY-100",        sub_units=[            {"serial_number": psu},            {"serial_number": cpu},        ],    )    test.add_output_callbacks(upload())    test.execute(lambda: input("Scan assembly serial: "))if __name__ == "__main__":    main()

Scanning before the test starts also means a mis-scan is caught before you spend cycle time on measurements. Validate the serial format at the prompt rather than discovering a typo in the traceability record months later.

For a station with a real operator interface, replace input() with openhtf.plugs.user_input so the prompts appear on the operator screen instead of a console.

Multi-Level Assemblies

For products with nested assemblies (a module inside a board inside a chassis), test each level separately with its own sub-units. The hierarchy builds automatically.

test_nested_assembly.py
47 lines
# Test a module, then test the board that contains itimport openhtf as htffrom tofupilot.openhtf import uploadMODULE_PROCEDURE_ID = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"BOARD_PROCEDURE_ID = "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy"@htf.measures(    htf.Measurement("module_self_test").in_range(minimum=1, maximum=1),)def module_test(test):    test.measurements.module_self_test = 1@htf.measures(    htf.Measurement("board_communication").in_range(minimum=1, maximum=1),)def board_test(test):    test.measurements.board_communication = 1def main():    # First: test the module by itself    module_serial = "MOD-2026-0551"    t1 = htf.Test(        module_test,        procedure_id=MODULE_PROCEDURE_ID,        part_number="MOD-100",    )    t1.add_output_callbacks(upload())    t1.execute(lambda: module_serial)    # Second: test the board, declaring the module as a sub-unit    board_serial = "BRD-2026-0112"    t2 = htf.Test(        board_test,        procedure_id=BOARD_PROCEDURE_ID,        part_number="BRD-100",        sub_units=[{"serial_number": module_serial}],    )    t2.add_output_callbacks(upload())    t2.execute(lambda: board_serial)if __name__ == "__main__":    main()

Searching for the board serial shows the module as a sub-unit. Searching for the module serial shows its own test results plus the parent board it was installed in.

In production these two tests are usually separate scripts on separate stations, run hours apart. They are together here only to show both halves of the link.

BOM Traceability Use Cases

Once sub-assembly links exist, you can answer questions that matter in production:

  • Field failure investigation. A sensor module fails in the field. Search its serial number to find every parent unit that contains the same module type from the same production batch.
  • Component recall. A supplier flags a batch of capacitors. If your test phases record component lot numbers as measurements, you can trace which assemblies used them.
  • Yield by component. First pass yield broken down by sub-assembly batch shows whether a specific component lot is driving failures.

The recall case is the one that justifies the whole exercise. Without the links you scrap or recall everything built in a date range; with them you scrap only the units that actually contain the suspect lot.

Open any unit's page to see its sub-assembly tree:

  • Direct child components with their serial numbers
  • Each child's own test history (click through to view)
  • The parent assembly, if this unit is itself a sub-component
  • All test runs for the unit, across every production stage

This gives quality engineers and auditors a single place to trace any component through the entire product hierarchy.

More Guides

Put this guide into practice