Skip to content

Data2025-09-30

Data lineage that survives an audit

A lineage diagram shows how the pipeline works today. An auditor asks how it worked on 14 March. Those are different questions, and only one of them is answered by a graph.

Author
Marcus Adeyemi
Published
30 SEP 2025
Read
5 MIN
Ref
E0CC9F

Every data platform we inherit has a lineage diagram. It is usually beautiful, usually in a catalogue tool, and usually useless in an audit — because it describes the pipeline as it is configured right now, and an auditor is asking about a number that was reported nineteen months ago.

The distinction is worth being precise about. A catalogue answers how does this column get populated. An audit asks prove that this figure, on this date, was produced by the logic you documented, and show me who could have changed it. The first is a graph. The second is an append-only record of events.

What is actually asked for

Cambridge Mutual's regulatory reporting review ran for eleven weeks. Stripped of formality, the questions were these:

The questionThe artefact that answers itThe artefact that does not
Where did this figure come from?Column-level lineage for the run that produced itA table-level diagram
Was that the logic in force at the time?Immutable job version pinned to the runThe current repository HEAD
What data went in?Input dataset versions and row counts for that runA source system name
Who could have altered it?Grants effective on that date, plus change historyToday's access control list
Did anything fail and get re-run?Run history including failed and retried attemptsA green dashboard

Four of those five demand a point-in-time answer. If your lineage is derived by parsing the code in your repository today, you cannot answer any of them, and you will spend three weeks reconstructing the past from Git history and Slack.

Emit lineage as run events, not as documentation

The design that survives contact with an auditor treats lineage as telemetry: every job run emits an event describing its inputs, its outputs, the exact code version, and the column-level mapping in force for that run. Events are appended, never edited. The graph in your catalogue becomes a view over the events rather than a separate artefact that drifts.

The OpenLineage schema is a reasonable default here, and being an open standard rather than a vendor format matters more than it sounds — your catalogue will be replaced within the retention period.

{
  "eventType": "COMPLETE",
  "eventTime": "2025-03-14T02:41:07.412Z",
  "run": {
    "runId": "0192d1c3-8f4a-7c21-9a55-2b6e5f0c7d31",
    "facets": {
      "parent": { "job": { "name": "reg_daily_close", "namespace": "prod" } },
      "sourceCodeVersion": {
        "type": "git",
        "url": "git@internal:reporting/close.git",
        "version": "e7b41c0d9a2f5b83c1e4d0a7f2913b6c8d5e4a91"
      }
    }
  },
  "job": { "namespace": "prod", "name": "reg_daily_close.exposure_agg" },
  "inputs": [
    {
      "namespace": "warehouse",
      "name": "risk.positions_eod",
      "facets": {
        "dataQualityMetrics": { "rowCount": 4182233, "nullCount": 0 },
        "version": { "datasetVersion": "2025-03-14T02:15:00Z#snap_7741" }
      }
    }
  ],
  "outputs": [
    {
      "namespace": "warehouse",
      "name": "reg.exposure_by_counterparty",
      "facets": {
        "columnLineage": {
          "fields": {
            "gross_exposure": {
              "inputFields": [
                { "namespace": "warehouse", "name": "risk.positions_eod", "field": "notional" },
                { "namespace": "warehouse", "name": "risk.positions_eod", "field": "fx_rate" }
              ],
              "transformationType": "AGGREGATION",
              "transformationDescription": "sum(notional * fx_rate) group by counterparty_id"
            }
          }
        }
      }
    }
  ]
}

That single object answers four of the five questions in the table above, in a form you can hand over without a human interpreting it. The fifth — who could have altered it — comes from snapshotting effective grants on the same cadence, which is a nightly query and a versioned table, not a governance programme.

Three details that decide whether it holds up

Column level or nothing. Table-level lineage tells an auditor that reg.exposure_by_counterparty derives from risk.positions_eod. They already assumed that. The finding lands on a specific column, so the evidence has to be about a specific column.

Capture failed runs. The most common real finding is not wrong logic, it is a partial load that was silently re-run with different inputs. If your lineage only records successes, the interesting event is the one you deleted.

Retain the events for as long as you retain the reports. Seven years, typically. That means the lineage store is a warehouse table with a partition key, not a feature of a SaaS catalogue on an annual contract. We keep the raw events in object storage as the system of record and let the catalogue index them.

The test

Pick a figure from a report filed at least a year ago. Give an engineer who did not build the pipeline two hours to produce the code version, the input dataset versions, the row counts, and the access list in force on that date. If they can, your lineage will survive an audit. If the answer involves reading a diagram and inferring, it will not — and you will find that out under a deadline set by someone else.

Written by

Marcus Adeyemi

Contributor

Share

Related notes

All notes
  • 01Engineering

    Reading a COBOL codebase you did not write

    The programs are the last thing to read. Start with the job control and the copybooks, because that is where forty years of business rules were actually written down.

    COBOL · Legacy5 MIN
  • 02Reliability

    Cutover rehearsals, and why we do six

    A 412-step runbook took 19 hours on its first rehearsal against a 9-hour window. By the sixth it took 6 hours 40. The rehearsals were not practice — each one was designed to fail differently.

    Cutover · Runbooks5 MIN
  • 03Engineering

    The integration nobody documented

    Every legacy platform has interfaces that appear in no diagram and no register. Here is how we find them before cutover finds them for us — and why the interface diagram is the last place to look.

    Integration · Discovery4 MIN