Migrating from Legacy Systems

Import Historical Test Data via the API

Learn how to backfill years of legacy test results with the runs API, keeping original test dates so trends and analytics stay correct.

JJulien Buteau
intermediate7 min de lecture11 août 2026

Switching test data platforms should not mean starting the fleet history from zero. Trend charts, per-serial degradation curves, and capability indices are only as good as the history behind them. This guide shows how to backfill legacy results through the runs API with their original test dates intact.

The key mechanism: a run's started_at is the timestamp analytics use, and the API accepts past dates. Upload date is stored separately, so a run tested in 2024 and imported today lands in 2024 on every chart.

Prerequisites

  • An API key (Settings, API keys)
  • The procedure created in TofuPilot, and its procedure id
  • Legacy results exported to something parseable (CSV, database dump, report files)

Step 1: Map the legacy fields

The minimum viable mapping:

Legacy fieldRuns API field
Test date/timestarted_at, ended_at (ISO 8601)
Overall verdictoutcome (PASS, FAIL, ERROR)
Unit serial numberserial_number
Part numberpart_number
Test stepsphases[] with name, outcome, timestamps
Measured values and limitsmeasurements[] per phase, with validators

Keep phase and measurement names identical to what the live bench will record going forward. Analytics group by name, so Contact Drop A2 in the import and contact_drop_a2 from the new bench would land in two different series.

Step 2: Create runs with original timestamps

import_legacy.py
from tofupilot import TofuPilotclient = TofuPilot(api_key="...")for row in legacy_rows:    client.runs.create(        procedure_id="550e8400-e29b-41d4-a716-446655440000",        serial_number=row.serial,        part_number=row.part_number,        outcome=row.verdict,                      # "PASS" / "FAIL"        started_at=row.tested_at,                 # original date, ISO 8601        ended_at=row.finished_at,        phases=[{            "name": "Contact Voltage Drop",            "outcome": row.phase_verdict,            "started_at": row.tested_at,            "ended_at": row.finished_at,            "measurements": [{                "name": "Contact Drop A2",                "outcome": row.a2_verdict,                "measured_value": row.a2_mv,                "units": "mV",                "validators": [{"operator": "<=", "expected_value": 100,                                "outcome": row.a2_verdict}],            }],        }],    )

Units are created automatically from serial numbers, so a serial with five historical visits ends up with a five-run history without any separate unit setup.

Step 3: Verify in analytics

Set the date range in run analytics or measurement control to cover the imported period and check three things:

  1. Run counts per month match the legacy system
  2. Per-serial history is complete: pick a unit with known repeat visits and confirm every visit is there
  3. Limits came through: measurements show their validators, so Cpk and pass rates compute against the right limits

Step 4: Cut over the live bench

Once history is verified, point the live bench at the same procedure. New runs continue the same series: the first live run on an imported serial extends its existing trend rather than starting a new one, which is the entire point of importing with real timestamps.

Two details worth knowing:

  • Imported runs record the API key's user as creator, and the upload date as created_at. Both are visible on the run detail page; neither affects analytics, which use started_at.
  • Station attribution requires uploading with a station key. For historical data this rarely matters, but record the original site or bench as a measurement or run metadata if you need to filter by it later.

Plus de guides

Mettez ce guide en pratique