UniteLabs
How-to

Read lineage events

Read lineage history through the API and subscribe to new events.

Use the lineage API to send run history to a LIMS, audit store, or data pipeline. You can read completed events, catch up across workflows, or subscribe while runs are active.

For the data model and Platform views, see Data lineage. To record events from a workflow, see Emit lineage events.

Choose how to read lineage

NeedUse
Inspect one run as a personThe Lineage tab in the Platform run view
Retrieve one run's historyGET /v1/runs/:runId/lineage/events
Read events across workflowsGET /v1/lineage/events
Receive new eventsGET /v1/lineage/events/stream
Interpret labware IDs used by one runGET /v1/runs/:runId/lineage/workspace

Before you start

You need:

  • UniteLabs API ≥ 0.17.0
  • A run executed with UniteLabs SDK ≥ 0.15.0
  • A bearer token that can access the run data

Set these shell variables before using the examples:

export API="<platform-api-base-url>"
export TOKEN="<bearer-token>"
export RUN_ID="<run-uuid>"

Leave /v1 off $API. Each example adds it.

Read one run's lineage

Request the run's events:

curl -H "Authorization: Bearer $TOKEN" \
  "$API/v1/runs/$RUN_ID/lineage/events?_take=100"

The API returns events ordered by their global sequence value:

{
  "data": [
    {
      "id": "b9a019f9-2b68-47f3-8599-a380de65c60e",
      "sequence": "1024",
      "runId": "c4b374dd-469d-4453-8678-ddd8a0081738",
      "workflowId": "9d8e7f6a-5b4c-4d3e-2f1a-0b9c8d7e6f5a",
      "type": "lineage",
      "operation": "aspirate",
      "actor": "liquid_handler",
      "version": "1.0.0",
      "timestamp": "2026-07-13T09:41:22.556Z",
      "inputs": { "entity_id": "source_plate:A1" },
      "outputs": { "entity_id": "channel:0" },
      "extras": { "volume_ul": 300, "channel": 0 },
      "phaseId": "3c6f1bd8-6a5c-45fe-93ba-af5601017630",
      "stepId": "a1f0c2e4-3b87-4162-982c-7e8d7f3db401",
      "createdAt": "2026-07-13T09:41:22.721Z"
    }
  ],
  "pagination": {
    "nextCursor": "eyJpZCI6IjEwMjQifQ==",
    "hasMore": true,
    "totalCount": 812
  }
}

Event response fields

FieldMeaning
idEvent UUID generated by the SDK and used for deduplication.
sequenceGlobal ordering value returned as a string.
runIdRun that owns the event.
workflowIdWorkflow that owns the run.
typeEvent category.
operationAction that took place.
actorDevice or service that performed the action.
versionEvent payload schema version.
timestampTime at which the SDK emitted the event.
inputs, outputsEntities connected by the action.
extrasOptional action-specific metadata.
phaseId, stepIdOptional location in the run timeline.
createdAtTime at which the API stored the event.

An event emitted directly at workflow level has no phaseId. stepId is present when the event came from a step.

Read workspace context

Events use compact entity IDs. The run workspace supplies names, roles, types, and barcodes for the labware referenced by those IDs:

curl -H "Authorization: Bearer $TOKEN" \
  "$API/v1/runs/$RUN_ID/lineage/workspace"
{
  "runId": "c4b374dd-469d-4453-8678-ddd8a0081738",
  "snapshot": {
    "labware_context": {
      "source_plate": {
        "name": "Source plate",
        "type": "Axygen_96_DW_2mL",
        "role": "wellplate",
        "barcode": "SOURCE-001"
      },
      "target_plate": {
        "name": "Target plate",
        "type": "Eppendorf_96_PCR",
        "role": "wellplate",
        "barcode": "TARGET-001"
      }
    }
  },
  "labwareContext": {
    "source_plate": {
      "name": "Source plate",
      "type": "Axygen_96_DW_2mL",
      "role": "wellplate",
      "barcode": "SOURCE-001"
    },
    "target_plate": {
      "name": "Target plate",
      "type": "Eppendorf_96_PCR",
      "role": "wellplate",
      "barcode": "TARGET-001"
    }
  }
}

