Skip to contents
# Use development build when interactive *and* explicitly enabled via env var.
dev_mode <- (Sys.getenv("DEV_VIGNETTES", "false") == "true")

if (dev_mode && requireNamespace("pkgload", quietly = TRUE)) {
  cli::cli_inform("loading with {.pkg pkgload}")
  pkgload::load_all()
} else {
  # fall back to the installed package (the path CRAN, CI, and pkgdown take)
  cli::cli_inform("loading with {.pkg library}")
  library(stamp)
}
## loading with library

This vignette shows how stamp captures lineage (parents → children), how to detect staleness, and how to plan & rebuild downstream artifacts in level order.

You’ll use:

Strict vs. Propagate Strict checks actual mismatch vs the parents’ current latest versions. Propagate simulates change pushing downstream (A changes ⇒ schedule B ⇒ schedule C, etc.).

A few implementation notes that clarify behavior used by the examples below:

  • Committed parents (the parents.json file inside a version snapshot) are the authoritative record for lineage when present. They are captured at commit time and are used for reproducible, recursive lineage walking.
  • Sidecar parents (the parents field in the sidecar metadata written next to the artifact file) are a lightweight convenience that let you inspect first-level lineage before a snapshot is committed. st_lineage() falls back to sidecar parents only for the immediate parent level when no snapshot is available; recursive walking beyond level 1 requires snapshot-backed parents.

This design keeps interactive workflows fast (sidecars are quick) while preserving reproducible history once snapshots are written.

Setup a tiny graph A → B(A) → C(B)

st_opts_reset()
st_opts(
  versioning = "content",
  code_hash = TRUE,
  store_file_hash = TRUE,
  verify_on_load = TRUE,
  meta_format = "both"
)
##  stamp options updated
##   versioning = "content", code_hash = "TRUE", store_file_hash = "TRUE",
##   verify_on_load = "TRUE", meta_format = "both"
root <- tempdir()
st_init(root)
##  stamp initialized
##   alias: default
##   root: /tmp/Rtmpbcollr
##   state: /tmp/Rtmpbcollr/.stamp
# A
pA <- "A.qs"
xA <- data.frame(a = 1:3)
st_save(xA, pA, code = function(z) z, alias = NULL)
##  Saved [qs2] → /tmp/Rtmpbcollr/A.qs @ version
## 828d0e396e6f1b11
# B depends on A
pB <- "B.qs"
xB <- transform(xA, b = a * 2)
st_save(
  xB,
  pB,
  code = function(z) z,
  parents = list(list(path = pA, version_id = st_latest(pA, alias = NULL))),
  alias = NULL
)
##  Saved [qs2] → /tmp/Rtmpbcollr/B.qs @ version
## 37fac9d3ff54c43d
# C depends on B
pC <- "C.qs"
xC <- transform(xB, c = b + 1L)
st_save(
  xC,
  pC,
  code = function(z) z,
  parents = list(list(path = pB, version_id = st_latest(pB, alias = NULL))),
  alias = NULL
)
##  Saved [qs2] → /tmp/Rtmpbcollr/C.qs @ version
## 39f68ea1c87c833e

Note: after these saves each artifact has a sidecar (in stmeta/ next to the artifact) and snapshots in its own versions/ directory (per-artifact storage, not centralized).

Inspect lineage

# Immediate children of A (depth 1)
st_children(pA, depth = 1, alias = NULL)
##             child_path    child_version
## 1 /tmp/Rtmpbcollr/B.qs 37fac9d3ff54c43d
##                                    parent_path   parent_version level
## 1 /home/runner/work/stamp/stamp/vignettes/A.qs 828d0e396e6f1b11     1
# Full lineage (parents of an artifact)
st_lineage(pC, depth = Inf, alias = NULL)
## [1] level          child_path     child_version  parent_path    parent_version
## <0 rows> (or 0-length row.names)

Make a change upstream & detect staleness

# Change A → new version
xA2 <- transform(xA, a = a + 10L)
st_save(xA2, pA, code = function(z) z, alias = NULL)
##  Saved [qs2] → /tmp/Rtmpbcollr/A.qs @ version
## 735e79e4e016b08e
# Strict staleness
st_is_stale(pB) # TRUE (B's recorded A version is now old)
## [1] TRUE
st_is_stale(pC) # FALSE (C points to B, which hasn't changed yet)
## [1] FALSE

Plan rebuilds

Two strategies:

  • strict: only items whose recorded parent IDs differ from parents’ current latest.
  • propagate: assume targets will change and plan descendants in BFS layers.
