Dependency relationships

A dependency is a live reference from one representation to another production object. Examples include a USD layer referencing a character layer, a Nuke script reading a plate, or an OCIO configuration naming a LUT. The target is consulted whenever the source is opened or used.

Dependencies are distinct from two existing relationships:

Relationship

Meaning

Effect of a target change

Membership

Resources constitute a representation

The representation changed

Provenance

An activity used inputs to produce outputs

Generated outputs may be stale

Dependency

A representation needs a live target

The source sees the target’s current content

A changed dependency therefore does not make the referencing representation stale. It can make an artifact produced from that representation stale, because the activity used a previous state of the dependency closure.

In the following example, a compositing script is one input of a render, and the script reads a LUT at render time:

        flowchart LR
    file["script file<br/>resource"] -- "membership" --> script["compositing script<br/>representation"]
    script -. "dependency" .-> lut["LUT<br/>representation"]
    plate["plate<br/>representation"] -- "input" --> render{{"render activity"}}
    script -- "input" --> render
    render -- "output" --> comp["comp render<br/>representation"]
    

A new LUT does not make the script stale; the script still reads whichever LUT is current. It makes the comp render stale, because the render activity captured the LUT that the script read when the render was made.

Edge model

Every edge records:

  • the source representation and, optionally, the source resource containing the reference;

  • an open-world namespaced kind such as org.openusd:reference;

  • either a floating asset target or a pinned representation target;

  • the representation selected for a floating target, when the caller resolved one;

  • whether the dependency is required; and

  • the authored reference exactly as supplied, without normalization.

Edges have no separate durable IDs. Their order within the source’s complete recorded set distinguishes repeated occurrences.

PostProject does not inspect files or resolve authored paths. A host or adapter supplies the dependency set and any resolved representation. Unknown valid kinds and authored reference text round-trip unchanged.

Complete observations

Dependencies are observations of source content rather than individually editable rows. Recording replaces the complete ordered set atomically. An identical current set is a no-op; a different set creates one semantic revision event. An explicitly recorded empty set means the source was inspected and has no dependencies. This is different from an absent set, which means dependency knowledge has never been recorded.

Recording a new fingerprint for the source representation marks its existing dependency set as needs_extraction. A caller must extract and record a new complete set before it is current again. Storage never reads source files on the caller’s behalf.

Cycles are permitted. Dependency graphs can reflect composition systems whose relationships are not acyclic, so traversal must always use explicit bounds.

Artifact evaluation

When an activity is recorded, storage captures the required dependency closure of each input alongside its direct fingerprint snapshot. Captured evidence includes the typed path and the representation chosen for each floating asset target.

Artifact evaluation reports:

  • stale when a captured required path changes or a dependency’s comparable fingerprint changes;

  • indeterminate when dependency knowledge is absent, needs extraction, cannot resolve a floating target, lacks comparable fingerprint evidence, or exceeds a traversal bound; and

  • current only when the required captured closure remains comparable and unchanged.

Reasons contain the exact dependency path so an application can explain which authored reference led to the result. Optional dependencies remain queryable but are excluded from activity snapshot closure.

Public operations

Rust exposes complete-set recording through ProductionStoreTransaction and bounded forward/reverse queries through ProductionRead. A query supplies a depth from 1 through 64, a visited-representation bound from 1 through 1,000, and a page size from 1 through 1,000. Depth one is a direct query. Results are ordered by stable target identity, include the shortest observed depth, and exclude a cycle back to the query root. traversal_truncated is independent of next_cursor: the former means graph bounds hid part of the closure, while the latter means more results remain in the bounded closure.

Continuation cursors are opaque and bound to the query root and all traversal parameters. Reusing one with different parameters is an invalid argument. Pages are weakly consistent across commits; use the revision feed when a caller needs change tracking rather than a snapshot scan.

The C ABI uses owned dependency observation and query-page handles; strings and cursors borrow their owning handle until release. The C++ and Python wrappers copy those values into native immutable objects.