snapshot is the opaque value sent by the SDK. labwareContext is the API's extracted map for consumers. It is empty when the SDK did not report workspace context for the run.

Page through results

A response with hasMore: true includes a nextCursor. Pass that value back through _cursor:

export NEXT_CURSOR="eyJpZCI6IjEwMjQifQ=="

curl -H "Authorization: Bearer $TOKEN" \
  "$API/v1/runs/$RUN_ID/lineage/events?_take=100&_cursor=$NEXT_CURSOR"

Treat the cursor as opaque. Continue with each returned nextCursor until hasMore is false.

Filter events

Filter one run by event category:

curl -H "Authorization: Bearer $TOKEN" \
  "$API/v1/runs/$RUN_ID/lineage/events?type=identification"

The global list accepts both runId and type:

curl -H "Authorization: Bearer $TOKEN" \
  "$API/v1/lineage/events?runId=$RUN_ID&type=lineage"

The standard SDK categories are lineage, identification, measurement, and audit. The API also stores custom strings.

Read across workflows

Use the global endpoint for catch-up jobs and backfills:

curl -H "Authorization: Bearer $TOKEN" \
  "$API/v1/lineage/events?_take=100"

The endpoint orders all visible events by sequence. Follow its cursors until the current catch-up read is complete, then use the highest sequence to open the live stream.

Subscribe to new events

The stream uses Server-Sent Events (SSE). Each frame contains one event as JSON and uses its sequence as the SSE event ID.

Start with one run:

curl -N -H "Authorization: Bearer $TOKEN" \
  -H "Accept: text/event-stream" \
  "$API/v1/lineage/events/stream?runId=$RUN_ID"

Filter the stream by category when needed:

curl -N -H "Authorization: Bearer $TOKEN" \
  -H "Accept: text/event-stream" \
  "$API/v1/lineage/events/stream?runId=$RUN_ID&type=lineage"

Without a start position, the stream begins at the current highest sequence and sends events ingested after the connection opens.

Resume after a disconnect

Keep the highest event ID you received from the stream. To resume after it, send it in the standard Last-Event-ID header:

curl -N -H "Authorization: Bearer $TOKEN" \
  -H "Accept: text/event-stream" \
  -H "Last-Event-ID: 1024" \
  "$API/v1/lineage/events/stream?runId=$RUN_ID"

Or pass it as the since query parameter:

curl -N -H "Authorization: Bearer $TOKEN" \
  -H "Accept: text/event-stream" \
  "$API/v1/lineage/events/stream?runId=$RUN_ID&since=1024"

If a request carries both, Last-Event-ID wins.

A browser EventSource remembers the last event ID when it reconnects on its own. It can't set a bearer header, though, so browser apps need an authenticated same-origin proxy, like the one the UniteLabs Platform uses.

Combine catch-up and live streaming

Use both endpoints when an integration must process history and continue with new events:

  1. Page through GET /v1/lineage/events until hasMore is false.
  2. Record the highest sequence in the returned events.
  3. Open GET /v1/lineage/events/stream?since=<sequence>.
  4. Store each SSE event ID after processing its event.
  5. Send the stored value as Last-Event-ID after a disconnect.

The stream returns events with a sequence strictly greater than the supplied position.

Endpoint reference

RouteFilters and paginationPurpose
GET /v1/runs/:runId/lineage/events_cursor, _take, typeRead one run's events.
GET /v1/runs/:runId/lineage/workspaceNoneRead one run's workspace.
GET /v1/lineage/events_cursor, _take, runId, typeRead events across workflows.
GET /v1/lineage/events/streamsince, runId, typeSubscribe to new events.

List responses return 400 for invalid cursors or query values and 401 for missing or invalid authentication. Per-run routes return 404 when the run does not exist.

Last updated