# Strict: only B right now
plan_strict <- st_plan_rebuild(pA, depth = Inf, mode = "strict")
plan_strict
##   level                 path         reason latest_version_before
## 1     1 /tmp/Rtmpbcollr/B.qs parent_changed      37fac9d3ff54c43d
# Propagate: includes B (level 1) and C (level 2)
plan <- st_plan_rebuild(pA, depth = Inf, mode = "propagate")
plan
##   level                 path           reason latest_version_before
## 1     1 /tmp/Rtmpbcollr/B.qs upstream_changed      37fac9d3ff54c43d

Register builders and rebuild in level order

Builders are tiny functions that produce an artifact from its parents. They receive (path, parents) and return a list with at least x = <object>.

# Clear any previous registry
st_clear_builders()
##  Cleared all registered builders
# Register a builder for B: rebuild from A's committed version
st_register_builder(pB, function(path, parents) {
  # parents is list(list(path=..., version_id=...))
  A <- st_load_version(parents[[1]]$path, parents[[1]]$version_id, alias = NULL)
  list(
    x = transform(A, b = a * 2),
    code = function(z) z,
    code_label = "B <- A * 2"
  )
})
##  Registered builder for B.qs (default)
# Register a builder for C: rebuild from B's committed version
st_register_builder(pC, function(path, parents) {
  B <- st_load_version(parents[[1]]$path, parents[[1]]$version_id, alias = NULL)
  list(
    x = transform(B, c = b + 1L),
    code = function(z) z,
    code_label = "C <- B + 1"
  )
})
##  Registered builder for C.qs (default)
# Dry run first (uses registered builders found by st_rebuild when rebuild_fun is NULL)
st_rebuild(plan, dry_run = TRUE)
##  Rebuild level 1: 1 artifact
##   • /tmp/Rtmpbcollr/B.qs (upstream_changed)
##   DRY RUN
##  Rebuild summary
##   dry_run 1
# Now actually rebuild (will use registered builders)
res <- st_rebuild(plan, dry_run = FALSE)
##  Rebuild level 1: 1 artifact
##   • /tmp/Rtmpbcollr/B.qs (upstream_changed)
## Warning: FAILED: No builder registered for path: /tmp/Rtmpbcollr/B.qs and no rebuild_fun
## provided.
##  Rebuild summary
##   failed 1
res
##   level                 path           reason status version_id
## 1     1 /tmp/Rtmpbcollr/B.qs upstream_changed failed       <NA>
##                                                                                 msg
## 1 No builder registered for path: /tmp/Rtmpbcollr/B.qs and no rebuild_fun provided.

After rebuilding B, C becomes strictly stale if B changes again later. You can re-plan from B to keep propagating:

## [1] TRUE
## [1] FALSE
st_plan_rebuild(pB, depth = Inf, mode = "propagate")
##   level                 path           reason latest_version_before
## 1     1 /tmp/Rtmpbcollr/C.qs upstream_changed      39f68ea1c87c833e

Inspect snapshots on disk

vroot <- fs::path_dir(st_info(pA, alias = NULL)$sidecar$path)
vroot <- fs::path(vroot, "versions")
if (fs::dir_exists(vroot)) {
  fs::dir_tree(vroot, recurse = TRUE, all = TRUE)
}

Tip: st_info(path) summarizes sidecar, catalog status, and the latest snapshot dir, plus parsed parents.json from the committed snapshot (if present).

st_info(pC, alias = NULL)
## $sidecar
## $sidecar$path
## [1] "/tmp/Rtmpbcollr/C.qs"
## 
## $sidecar$format
## [1] "qs2"
## 
## $sidecar$created_at
## [1] "2026-03-04T21:29:49.589277Z"
## 
## $sidecar$size_bytes
## [1] 266
## 
## $sidecar$content_hash
## [1] "28e3e19ddcccd8c6"
## 
## $sidecar$code_hash
## [1] "488e8fa49c740261"
## 
## $sidecar$file_hash
## [1] "fefe26c9df86a402"
## 
## $sidecar$code_label
## NULL
## 
## $sidecar$parents
##   path       version_id
## 1 B.qs 37fac9d3ff54c43d
## 
## $sidecar$attrs
## list()
## 
## 
## $catalog
## $catalog$latest_version_id
## [1] "39f68ea1c87c833e"
## 
## $catalog$n_versions
## [1] 1
## 
## 
## $snapshot_dir
## /tmp/Rtmpbcollr/C.qs/versions/39f68ea1c87c833e
## 
## $parents
## $parents[[1]]
## $parents[[1]]$path
## [1] "B.qs"
## 
## $parents[[1]]$version_id
## [1] "37fac9d3ff54c43d"

Takeaways

  • Use strict staleness to detect objective mismatches.
  • Use propagate planning to build a full downstream schedule.
  • Keep builders small, pure, and deterministic; they make rebuilds trivial.