A limit changed in March, and the question "which serial numbers ran the old limit" is answerable only if every run records the exact test version that produced it. On most LabVIEW stations it doesn't: the record has a date, a serial number and a result, and the version lives in whoever remembers which build was copied when. This guide shows how to stamp the version on every run from a LabVIEW station today, and how a Python station under TofuPilot does it by default.
Why It Matters
The question arrives in one of three forms, and each has a deadline attached.
| Situation | The question | What you need on every run |
|---|---|---|
| A customer return | Was this unit tested with the limit that was in force, or the one before? | The test version and the limit values |
| An audit | Show the test version each shipped unit was tested with | The test version, per serial number, exportable |
| A limit wrong for a month | Which units passed under the wrong value? | The test version and the measured value |
The third one is the expensive one. Someone types 3.15 V instead of 3.25 V on February 10th and the mistake is found on March 12th.
If every run carries its version, the answer is a filter: the units that ran that build and measured between 3.15 V and 3.25 V, a rescreen of a few dozen units. If it doesn't, the answer is every unit shipped in the window, plus a week of reconstruction from file dates and emails.
In medical, aerospace and automotive the customer expects to be shown which test method each unit went through. The expectation is the same in any plant that has had one return escalate. Recording the version costs nothing per run and pays back the first time the question is asked.
What "Test Version" Has to Mean
The test version is the built artifact, not the source commit.
A rebuild from the same commit on a different machine can behave differently. A different LabVIEW patch level, a different driver, a subVI resolved from a different path, a 32-bit build where the last one was 64-bit. Two executables built from one commit are two versions, and only a number that increments with every build tells them apart.
So the rule is: one build, one number, stamped into the artifact and copied into every record it produces. Keep the source commit too, but as a property of the build, not as its identity. On a Python station this is exactly what a deployment is, and the commit SHA is one of its fields.
On a LabVIEW Station Today
Three steps, and none of them needs a migration.
Embed a build number in the executable. Fill the version information in the build specification and let it increment on every build, and have the build write the same number to a version.txt next to the executable. At startup the test reads it once and keeps it in a global.
Write it into every record. A test_build property on the TDMS file, or a column in the CSV, written on every run with the serial number. The record is now self-describing without the file date.
Post it to TofuPilot with the run. The REST API v2 takes a procedure_version field on each run, and the HTTP Client VIs can send this JSON with an Authorization: Bearer <api key> header.
run.json29 lines
{ "outcome": "PASS", "procedure_id": "9f1c6a2e-4b0d-4b8e-9d3a-2f0c5e7a1b44", "procedure_version": "fct-b117", "serial_number": "SN-004512", "part_number": "CTRL-PCBA-A", "started_at": "2026-09-23T08:14:02Z", "ended_at": "2026-09-23T08:15:41Z", "phases": [ { "name": "Power Rails", "outcome": "PASS", "started_at": "2026-09-23T08:14:05Z", "ended_at": "2026-09-23T08:14:20Z", "measurements": [ { "name": "rail_3v3", "outcome": "PASS", "measured_value": 3.31, "units": "V", "validators": [ { "operator": ">=", "expected_value": 3.2, "outcome": "PASS" }, { "operator": "<=", "expected_value": 3.4, "outcome": "PASS" } ] } ] } ]}Two things now hold on every run in TofuPilot. procedure_version carries the build number, so the run list filters on it. And the validators travel with the measurement, so the limit that was in force is on the run itself, not in a spreadsheet that says which limits build 117 had.
The station stays LabVIEW. The traceability starts before any migration, and the same station's rows appear next to the Python stations' rows on the same procedure page.
On a Python Station With TofuPilot
The version is part of the station's source, and the artifact is created for you.
# The version travels with the procedure and is bumped in the same commit as the limit.name: Controller PCBA FCTversion: 2.5.0A Git push on the connected repository creates a deployment, an immutable build of the procedure at that commit with an id like dep_9c04e8. tofupilot deploy --prod from a linked directory does the same from the command line. Stations fetch it and run it.
# On the development machine: build a production deployment from the linked directory.tofupilot deploy --prod# On each station: fetch the pinned deployment, then run with upload.tofupilot pulltofupilot run ./procedure.yaml --uploadEvery run uploaded from a pulled deployment carries its deployment_id. "Which units ran the old limit" becomes a filter on the run list by deployment, and tofupilot deployments ls --procedure-ids <id> --environments production lists every build that ever went to production, in order.
Rollback is a re-pin, not a rebuild. Instant rollback points every production station back at a previous deployment, and the runs already uploaded keep the id of the deployment that produced them, so the record of the bad month stays intact after the fix. The deploy and pull flow is in how to deploy Python test scripts to production stations, and the repository side is in how to version control hardware tests with Git and TofuPilot.
The DUT has a version too. How to track firmware versions in production records the firmware the same way, so a run carries both the test that judged the unit and the code that was running on it.
A Worked Example
The 3V3 rail lower limit on a controller PCBA, over one quarter.
| Date | Deployment | rail_3v3 lower limit | What it means for units |
|---|---|---|---|
| 2026-01-06 | dep_41b2c9 | 3.25 V | Baseline, 2,100 units |
| 2026-02-10 | dep_7f3a21 | 3.15 V, a typo in the pull request | 1,240 units ran this build |
| 2026-03-12 | dep_9c04e8 | 3.25 V, corrected | Correct again from this run on |
The question on March 12th is which units passed under 3.15 V with a value that 3.25 V would have rejected. Filter the run list on dep_7f3a21, look at rail_3v3 on those runs, and the answer is 38 serial numbers out of 1,240.
Those 38 get rescreened. The other 1,202 are shown, per serial number, to have measured above the correct limit.
Without the deployment on each run, the same question is "every unit shipped between February 10th and March 12th", and the rescreen is 1,240 units. The difference is the cost of one column.
The Five-Minute Check Before Every Release
Five questions, asked with the build in hand and before it reaches a station.
- Does the version differ from the last one?
version.txtshows b117 where the floor has b116, orprocedure.yamlmoved from 2.4.0 to 2.5.0 in the same commit as the change. - Does a golden unit run on the new build show that version on its run page in TofuPilot?
- Is the change one readable line somewhere, a CHANGELOG entry or a merged pull request, that names the limit and the reason?
- Is the way back known? The previous executable kept next to the new one, or the previous deployment id noted for a rollback.
- Is the list of stations that will receive it written down, with the LabVIEW version each one runs? Which LabVIEW version runs on each station has the inventory.
The last question is the one that stalls releases on LabVIEW stations, because the build has to match the Run-Time Engine on each of them. On a Python station the list is the set of stations pinned to the procedure, and pulling the deployment is the release. What the review of that change looks like, and why it's hard to get on a VI, is in code review for LabVIEW tests: why it fails, and the file-level reason behind all of it is in why LabVIEW version control is so painful.
Start With One Station
Pick the station whose limits changed most in the last year, rebuild it in Python with the TofuPilot Framework with the same limits, and run it next to the LabVIEW version on the same units for two weeks. Every run from the Python side will carry its deployment from the first day.
# Install the CLI, then run the station locally (no account needed) or with upload.curl -fsSL https://www.tofupilot.app/install | shtofupilot run ./procedure.yamltofupilot run ./procedure.yaml --uploadThe rebuild is in how to migrate from LabVIEW to Python for manufacturing tests with TofuPilot, and the framework is at tofupilot.com/products/framework. The Lab tier is free, and tofupilot run works without an account.