The CLI accepts an ordered JSON array with dependency record, distinguishes absent and empty observations with dependency show, and exposes dependency dependencies and dependency dependents with --max-depth, --max-representations, --limit, and --cursor. JSON queries return items, next_cursor, and traversal_truncated. All recording remains explicit and transactional. dependency record reports accepted edges, changed and its own commit receipt. dependency show reads current stored facts, which another writer may have changed after recording.

Across public surfaces

The example records a complete dependency observation, then performs bounded forward and reverse queries:

static pp_error_code_t
record_and_query_dependencies(pp_production_t *production,
                              const pp_representation_id_t *source_id,
                              const pp_asset_id_t *target_asset_id,
                              const pp_representation_id_t *resolved_id,
                              pp_error_t **error) {
  pp_dependency_t dependency = {0};
  dependency.kind = "org.example:clip-reference";
  pp_error_code_t reference_status = pp_object_ref_from_asset(*target_asset_id, &dependency.target, error);
  if (reference_status != PP_OK) return reference_status;
  dependency.has_resolved_representation = UINT8_C(1);
  dependency.resolved_representation_id = *resolved_id;
  dependency.required = UINT8_C(1);
  dependency.authored_reference = "rushes/A001.mov";

  pp_read_session_t *view = NULL;
  pp_transaction_t *transaction = NULL;
  pp_dependency_query_set_t *dependencies = NULL;
  pp_dependency_query_set_t *dependents = NULL;
  pp_error_code_t status =
      pp_production_read_session(production, &view, error);
  if (status == PP_OK) {
    status = pp_read_session_begin_edit(view, NULL, &transaction, error);
  }
  pp_read_session_release(view);
  if (status == PP_OK) {
    status = pp_transaction_record_dependency_set(
        transaction, *source_id, &dependency, UINT64_C(1), error);
  }
  if (status == PP_OK) {
    pp_commit_receipt_t commit_receipt;
    status = pp_transaction_commit(transaction, &commit_receipt, error);
  }
  if (status == PP_OK) {
    status = pp_production_dependencies(production, *source_id, UINT32_C(4),
                                        UINT32_C(1000), UINT32_C(100), NULL,
                                        &dependencies, error);
  }
  for (uint64_t i = 0;
       status == PP_OK && i < pp_dependency_query_set_count(dependencies);
       ++i) {
    pp_dependency_match_t match;
    status = pp_dependency_query_set_get(dependencies, i, &match, error);
    if (status == PP_OK) {
      printf("dependency at depth %u\n", match.depth);
    }
  }
  pp_object_ref_t target;
  pp_error_code_t target_status = pp_object_ref_from_asset(*target_asset_id, &target, error);
  if (target_status != PP_OK) return target_status;
  if (status == PP_OK) {
    status = pp_production_dependents(production, &target, UINT32_C(4),
                                      UINT32_C(1000), UINT32_C(100), NULL,
                                      &dependents, error);
  }
  if (status == PP_OK &&
      (pp_dependency_query_set_traversal_truncated(dependencies) != 0 ||
       pp_dependency_query_set_count(dependents) != UINT64_C(1))) {
    status = PP_ERROR_INTERNAL;
  }

  pp_dependency_query_set_release(dependents);
  pp_dependency_query_set_release(dependencies);
  pp_transaction_release(transaction);
  return status;
}
void record_and_query_dependencies(postproject::Production &production,
                                   const postproject::RepresentationId &source_id,
                                   const postproject::AssetId &target_asset_id,
                                   const postproject::RepresentationId &resolved_id) {
  const postproject::ObjectRef target = postproject::ObjectRef::asset(target_asset_id);
  const postproject::Dependency dependency{
      std::nullopt, "org.example:clip-reference", target, resolved_id,
      true, "rushes/A001.mov"};
  auto transaction = production.readSession().value().edit().value();
  transaction.recordDependencySet(source_id, {dependency}).value();
  transaction.commit().value();

  const auto dependencies = production.dependencies(source_id, 4, 1000, 100).value();
  for (const auto &match : dependencies.items) {
    std::cout << "dependency at depth " << match.depth << '\n';
  }
  require(!dependencies.traversal_truncated, "complete dependency traversal");

  const auto dependents = production.dependents(target, 4, 1000, 100).value();
  require(dependents.items.size() == 1 &&
              dependents.items.front().target == postproject::ObjectRef::representation(source_id),
          "reverse dependency query");
}
def record_and_query_dependencies(
    production: Production,
    source_id: RepresentationId,
    target_asset_id: AssetId,
    resolved_id: RepresentationId,
) -> None:
    dependency = Dependency(
        kind="org.example:clip-reference",
        target=AssetRef(target_asset_id),
        authored_reference="rushes/A001.mov",
        resolved_representation_id=resolved_id,
    )
    with production.read_session() as view, view.edit() as transaction:
        transaction.record_dependency_set(source_id, (dependency,))
        transaction.commit()

    dependencies = production.dependencies(
        source_id, max_depth=4, max_representations=1000, limit=100
    )
    for match in dependencies.items:
        print(f"dependency {match.target} at depth {match.depth}")
    assert not dependencies.traversal_truncated

    dependents = production.dependents(
        AssetRef(target_asset_id), max_depth=4, max_representations=1000, limit=100
    )
    assert dependents.items[0].target == RepresentationRef(source_id)
