Skip to content

Lineage graph

The lineage graph answers questions storage alone can’t: which dataset produced this model? what shipped in 1.4.0? if we delete this checkpoint, what breaks?

Each repository has one graph, assembled from two sources:

Source Created by Deletable
Inferred edges CAVS, from your indexed tags: a release contains the artifacts reachable at it no — they follow the releases
Declared edges you, via the API yes
Terminal window
export API=https://cavsnode.com/api/v1
export H="Authorization: Bearer $CAVS_TOKEN"
curl -sS "$API/repositories/$REPO/graph" -H "$H"
Response shape
{
"nodes": [
{
"id": "artifact:9f2c8b0e…4f6b",
"type": "model",
"key": "9f2c8b0e…4f6b",
"label": "resnet50",
"size": 102340567
},
{ "id": "release:v1.4.0", "type": "release", "key": "v1.4.0", "label": "v1.4.0" }
],
"edges": [
{ "from": "release:v1.4.0", "to": "artifact:9f2c…", "relation": "contains", "inferred": true },
{ "id": "3b1f…", "from": "artifact:aa11…", "to": "artifact:9f2c…", "relation": "trained-with", "inferred": false }
]
}

Node ids are <type>:<key>:

Node type Key Label
artifact the object’s oid its registry name, or a shortened oid if the registry doesn’t know it
release the tag the tag

Artifact nodes are enriched from the artifact registry, so their type is one of model, dataset, media, archive, other — which is what colours them in the dashboard’s Graph tab.

Only artifact and release are valid node types; from_key, to_key and relation are required. The relation is a free-form string — pick a vocabulary and stick to it.

Terminal window
curl -sS -X POST "$API/repositories/$REPO/graph/edges" \
-H "$H" -H "Content-Type: application/json" \
-d '{
"from_type": "artifact",
"from_key": "aa11bb22…",
"to_type": "artifact",
"to_key": "9f2c8b0e…",
"relation": "trained-with"
}'

Requires repository.update. Returns 201 with the created edge, whose id you need in order to delete it:

Terminal window
curl -sS -X DELETE "$API/repositories/$REPO/graph/edges/$EDGE_ID" -H "$H"

Inferred edges have no id and can’t be deleted — remove the tag instead.

There’s no enforced list, which means consistency is on you. A set that covers most real cases:

Relation From → To Read as
trained-with model → dataset “this model was trained with that dataset”
derived-from artifact → artifact “this was produced from that”
evaluated-on model → dataset “these metrics come from that set”
fine-tuned-from model → model “this started from that checkpoint”
packaged-in artifact → release “this ships in that release”
replaces artifact → artifact “this supersedes that”
contains release → artifact inferred by CAVS; don’t declare it by hand

Write it down in your team’s docs. A graph with trained-with, trained_with and trainedWith in it is a graph nobody trusts.

The natural place is right after an upload, when you still hold every oid:

Python
from cavs import CAVS, sha256_file
client = CAVS.from_env()
PROJECT = "vision-models"
client.artifacts.upload(path="./data/train-v3.parquet", project=PROJECT,
name="train-v3", kind="dataset", version="3.0.0")
client.artifacts.upload(path="./out/resnet50.safetensors", project=PROJECT,
name="resnet50", kind="model", version="1.4.0")
# The oid is the content address — hash locally, no extra request.
dataset_oid = sha256_file("./data/train-v3.parquet")
model_oid = sha256_file("./out/resnet50.safetensors")
client.lineage.add_edge(
PROJECT,
from_type="artifact", from_key=model_oid,
to_type="artifact", to_key=dataset_oid,
relation="trained-with",
)
TypeScript
import { CAVS, sha256File } from '@cavsnode/cavs-sdk';
const client = CAVS.fromEnv();
const repoId = process.env.CAVS_REPO_ID!; // the TS SDK takes the UUID
await client.lineage.addEdge(repoId, {
fromType: 'artifact', fromKey: await sha256File('./out/resnet50.safetensors'),
toType: 'artifact', toKey: await sha256File('./data/train-v3.parquet'),
relation: 'trained-with',
});
Shell
curl -sS -X POST "$API/repositories/$REPO/graph/edges" \
-H "$H" -H "Content-Type: application/json" \
-d "{\"from_type\":\"artifact\",\"from_key\":\"$MODEL_OID\",
\"to_type\":\"artifact\",\"to_key\":\"$DATASET_OID\",
\"relation\":\"trained-with\"}"

Impact analysis before deleting. Fetch the graph, find the artifact’s node, walk the incoming edges. Anything pointing at it is something that would lose its provenance.

Reproducing a result. Start from the model’s oid, follow trained-with and derived-from backwards, download each oid. Because oids are content addresses, you get exactly the bytes that produced the result.

Auditing a release. Follow the inferred contains edges out of the release node for the complete manifest.

Terminal window
# Everything v1.4.0 contains
curl -sS "$API/repositories/$REPO/graph" -H "$H" \
| jq -r '.edges[] | select(.from == "release:v1.4.0") | .to'
# Everything that points at one artifact
curl -sS "$API/repositories/$REPO/graph" -H "$H" \
| jq -r --arg n "artifact:$OID" '.edges[] | select(.to == $n) | "\(.relation) ← \(.from)"'
  • One graph per repository; cross-repository edges aren’t modelled.
  • Inferred edges cover release → artifact only. Commit-level provenance lives in the commit index instead.
  • Registry enrichment reads up to 200 artifacts per repository, so a very large repository may show shortened-oid labels on its long tail.
  • The Runs API (a first-class execution record with automatic run→artifact lineage) is on the roadmap and not available today. Declared edges are the supported mechanism. See Choosing an integration.