Skip to content

Backfilling history

Your automation platform already knows about the runs that happened before you adopted LumaTrack. Import them so the trend lines, cumulative value, and reliability charts start with the truth instead of a cliff.

Two ways in:

  • In the app: Automations, Import runs. Paste or upload, see the results per row.
  • Over the API: POST /api/v1/runs/backfill.

Either way, imported runs carry a permanent backfilled provenance flag, so a chart reader can always tell evidence that arrived live from evidence that arrived in bulk.

Formats

CSV (a template is downloadable in the app at /automations/import/template.csv):

automation,status,executed_at,duration_seconds,source,external_id,units,manual
os-patching,success,2026-05-14T02:30:00Z,142,ansible,tower-job-99412,240
os-patching,failure,2026-05-15T02:30:00Z,98,ansible,tower-job-99511,238

units is the records/items the execution processed; blank means 1. manual marks a real manual execution (opportunity evidence, never savings); true/false, blank follows the automation's status (measuring ⇒ manual). An optional baseline_minutes_override column carries the total manual minutes a historical execution replaced, where your export knows it per row (asserting a baseline per run); blank uses the automation's standard baseline.

Or JSONL, one object per line, same fields plus optional metadata:

{"automation": "os-patching", "status": "success", "executed_at": "2026-05-14T02:30:00Z", "external_id": "tower-job-99412", "metadata": {"hosts": 240}}

automation (slug) and executed_at (ISO 8601; naive timestamps are treated as UTC) are required. A backfill row without a timestamp is meaningless, so unlike live ingest there is no "default to now".

Over the API

curl -s -X POST "$LUMATRACK_URL/api/v1/runs/backfill" \
  -H "Authorization: Bearer $LUMATRACK_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"format\": \"csv\", \"data\": $(jq -Rs . < history.csv)}"

The response is an honest accounting:

{
  "processed": 1193,
  "created": 1180,
  "deduplicated": 12,
  "held": 0,
  "errors": [
    {"line": 481, "error": "March 2026 is closed; its numbers are frozen."}
  ]
}

processed is the total number of input rows: created (which includes the held subset) plus deduplicated plus the count of errors. It lets you reconcile the response against the file you sent in one number.

The rules, in the order each row meets them

  1. The automation slug must exist in your organization.
  2. executed_at must be present, parse as ISO 8601, and not be in the future.
  3. Rows older than your plan's retention window are refused outright (they would be pruned immediately anyway).
  4. Rows in frozen months are refused, exactly like live ingest. Closed numbers never move, not even for bulk loads.
  5. external_id replays dedupe: re-importing the same file is safe and reports deduplicated counts instead of creating duplicates.
  6. Monthly plan caps apply per target month: rows over a month's cap are stored as held, never dropped.
  7. A bad row never aborts the rest of the file. Every rejected row comes back in errors with its line number and the reason.

Rules 5 and 7 together make the sane strategy simple: export everything, import it all, fix the rows listed in errors, and import the whole file again.