fn record_and_query_dependencies(
    production: &mut SqliteProduction,
    source_id: RepresentationId,
    target_asset_id: AssetId,
    resolved_id: RepresentationId,
) -> Result<()> {
    let dependency = Dependency::new(
        None,
        DependencyKind::new("org.example:clip-reference")?,
        DependencyTarget::Asset(target_asset_id),
        Some(resolved_id),
        true,
        "rushes/A001.mov",
    )?;
    {
        let base = production.read_session()?.decision_base();
        let mut transaction = production.begin_edit(base)?;
        transaction.record_dependency_set(source_id, &[dependency])?;
        transaction.commit()?;
    }

    let limits = DependencyQueryLimits::new(4, 1_000)?;
    let request = QueryPageRequest::new(100, None)?;
    let dependencies = production.dependencies(source_id, limits, &request)?;
    for item in dependencies.items() {
        println!("dependency {:?} at depth {}", item.target(), item.depth());
    }
    assert!(!dependencies.traversal_truncated());

    let dependents =
        production.dependents(DependencyTarget::Asset(target_asset_id), limits, &request)?;
    assert_eq!(
        dependents.items()[0].target(),
        DependencyTarget::Representation(source_id)
    );
    Ok(())
}
# This small scene actually authors the live reference; rendered frames do not.
printf 'rushes/A001.mov\n' > scene.ppscene
SCENE_ID=$(postproject --json media add production.pproj scene.ppscene | jq -r .representation_id)
cat > dependency.json <<EOF
[
  {
    "kind": "org.example:media-reference",
    "target": {"kind": "asset", "id": "$ASSET_ID"},
    "resolved_representation_id": "$ORIGINAL_ID",
    "required": true,
    "authored_reference": "rushes/A001.mov"
  }
]
EOF
BASE=$(postproject --json inspect production.pproj | jq -r .decision_base)
postproject --decision-base "$BASE" dependency record production.pproj "$SCENE_ID" dependency.json
postproject --json dependency dependencies production.pproj "$SCENE_ID" \
  --max-depth 4 --max-representations 1000 --limit 100 |
  jq '.items[] | {target, depth}'
postproject dependency dependents production.pproj asset "$ASSET_ID" \
  --max-depth 4 --max-representations 1000 --limit 100

A new fingerprint observation of the source marks its recorded set as needing extraction until a fresh complete set is recorded:

static pp_error_code_t read_dependency_set(
    const pp_production_t *production, const pp_representation_id_t *representation_id,
    pp_dependency_set_status_t *out_status, pp_error_t **error) {
  pp_dependency_set_t *set = NULL;
  uint8_t present = 0;
  pp_representation_id_t source_id;
  uint64_t recorded_at = 0;
  uint64_t count = 0;
  pp_error_code_t status =
      pp_production_dependency_set(production, *representation_id, &set, error);
  if (status == PP_OK) {
    status = pp_dependency_set_get(set, &present, &source_id, &recorded_at,
                                   out_status, &count, error);
  }
  /* Absent knowledge differs from a recorded empty set. */
  if (status == PP_OK && !present) {
    printf("no dependency set recorded\n");
  }
  for (uint64_t i = 0; status == PP_OK && present && i < count; ++i) {
    pp_dependency_t dependency;
    status = pp_dependency_set_get_dependency(set, i, &dependency, error);
    if (status == PP_OK) {
      printf("%s -> %s (%s)\n", dependency.kind,
             dependency.authored_reference != NULL
                 ? dependency.authored_reference
                 : "-",
             dependency.required ? "required" : "optional");
    }
  }
  if (status == PP_OK && present) {
    printf("status %u, recorded at revision %llu\n", *out_status,
           (unsigned long long)recorded_at);
  }
  pp_dependency_set_release(set);
  return status;
}

static pp_error_code_t record_dependencies(pp_production_t *production,
                                           const pp_representation_id_t *source_id,
                                           const pp_asset_id_t *original_asset,
                                           const pp_representation_id_t *original_id,
                                           pp_error_t **error) {
  pp_dependency_t dependencies[2];
  memset(dependencies, 0, sizeof dependencies);
  dependencies[0].kind = "org.example:source-media";
  pp_error_code_t reference_status = pp_object_ref_from_asset(*original_asset, &dependencies[0].target, error);
  if (reference_status != PP_OK) return reference_status;
  dependencies[0].has_resolved_representation = 1;
  dependencies[0].resolved_representation_id = *original_id;
  dependencies[0].required = 1;
  dependencies[0].authored_reference = "rushes/A001.mov";
  dependencies[1].kind = "org.example:timecode-reference";
  reference_status = pp_object_ref_from_representation(*original_id, &dependencies[1].target, error);
  if (reference_status != PP_OK) return reference_status;
  dependencies[1].required = 0;
  dependencies[1].authored_reference = "A001.mov#timecode";

  pp_read_session_t *view = NULL;
  pp_transaction_t *transaction = NULL;
  pp_error_code_t status =
      pp_production_read_session(production, &view, error);
  if (status == PP_OK) {
    status = pp_read_session_begin_edit(view, NULL, &transaction, error);
  }
  pp_read_session_release(view);
  if (status == PP_OK) {
    /* The call replaces the complete, ordered dependency observation. */
    status = pp_transaction_record_dependency_set(transaction, *source_id,
                                                  dependencies, 2, error);
  }
  if (status == PP_OK) {
    pp_commit_receipt_t commit_receipt;
    status = pp_transaction_commit(transaction, &commit_receipt, error);
  }
  pp_transaction_release(transaction);
  return status;
}

static pp_error_code_t observe_source(pp_production_t *production,
                                      const pp_representation_id_t *source_id,
                                      const uint8_t value[8],
                                      pp_error_t **error) {
  pp_transaction_t *transaction = NULL;
  pp_read_session_t *view = NULL;
  pp_error_code_t status =
      pp_production_read_session(production, &view, error);
  if (status == PP_OK) {
    status = pp_read_session_begin_edit(view, NULL, &transaction, error);
  }
  pp_read_session_release(view);
  view = NULL;
  if (status == PP_OK) {
    /* A new source fingerprint marks its dependency set for re-extraction. */
    status = pp_transaction_record_representation_fingerprint(
        transaction, *source_id, HOST_REPRESENTATION_ALGORITHM, 1, value, 8,
        error);
  }
  if (status == PP_OK) {
    pp_commit_receipt_t commit_receipt;
    status = pp_transaction_commit(transaction, &commit_receipt, error);
  }
  pp_transaction_release(transaction);
  return status;
}

static pp_error_code_t
count_dependencies_by_page(const pp_production_t *production,
                           const pp_representation_id_t *source_id, uint64_t *out_count,
                           pp_error_t **error) {
  char cursor_storage[2049] = {0};
  const char *cursor = NULL;
  pp_error_code_t status = PP_OK;
  *out_count = 0;
  while (status == PP_OK) {
    pp_dependency_query_set_t *page = NULL;
    status = pp_production_dependencies(production, *source_id, UINT32_C(4),
                                        UINT32_C(1000), UINT32_C(1), cursor,
                                        &page, error);
    *out_count += status == PP_OK ? pp_dependency_query_set_count(page) : 0;
    const char *next =
        status == PP_OK ? pp_dependency_query_set_next_cursor(page) : NULL;
    const int copied =
        next != NULL
            ? snprintf(cursor_storage, sizeof cursor_storage, "%s", next)
            : 0;
    pp_dependency_query_set_release(page);
    if (status != PP_OK || copied == 0) {
      break;
    }
    if (copied < 0 || (size_t)copied >= sizeof cursor_storage) {
      status = PP_ERROR_INTERNAL;
    }
    cursor = cursor_storage;
  }
  return status;
}
std::vector<postproject::DependencyMatch> record_scene_dependencies(
    postproject::Production &production, const postproject::RepresentationId &scene_id,
    const std::vector<postproject::Dependency> &observed) {
  // A dependency set replaces the complete previous observation.
  auto transaction = production.readSession().value().edit().value();
  transaction.recordDependencySet(scene_id, observed).value();
  transaction.commit().value();

  if (const auto set = production.dependencySet(scene_id).value()) {
    std::cout << set->dependencies.size() << " dependencies, "
              << (set->status == postproject::DependencySetStatus::current
                      ? "current"
                      : "needs extraction")
              << '\n';
  }

  std::vector<postproject::DependencyMatch> matches;
  std::optional<std::string> cursor;
  do {
    const auto page = production.dependencies(scene_id, 4, 1000, 1, cursor).value();
    matches.insert(matches.end(), page.items.begin(), page.items.end());
    cursor = page.next_cursor;
  } while (cursor.has_value());
  return matches;
}
def record_edit_dependencies(
    production: Production,
    edit_id: RepresentationId,
    camera_asset_id: AssetId,
    original_id: RepresentationId,
    proxy_id: RepresentationId,
) -> DependencySet | None:
    # Always record the complete set: it replaces the previous observation.
    dependencies = (
        Dependency(
            "org.example:clip-reference",
            AssetRef(camera_asset_id),
            "rushes/A001.mov",
            resolved_representation_id=original_id,
        ),
        Dependency(
            "org.example:offline-proxy",
            RepresentationRef(proxy_id),
            "proxies/A001_proxy.mov",
            required=False,
        ),
    )
    with production.read_session() as view, view.edit() as transaction:
        transaction.record_dependency_set(edit_id, dependencies)
        transaction.commit()

    recorded = production.dependency_set(edit_id)  # None: never extracted
    if recorded is not None:
        print(f"{len(recorded.dependencies)} dependencies, {recorded.status.name}")
    return recorded


def all_dependencies(
    production: Production, edit_id: RepresentationId
) -> list[DependencyMatch]:
    matches: list[DependencyMatch] = []
    cursor = None
    while True:
        page = production.dependencies(
            edit_id, max_depth=1, max_representations=1000, limit=1, cursor=cursor
        )
        matches.extend(page.items)
        cursor = page.next_cursor
        if cursor is None:
            return matches
fn record_comp_dependencies(
    production: &mut SqliteProduction,
    comp_id: RepresentationId,
    plate_asset_id: AssetId,
    plate_original_id: RepresentationId,
    proxy_id: RepresentationId,
) -> Result<()> {
    let dependencies = [
        // A floating asset reference, recorded with what it resolved to.
        Dependency::new(
            None,
            DependencyKind::new("org.example:plate")?,
            DependencyTarget::Asset(plate_asset_id),
            Some(plate_original_id),
            true,
            "rushes/A001.mov",
        )?,
        // A reference pinned to one representation.
        Dependency::new(
            None,
            DependencyKind::new("org.example:preview")?,
            DependencyTarget::Representation(proxy_id),
            None,
            false,
            "proxies/A001_proxy.mov",
        )?,
    ];
    {
        let base = production.read_session()?.decision_base();
        let mut transaction = production.begin_edit(base)?;
        // The set replaces the previous observation as a whole.
        transaction.record_dependency_set(comp_id, &dependencies)?;
        transaction.commit()?;
    }

    let set = production
        .dependency_set(comp_id)?
        .expect("a dependency set was recorded");
    println!(
        "{} dependencies recorded at revision {}: {:?}",
        set.dependencies().len(),
        set.recorded_at_revision(),
        set.status()
    );
    for dependency in set.dependencies() {
        println!(
            "{} -> {:?} ({})",
            dependency.kind().as_str(),
            dependency.target(),
            dependency.authored_reference()
        );
    }

    let limits = DependencyQueryLimits::new(4, 1_000)?;
    let mut cursor = None;
    loop {
        let page = production.dependencies(comp_id, limits, &QueryPageRequest::new(1, cursor)?)?;
        for item in page.items() {
            println!("depends on {:?} at depth {}", item.target(), item.depth());
        }
        cursor = page.next_cursor().cloned();
        if cursor.is_none() {
            return Ok(());
        }
    }
}

fn dependency_status(
    production: &SqliteProduction,
    comp_id: RepresentationId,
) -> Result<Option<DependencySetStatus>> {
    // After a new fingerprint observation of the comp, the recorded set is
    // reported as needing extraction until it is recorded again.
    Ok(production.dependency_set(comp_id)?.map(|set| set.status()))
}
# The recorded array replaces the representation's complete dependency set.
cat > dependencies.json <<EOF
[
  {
    "kind": "org.example:plate",
    "target": {"kind": "asset", "id": "$ASSET_ID"},
    "resolved_representation_id": "$ORIGINAL_ID",
    "authored_reference": "rushes/A001.mov"
  },
  {
    "kind": "org.example:preview",
    "target": {"kind": "representation", "id": "$PROXY_ID"},
    "required": false,
    "authored_reference": "proxies/A001_proxy.mov"
  }
]
EOF
BASE=$(postproject --json inspect provenance.pproj | jq -r .decision_base)
RECORDED=$(postproject --json --decision-base "$BASE" dependency record provenance.pproj "$COMP_ID" dependencies.json)
# This is the accepted submission; dependency show reads current stored facts.
test "$(jq -r .changed <<<"$RECORDED")" = true
test "$(jq '.dependencies | length' <<<"$RECORDED")" = 2
test "$(jq '.commit_receipt.revision.sequence > 0' <<<"$RECORDED")" = true
postproject dependency show provenance.pproj "$COMP_ID"
CURSOR=
while :; do
  ARGS=(--json dependency dependencies provenance.pproj "$COMP_ID" --limit 1)
  [[ -n "$CURSOR" ]] && ARGS+=(--cursor "$CURSOR")
  PAGE=$(postproject "${ARGS[@]}")
  jq -r '.items[] | "\(.target.kind) \(.target.id) depth \(.depth)"' <<<"$PAGE"
  CURSOR=$(jq -r '.next_cursor // empty' <<<"$PAGE")
  [[ -z "$CURSOR" ]] && break
done
# Once the comp itself changes, its recorded dependencies need re-extraction.
printf 'comp script v2' > comp/shot010.nk
COMP_RESOURCE_ID=$(postproject --json representation resources provenance.pproj \
  "$COMP_ID" | jq -r '.items[0].id')
DECISION_BASE=$(postproject --json inspect provenance.pproj | jq -r .decision_base)
postproject --decision-base "$DECISION_BASE" media fingerprint provenance.pproj "$COMP_RESOURCE_ID" \
  comp/shot010.nk
postproject --json dependency show provenance.pproj "$COMP_ID" | jq -r .status

See bounded queries and cursors for cursor ownership, pagination, and weak-consistency guidance.

See artifact knowledge and reproducibility for the derived state model and production provenance for completed transformations. The standards mappings and deliberate non-mappings are in the standards matrix.