Bounded queries and cursors

Development cursors are production-scoped. Live cursors can resume through another handle to the same production. Read-session cursors require that exact open session; another view at the same revision cannot resume them. Version ppq1 cursors must be discarded when moving to the development API. For coherent paging, see Coherent reads and scoped edits.

Production-sized queries return stable keyset pages instead of complete lists. Choose a page size from 1 through 1,000 and pass the returned opaque cursor to the next call. Keep the query root, filters, page size, and traversal bounds unchanged while following a cursor.

A cursor is a weakly consistent continuation, not a durable snapshot or a revision-feed position. A later page sees the current durable state after its key. Use the revision feed when an integration needs to observe changes rather than scan current state.

Available queries

Every named query below is available on all five surfaces as of C ABI version 25 and package 0.4.0-alpha.1. The CLI prints a page as items followed by a next_cursor line, or as a JSON object with items, next_cursor, and traversal_truncated; --limit defaults to 100 and --cursor continues a page.

Query

C

C++

Python

Rust

CLI

Assets

pp_production_assets_page

assets(limit)

assets_page

assets_page

media list --limit

Representations of an asset

pp_production_representations_page

representations(id, limit)

representations_page

representations_page

representation list

Resources of a representation

pp_production_resources_page

resources

resources_page

resources_page

representation resources

Locators of a resource

pp_production_locators_page

locators

locators_page

locators_page

locator list

Unresolved media

pp_production_unresolved_media

unresolvedMedia

unresolved_media

unresolved_media

media unresolved

Representations under a media root

pp_production_representations_under_media_root

representationsUnderMediaRoot

representations_under_media_root

representations_under_media_root

media under-root

Objects carrying a metadata property

pp_production_query_metadata

queryMetadata

query_metadata

metadata_query

metadata find

Activities producing or consuming

pp_production_activities_producing_page, pp_production_activities_consuming_page

activitiesProducing(id, limit), activitiesConsuming(id, limit)

activities_producing_page, activities_consuming_page

activities_producing_page, activities_consuming_page

activity producing, activity consuming

Outputs by activity kind or tool

pp_production_outputs_by_activity_kind, pp_production_outputs_by_tool

outputsByActivityKind, outputsByTool

outputs_by_activity_kind, outputs_by_tool

activity_outputs

activity outputs --kind or --tool-name

Provenance ancestors or descendants

pp_production_provenance_ancestors_page, pp_production_provenance_descendants_page

ancestors(id, depth, …), descendants(id, depth, …)

provenance_ancestors_page, provenance_descendants_page

ancestors_page, descendants_page

activity ancestors, activity descendants

Dependencies or dependents

pp_production_dependencies, pp_production_dependents

dependencies, dependents

dependencies, dependents

dependencies, dependents

dependency dependencies, dependency dependents

Stale artifacts

pp_production_stale_artifacts

staleArtifacts

stale_artifacts

stale_artifacts

artifact stale

Jobs by state and kind

pp_production_jobs

jobs

jobs

jobs

job list

Objects changed since a revision

pp_production_objects_changed_since

objectsChangedSince

objects_changed_since

objects_changed_since

revisions changed

Complete-list asset, representation, resource and locator helpers cap at 1000. Larger results return Unsupported; page them instead. Compound structures retain their separate domain member limit. Producing/consuming activity helpers also cap at 1000; ancestry helpers cap traversal at 64 levels and 1000 visited representations and reject incomplete results. The CLI query commands return one page.

Materialized collections cap at 100000 rows and 64 MiB of stored payload; nested pages share the limit with their child rows. Artifact explanations and traversal-cache copies also have finite budgets. Metadata property convenience queries cap at 1000 matches. Pages also check the payload budget, including their lookahead row: after Unsupported, request a smaller page. Individual-value/domain limits are separate, and queries preserve the stored values rather than trimming them.

Read one object by identity

An integration that already holds an identity, for example from a host-object binding or an OpenAssetIO entity reference, reads that object directly instead of searching a page. Reading an asset, a representation, or a job returns the one object or reports not found. A resource is not owned by exactly one representation — the compound model allows several representations to share it — so the representations using a resource are a bounded page like any other query. These reads are available on all five surfaces as of C ABI version 27; the C ABI returns a point read as a one-element result set of the usual kind.

static pp_error_code_t read_known_objects(const pp_production_t *production,
                                          const pp_asset_id_t *asset_id,
                                          const pp_representation_id_t *representation_id,
                                          pp_error_t **error) {
  /* A host reference names one object; read it without scanning the
   * production. Point reads return one-element sets. */
  pp_asset_set_t *asset = NULL;
  pp_representation_set_t *representation = NULL;
  pp_representation_set_t *users = NULL;
  pp_asset_id_t id;
  int64_t created_at = 0;
  const char *name = NULL;
  const char *source = NULL;
  pp_resource_id_t resource_id = {0};
  const char *role = NULL;
  uint8_t required = 0;
  pp_error_code_t status =
      pp_production_asset(production, *asset_id, &asset, error);
  if (status == PP_OK) {
    status = pp_asset_set_get(asset, UINT64_C(0), &id, &created_at, &name,
                              &source, error);
  }
  if (status == PP_OK) {
    printf("asset: %s\n", name != NULL ? name : "unnamed");
    status = pp_production_representation(production, *representation_id,
                                          &representation, error);
  }
  if (status == PP_OK) {
    status = pp_representation_set_get_member(representation, UINT64_C(0),
                                              UINT64_C(0), &resource_id, &role,
                                              &required, error);
  }
  if (status == PP_OK) {
    status = pp_production_representations_using_resource(
        production, resource_id, UINT32_C(100), NULL, &users, error);
  }
  if (status == PP_OK) {
    printf("representations using the resource: %llu\n",
           (unsigned long long)pp_representation_set_count(users));
  }
  pp_representation_set_release(users);
  pp_representation_set_release(representation);
  pp_asset_set_release(asset);
  return status;
}
void read_known_objects(const postproject::Production &production,
                        const postproject::AssetId &asset_id,
                        const postproject::RepresentationId &representation_id) {
  // A host reference names one object; read it without scanning the
  // production.
  const auto asset = production.asset(asset_id).value();
  const auto representation = production.representation(representation_id).value();
  std::cout << asset.display_name.value_or("unnamed") << ": "
            << representation.resources.size() << " resource(s)\n";
  const auto users = production.representationsUsingResource(
      representation.resources[0].id, 100).value();
  require(std::any_of(users.items.begin(), users.items.end(),
                      [&](const postproject::Representation &item) {
                        return item.id == representation_id;
                      }),
          "representation uses its resource");
}
def read_known_objects(
    production: Production, asset_id: AssetId, representation_id: RepresentationId
) -> None:
    # A host reference names one object; read it without scanning the production.
    asset = production.asset(asset_id)
    representation = production.representation(representation_id)
    print(f"{asset.display_name}: {representation.kind.value}")
    resource_id = representation.resources[0].id
    users = production.representations_using_resource(resource_id, limit=100)
    assert representation_id in {item.id for item in users.items}
fn read_known_objects(
    production: &SqliteProduction,
    asset_id: AssetId,
    representation_id: RepresentationId,
) -> Result<()> {
    // A host reference names one object; read it without scanning the production.
    let asset = production.asset(asset_id)?;
    let representation = production.representation(representation_id)?;
    println!(
        "{}: {:?}",
        asset.display_name().unwrap_or("unnamed"),
        representation.kind()
    );
    let first_resource =
        production.resources_page(representation_id, &QueryPageRequest::new(1, None)?)?;
    let users = production.representations_using_resource(
        first_resource.items()[0].id(),
        &QueryPageRequest::new(100, None)?,
    )?;
    assert!(
        users
            .items()
            .iter()
            .any(|item| item.id() == representation_id)
    );
    Ok(())
}
# A host reference names one object; read it without scanning the production.
postproject media show production.pproj "$ASSET_ID"
postproject --json representation show production.pproj "$ORIGINAL_ID" |
  jq -r .kind
RESOURCE_ID=$(postproject --json representation resources production.pproj \
  "$ORIGINAL_ID" --limit 1 | jq -r '.items[0].id')
postproject --json representation using-resource production.pproj \
  "$RESOURCE_ID" --limit 100 | jq -r '.items[].id'

Read

C

C++

Python

Rust

CLI

One asset

pp_production_asset

asset

asset

asset

media show

One representation

pp_production_representation

representation

representation

representation

representation show

One job

pp_production_job

job

job

job

job show

Representations using a resource

pp_production_representations_using_resource

representationsUsingResource

representations_using_resource

representations_using_resource

representation using-resource

Page media structure

Asset pages follow creation time and asset ID. Representation pages list one asset’s representations, and resource pages list one representation’s resources in structural order. Locator pages list one resource’s locators together with the owning resource and the logical media root recorded when the locator was confirmed. This example drains the asset pages and, for brevity, reads the first page of each nested query:

static pp_error_code_t
print_resource_locators(const pp_production_t *production,
                        const pp_resource_id_t *resource_id,
                        pp_error_t **error) {
  pp_locator_query_set_t *locators = NULL;
  pp_error_code_t status = pp_production_locators_page(
      production, *resource_id, UINT32_C(100), NULL, &locators, error);
  for (uint64_t i = 0;
       status == PP_OK && i < pp_locator_query_set_count(locators); ++i) {
    pp_locator_id_t locator_id;
    pp_resource_id_t owner_id;
    const char *uri = NULL;
    pp_locator_availability_t availability;
    uint8_t has_last_seen = 0;
    int64_t last_seen = 0;
    const char *media_root = NULL;
    uint8_t has_naming = 0;
    pp_sequence_naming_t naming;
    status = pp_locator_query_set_get(
        locators, i, &locator_id, &owner_id, &uri, &availability,
        &has_last_seen, &last_seen, &media_root, &has_naming, &naming, error);
    if (status == PP_OK) {
      printf("%s (root: %s)\n", uri, media_root != NULL ? media_root : "-");
    }
  }
  pp_locator_query_set_release(locators);
  return status;
}

static pp_error_code_t print_asset_locators(const pp_production_t *production,
                                            const pp_asset_id_t *asset_id,
                                            pp_error_t **error) {
  /* Follow each nested cursor the same way in large productions. */
  pp_representation_set_t *representations = NULL;
  pp_error_code_t status = pp_production_representations_page(
      production, *asset_id, UINT32_C(100), NULL, &representations, error);
  for (uint64_t r = 0;
       status == PP_OK && r < pp_representation_set_count(representations);
       ++r) {
    pp_representation_id_t representation_id;
    pp_asset_id_t owner;
    pp_representation_kind_t kind;
    pp_content_structure_kind_t structure;
    uint64_t members, resources, fingerprints;
    pp_object_query_set_t *resource_page = NULL;
    status = pp_representation_set_get(representations, r, &representation_id,
                                       &owner, &kind, &structure, &members,
                                       &resources, &fingerprints, error);
    if (status == PP_OK) {
      status = pp_production_resources_page(production, representation_id,
                                            UINT32_C(100), NULL,
                                            &resource_page, error);
    }
    for (uint64_t s = 0;
         status == PP_OK && s < pp_object_query_set_count(resource_page); ++s) {
      pp_object_ref_t resource;
      uint32_t depth = 0;
      status =
          pp_object_query_set_get(resource_page, s, &resource, &depth, error);
      if (status == PP_OK) {
        pp_resource_id_t resource_id = {0};
        status = pp_object_ref_get_resource(&resource, &resource_id, error);
        if (status == PP_OK)
          status = print_resource_locators(production, &resource_id, error);
      }
    }
    pp_object_query_set_release(resource_page);
  }
  pp_representation_set_release(representations);
  return status;
}

static pp_error_code_t
print_recorded_locators(const pp_production_t *production,
                        pp_error_t **error) {
  char cursor_storage[2049] = {0};
  const char *cursor = NULL;
  pp_error_code_t status = PP_OK;
  while (status == PP_OK) {
    pp_asset_set_t *assets = NULL;
    status = pp_production_assets_page(production, UINT32_C(100), cursor,
                                       &assets, error);
    for (uint64_t i = 0; status == PP_OK && i < pp_asset_set_count(assets);
         ++i) {
      pp_asset_id_t asset_id;
      int64_t created_at = 0;
      const char *name = NULL;
      const char *import_source = NULL;
      status = pp_asset_set_get(assets, i, &asset_id, &created_at, &name,
                                &import_source, error);
      if (status == PP_OK) {
        status = print_asset_locators(production, &asset_id, error);
      }
    }
    /* The cursor borrows the page; copy it before releasing the page. */
    const char *next =
        status == PP_OK ? pp_asset_set_next_cursor(assets) : NULL;
    const int copied =
        next != NULL
            ? snprintf(cursor_storage, sizeof cursor_storage, "%s", next)
            : 0;
    pp_asset_set_release(assets);
    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;
}
void print_recorded_locators(const postproject::Production &production) {
  std::optional<std::string> cursor;
  do {
    const auto assets = production.assets(100, cursor).value();
    for (const auto &asset : assets.items) {
      // Follow each nested next_cursor the same way in large productions.
      for (const auto &representation :
           production.representations(asset.id, 100).value().items) {
        for (const auto &resource_id :
             production.resources(representation.id, 100).value().items) {
          const auto locators = production.locators(resource_id, 100).value();
          for (const auto &match : locators.items) {
            std::cout << match.locator.uri
                      << " (root: " << match.media_root.value_or("-") << ")\n";
          }
        }
      }
    }
    cursor = assets.next_cursor;
  } while (cursor.has_value());
}
def print_recorded_locators(production: Production) -> None:
    cursor = None
    while True:
        assets = production.assets_page(limit=100, cursor=cursor)
        for asset in assets.items:
            # Follow each nested next_cursor the same way in large productions.
            for representation in production.representations_page(
                asset.id, limit=100
            ).items:
                for resource_id in production.resources_page(
                    representation.id, limit=100
                ).items:
                    for match in production.locators_page(resource_id, limit=100).items:
                        print(f"{match.locator.uri} (root: {match.media_root or '-'})")
        cursor = assets.next_cursor
        if cursor is None:
            break
fn print_recorded_locators(production: &SqliteProduction) -> Result<()> {
    // Follow each nested next_cursor the same way in large productions.
    let first_page = || QueryPageRequest::new(100, None);
    let mut cursor = None;
    loop {
        let assets = production.assets_page(&QueryPageRequest::new(100, cursor)?)?;
        for asset in assets.items() {
            for representation in production
                .representations_page(asset.id(), &first_page()?)?
                .items()
            {
                for resource in production
                    .resources_page(representation.id(), &first_page()?)?
                    .items()
                {
                    for locator in production
                        .locators_page(resource.id(), &first_page()?)?
                        .items()
                    {
                        let root = locator.media_root().unwrap_or("-");
                        println!("{} (root: {root})", locator.uri());
                    }
                }
            }
        }
        cursor = assets.next_cursor().cloned();
        if cursor.is_none() {
            return Ok(());
        }
    }
}
CURSOR=
while :; do
  ARGS=(--json media list production.pproj --limit 100)
  [[ -n "$CURSOR" ]] && ARGS+=(--cursor "$CURSOR")
  PAGE=$(postproject "${ARGS[@]}")
  for ASSET in $(jq -r '.items[].id' <<<"$PAGE"); do
    # Follow each nested next_cursor the same way in large productions.
    for REPRESENTATION in $(postproject --json representation list \
      production.pproj "$ASSET" --limit 100 | jq -r '.items[].id'); do
      for RESOURCE in $(postproject --json representation resources \
        production.pproj "$REPRESENTATION" --limit 100 | jq -r '.items[].id'); do
        postproject --json locator list production.pproj "$RESOURCE" --limit 100 |
          jq -r '.items[] | "\(.uri) (root: \(.media_root // "-"))"'
      done
    done
  done
  CURSOR=$(jq -r '.next_cursor // empty' <<<"$PAGE")
  [[ -z "$CURSOR" ]] && break
done

A locator records a root only when it was confirmed under that root. Pass the candidate’s media root to pp_transaction_confirm_locator, C++ confirmLocator, Python confirm_locator, or Rust prepare_confirmed_locator, as the confirmation example does. The CLI media resolve --confirm records that root automatically. Imported locators, locators confirmed without a root, and locators stored before schema version 11 report no root; PostProject never infers one from a machine-local path.

C, C++, and Python resource pages return resource IDs; Rust returns complete resource values, and the CLI prints each resource’s ID, size, and fingerprints.

Query recorded media knowledge

Two media queries answer questions from what the production has recorded:

  • unresolved media lists representations with a required resource for which the production records no locator at all; and

  • representations under a media root lists representations with at least one locator recorded under the named logical root. The root must be configured in the production.

static pp_error_code_t list_media_knowledge(const pp_production_t *production,
                                            uint64_t *out_under_rushes,
                                            pp_error_t **error) {
  /* Both queries read recorded knowledge; neither touches the filesystem. */
  pp_object_query_set_t *unresolved = NULL;
  pp_representation_set_t *under_rushes = NULL;
  pp_error_code_t status = pp_production_unresolved_media(
      production, UINT32_C(100), NULL, &unresolved, error);
  if (status == PP_OK) {
    printf("representations without a recorded locator: %llu\n",
           (unsigned long long)pp_object_query_set_count(unresolved));
    status = pp_production_representations_under_media_root(
        production, "rushes", UINT32_C(100), NULL, &under_rushes, error);
  }
  if (status == PP_OK) {
    *out_under_rushes = pp_representation_set_count(under_rushes);
  }

  pp_representation_set_release(under_rushes);
  pp_object_query_set_release(unresolved);
  return status;
}
std::vector<postproject::RepresentationId>
list_media_knowledge(const postproject::Production &production) {
  // Both queries read recorded knowledge; neither touches the filesystem.
  const auto unresolved = production.unresolvedMedia(100).value();
  std::cout << "representations without a recorded locator: "
            << unresolved.items.size() << '\n';

  std::vector<postproject::RepresentationId> under_rushes;
  for (const auto &representation :
       production.representationsUnderMediaRoot("rushes", 100).value().items) {
    under_rushes.push_back(representation.id);
  }
  return under_rushes;
}
def list_media_knowledge(production: Production) -> tuple[RepresentationId, ...]:
    # Both queries read recorded knowledge; neither touches the filesystem.
    unresolved = production.unresolved_media(limit=100)
    for representation_id in unresolved.items:
        print(f"no recorded locator: {representation_id}")

    under_rushes = production.representations_under_media_root("rushes", limit=100)
    return tuple(representation.id for representation in under_rushes.items)
fn list_media_knowledge(production: &SqliteProduction) -> Result<Vec<RepresentationId>> {
    // Both queries read recorded knowledge; neither touches the filesystem.
    let request = QueryPageRequest::new(100, None)?;
    for representation_id in production.unresolved_media(&request)?.items() {
        println!("no recorded locator: {representation_id}");
    }

    let under_rushes = production.representations_under_media_root("rushes", &request)?;
    Ok(under_rushes
        .items()
        .iter()
        .map(Representation::id)
        .collect())
}
# Both queries read recorded knowledge; neither touches the filesystem.
postproject media unresolved production.pproj --limit 100
postproject --json media under-root production.pproj rushes --limit 100 |
  jq -r '.items[].id'

Both queries read recorded knowledge only. They never stat, open, or search the filesystem, and they do not map a root name to a local directory. A representation whose recorded locator points at a moved or deleted file is not unresolved, and a representation under a root may be offline on this machine. Current filesystem state stays with resolution and the non-mutating inventory scan (postproject media inventory).

Query metadata by property

A metadata query finds assertions of one exact vocabulary and property. An optional exact value restricts the page to one scalar value: text, a language-tagged string, an integer, a decimal, a rational, a boolean, a timestamp, a URI, bytes, or an object reference. The comparison is exact on the typed value, without case folding, numeric normalization, or comparison across value kinds, so a language-tagged string matches only the same text and language tag. A list or structure predicate is an invalid argument.

static pp_error_code_t find_interview_titles(const pp_production_t *production,
                                             uint64_t *out_count,
                                             pp_error_t **error) {
  pp_metadata_input_t *interview = NULL;
  pp_metadata_set_t *page = NULL;
  pp_error_code_t status =
      pp_metadata_input_create_string("Interview", "en-US", &interview, error);
  if (status == PP_OK) {
    /* Pass NULL instead of an exact value to match every value. */
    status = pp_production_query_metadata(
        production,
        "https://iptc.org/std/videometadatahub/recommendation/"
        "iptc-vmhub-1.7-schema.json",
        "title", interview, UINT32_C(100), NULL, &page, error);
  }
  if (status == PP_OK) {
    *out_count = pp_metadata_set_count(page);
    printf("exact title matches: %llu\n", (unsigned long long)*out_count);
  }

  pp_metadata_set_release(page);
  pp_metadata_input_release(interview);
  return status;
}
std::vector<postproject::ObjectRef>
find_interview_titles(const postproject::Production &production) {
  const auto page = production.queryMetadata(
      "https://iptc.org/std/videometadatahub/recommendation/"
      "iptc-vmhub-1.7-schema.json",
      "title", postproject::MetadataValue::languageString("Interview", "en-US"),
      100).value();
  std::cout << "exact title matches: " << page.items.size() << '\n';
  std::vector<postproject::ObjectRef> targets;
  for (const auto &assertion : page.items) {
    targets.push_back(assertion.target);
  }
  return targets;
}
def find_interview_titles(production: Production) -> tuple[ObjectReference, ...]:
    title = MetadataProperty(
        "https://iptc.org/std/videometadatahub/recommendation/iptc-vmhub-1.7-schema.json",
        "title",
    )
    page = production.query_metadata(
        title, limit=100, value=MetadataLanguageString("Interview", "en-US")
    )
    for assertion in page.items:
        print(f"{assertion.target}: {assertion.value}")
    return tuple(assertion.target for assertion in page.items)
fn find_interview_titles(production: &SqliteProduction) -> Result<Vec<ObjectRef>> {
    let title = MetadataProperty::new(
        VocabularyId::new(
            "https://iptc.org/std/videometadatahub/recommendation/iptc-vmhub-1.7-schema.json",
        )?,
        PropertyId::new("title")?,
    );
    let query = MetadataQuery::new(
        title,
        Some(MetadataValue::language_string("Interview", "en-US")?),
    )?;
    let page = production.metadata_query(&query, &QueryPageRequest::new(100, None)?)?;
    for matched in page.items() {
        println!("{:?}: {:?}", matched.target(), matched.assertion());
    }
    Ok(page.items().iter().map(MetadataMatch::target).collect())
}
cat > interview.json <<'EOF'
{"type": "lang_string", "value": "Interview", "language": "en-US"}
EOF
postproject --json metadata find production.pproj "$IPTC_VMHUB" title \
  --value-file interview.json --limit 100 |
  jq -r '.items[] | "\(.target_kind) \(.target_id)"'

To match every value, C passes NULL, C++ omits the value argument, Python omits value, and Rust passes None; the CLI omits --value-file. The CLI reads an exact value from the tagged JSON file passed with --value-file, using the shape shown in metadata vocabularies.

Query provenance

Producing and consuming activity pages return activities connected to one representation. Output queries return the distinct representations produced by activities of one exact kind, or by activities with one exact tool identity. Tool matching compares name, version, and URI; an absent version or URI matches only activities that recorded none.

Ancestor and descendant queries follow activity inputs or outputs. Like dependency traversal, they require a maximum depth from 1 through 64 and a visited-representation bound from 1 through 1,000, including the starting representation. Each match reports its shortest observed depth, and pages are ordered by representation ID.

static pp_error_code_t query_render_lineage(const pp_production_t *production,
                                            const pp_representation_id_t *source_id,
                                            const pp_representation_id_t *render_id,
                                            pp_error_t **error) {
  pp_activity_set_t *producing = NULL;
  pp_activity_set_t *consuming = NULL;
  pp_object_query_set_t *by_kind = NULL;
  pp_object_query_set_t *by_tool = NULL;
  pp_object_query_set_t *ancestors = NULL;
  pp_error_code_t status = pp_production_activities_producing_page(
      production, *render_id, UINT32_C(100), NULL, &producing, error);
  if (status == PP_OK) {
    status = pp_production_activities_consuming_page(
        production, *source_id, UINT32_C(100), NULL, &consuming, error);
  }
  if (status == PP_OK) {
    status = pp_production_outputs_by_activity_kind(
        production, "org.postproject:render", UINT32_C(100), NULL, &by_kind,
        error);
  }
  if (status == PP_OK) {
    /* The tool identity matches exactly; NULL matches an absent field. */
    status = pp_production_outputs_by_tool(
        production, "Example Renderer", "2.1", "https://example.com/renderer",
        UINT32_C(100), NULL, &by_tool, error);
  }
  if (status == PP_OK) {
    status = pp_production_provenance_ancestors_page(
        production, *render_id, UINT32_C(8), UINT32_C(1000), UINT32_C(100), NULL,
        &ancestors, error);
  }
  for (uint64_t i = 0;
       status == PP_OK && i < pp_object_query_set_count(ancestors); ++i) {
    pp_object_ref_t ancestor;
    uint32_t depth = 0;
    status = pp_object_query_set_get(ancestors, i, &ancestor, &depth, error);
    if (status == PP_OK) {
      printf("ancestor at depth %u\n", depth);
    }
  }
  if (status == PP_OK &&
      (pp_activity_set_count(producing) != UINT64_C(1) ||
       pp_activity_set_count(consuming) != UINT64_C(1) ||
       pp_object_query_set_count(by_kind) != UINT64_C(1) ||
       pp_object_query_set_count(by_tool) != UINT64_C(1) ||
       pp_object_query_set_traversal_truncated(ancestors) != 0)) {
    status = PP_ERROR_INTERNAL;
  }

  pp_object_query_set_release(ancestors);
  pp_object_query_set_release(by_tool);
  pp_object_query_set_release(by_kind);
  pp_activity_set_release(consuming);
  pp_activity_set_release(producing);
  return status;
}
void query_render_lineage(const postproject::Production &production,
                          const postproject::RepresentationId &source_id,
                          const postproject::RepresentationId &render_id) {
  const auto producing = production.activitiesProducing(render_id, 100).value();
  const auto consuming = production.activitiesConsuming(source_id, 100).value();
  require(producing.items.size() == 1 && consuming.items.size() == 1 &&
              producing.items.front().id == consuming.items.front().id,
          "render activity");

  const auto by_kind =
      production.outputsByActivityKind("org.postproject:render", 100).value();
  const auto by_tool = production.outputsByTool(
      {"Example Renderer", "2.1", "https://example.com/renderer"}, 100).value();
  require(by_kind.items == std::vector{render_id} &&
              by_tool.items == std::vector{render_id},
          "render outputs");

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

  const auto descendants = production.descendants(source_id, 8, 1000, 100).value();
  require(descendants.items.front().object == postproject::ObjectRef::representation(render_id),
          "render descends from its source");
}
def query_render_lineage(
    production: Production,
    source_id: RepresentationId,
    render_id: RepresentationId,
) -> None:
    producing = production.activities_producing_page(render_id, limit=100)
    consuming = production.activities_consuming_page(source_id, limit=100)
    assert producing.items == consuming.items

    by_kind = production.outputs_by_activity_kind("org.postproject:render", limit=100)
    by_tool = production.outputs_by_tool(
        ToolIdentity("Example Renderer", "2.1", "https://example.com/renderer"),
        limit=100,
    )
    assert by_kind.items == by_tool.items == (render_id,)

    ancestors = production.provenance_ancestors_page(
        render_id, max_depth=8, max_representations=1000, limit=100
    )
    for match in ancestors.items:
        print(f"ancestor {match.representation_id} at depth {match.depth}")
    assert not ancestors.traversal_truncated

    descendants = production.provenance_descendants_page(
        source_id, max_depth=8, max_representations=1000, limit=100
    )
    assert descendants.items[0].representation_id == render_id
fn query_render_lineage(
    production: &SqliteProduction,
    source_id: RepresentationId,
    render_id: RepresentationId,
) -> Result<()> {
    let request = QueryPageRequest::new(100, None)?;
    let producing = production.activities_producing_page(render_id, &request)?;
    let consuming = production.activities_consuming_page(source_id, &request)?;
    assert_eq!(producing.items(), consuming.items());

    let by_kind = production.activity_outputs(
        &ActivityOutputQuery::Kind(ActivityKind::new("org.postproject:render")?),
        &request,
    )?;
    let by_tool = production.activity_outputs(
        &ActivityOutputQuery::Tool(ToolIdentity::new(
            "Example Renderer",
            Some("2.1".to_owned()),
            Some("https://example.com/renderer".to_owned()),
        )?),
        &request,
    )?;
    assert_eq!(by_kind.items(), [render_id]);
    assert_eq!(by_tool.items(), [render_id]);

    let limits = ProvenanceQueryLimits::new(8, 1_000)?;
    let ancestors = production.ancestors_page(render_id, limits, &request)?;
    for item in ancestors.items() {
        println!(
            "ancestor {} at depth {}",
            item.representation_id(),
            item.depth()
        );
    }
    assert!(!ancestors.traversal_truncated());

    let descendants = production.descendants_page(source_id, limits, &request)?;
    assert_eq!(descendants.items()[0].representation_id(), render_id);
    Ok(())
}
postproject activity producing production.pproj "$SEQUENCE_ID" --limit 100
postproject activity consuming production.pproj "$ORIGINAL_ID" --limit 100
postproject activity outputs production.pproj --kind org.postproject:render
postproject activity outputs production.pproj --tool-name "Example Renderer" \
  --tool-version 2.1 --tool-uri https://example.com/renderer
postproject --json activity ancestors production.pproj "$SEQUENCE_ID" \
  --max-depth 8 --max-representations 1000 --limit 100 |
  jq '{items, traversal_truncated}'
postproject activity descendants production.pproj "$ORIGINAL_ID" \
  --max-depth 8 --max-representations 1000 --limit 100

The CLI traversal commands default to --max-depth 64 and --max-representations 1000.

Find stale artifacts

A stale-artifact query evaluates activity outputs as artifact evaluation does, with the supplied depth and representation bounds applied to each evaluation, and returns those currently evaluated as stale. An optional source representation restricts the candidates to its provenance descendants.

static pp_error_code_t
count_stale_descendants(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_object_query_set_t *page = NULL;
    status = pp_production_stale_artifacts(production, source_id, UINT32_C(64),
                                           UINT32_C(1000), UINT32_C(100),
                                           cursor, &page, error);
    /* A page bounds the candidates examined, so it may hold fewer stale
     * results, or none, and still carry a continuation. */
    *out_count += status == PP_OK ? pp_object_query_set_count(page) : 0;
    const char *next =
        status == PP_OK ? pp_object_query_set_next_cursor(page) : NULL;
    const int copied =
        next != NULL
            ? snprintf(cursor_storage, sizeof cursor_storage, "%s", next)
            : 0;
    pp_object_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::RepresentationId>
stale_descendants(const postproject::Production &production,
                  const postproject::RepresentationId &source_id) {
  std::vector<postproject::RepresentationId> stale;
  std::optional<std::string> cursor;
  do {
    const auto page =
        production.staleArtifacts(64, 1000, 100, cursor, source_id).value();
    // A page bounds the candidates examined, so it may hold fewer stale
    // results, or none, and still carry a continuation.
    stale.insert(stale.end(), page.items.begin(), page.items.end());
    cursor = page.next_cursor;
  } while (cursor.has_value());
  return stale;
}
def stale_descendants(
    production: Production, source_id: RepresentationId
) -> list[RepresentationId]:
    stale: list[RepresentationId] = []
    cursor = None
    while True:
        page = production.stale_artifacts(
            max_depth=64,
            max_representations=1000,
            limit=100,
            cursor=cursor,
            source=source_id,
        )
        # A page bounds the candidates examined, so it may hold fewer stale
        # results, or none, and still carry a continuation.
        stale.extend(page.items)
        cursor = page.next_cursor
        if cursor is None:
            return stale
fn stale_descendants(
    production: &SqliteProduction,
    source_id: RepresentationId,
) -> Result<Vec<RepresentationId>> {
    let query = StaleArtifactQuery::new(Some(source_id), ArtifactEvaluationLimits::new(64, 1_000)?);
    let mut stale = Vec::new();
    let mut cursor = None;
    loop {
        let page = production.stale_artifacts(query, &QueryPageRequest::new(100, cursor)?)?;
        // A page bounds the candidates examined, so it may hold fewer stale
        // results, or none, and still carry a continuation.
        stale.extend_from_slice(page.items());
        cursor = page.next_cursor().cloned();
        if cursor.is_none() {
            return Ok(stale);
        }
    }
}
CURSOR=
while :; do
  ARGS=(--json artifact stale production.pproj --source "$ORIGINAL_ID"
    --max-depth 64 --max-representations 1000 --limit 100)
  [[ -n "$CURSOR" ]] && ARGS+=(--cursor "$CURSOR")
  PAGE=$(postproject "${ARGS[@]}")
  # A page bounds the candidates examined, so it may hold fewer stale
  # results, or none, and still carry a continuation.
  jq -r '.items[].representation_id' <<<"$PAGE"
  CURSOR=$(jq -r '.next_cursor // empty' <<<"$PAGE")
  [[ -z "$CURSOR" ]] && break
done

A stale-artifact page bounds the number of candidate outputs examined, not only the number returned. A page can therefore contain fewer stale results than its page size, or none, and still carry a continuation; keep following the cursor until none is returned. With a source, traversal_truncated reports that the descendant traversal reached its bound and candidates may be missing. The CLI artifact stale command defaults to --max-depth 64 and --max-representations 1000.

List objects changed since a revision

The changed-object query returns each distinct production, asset, representation, resource, activity, or job touched by a revision after the supplied sequence, derived from the semantic event journal. Media-root events report the production, and locator events report the owning resource.

static pp_error_code_t
object_changed_after(const pp_production_t *production, uint64_t sequence,
                     const pp_object_ref_t *object, uint8_t *out_changed,
                     pp_error_t **error) {
  char cursor_storage[2049] = {0};
  const char *cursor = NULL;
  pp_error_code_t status = PP_OK;
  *out_changed = 0;
  while (status == PP_OK) {
    pp_object_query_set_t *page = NULL;
    status = pp_production_objects_changed_since(
        production, sequence, UINT32_C(100), cursor, &page, error);
    for (uint64_t i = 0;
         status == PP_OK && i < pp_object_query_set_count(page); ++i) {
      pp_object_ref_t changed;
      uint32_t depth = 0;
      status = pp_object_query_set_get(page, i, &changed, &depth, error);
      if (status == PP_OK && changed.kind == object->kind &&
          memcmp(&changed.id, &object->id, sizeof changed.id) == 0) {
        *out_changed = 1;
      }
    }
    const char *next =
        status == PP_OK ? pp_object_query_set_next_cursor(page) : NULL;
    const int copied =
        next != NULL
            ? snprintf(cursor_storage, sizeof cursor_storage, "%s", next)
            : 0;
    pp_object_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::ObjectRef>
objects_changed_after(const postproject::Production &production,
                      std::uint64_t sequence) {
  std::vector<postproject::ObjectRef> changed;
  std::optional<std::string> cursor;
  do {
    const auto page = production.objectsChangedSince(sequence, 100, cursor).value();
    changed.insert(changed.end(), page.items.begin(), page.items.end());
    cursor = page.next_cursor;
  } while (cursor.has_value());
  return changed;
}
def objects_changed_after(
    production: Production, sequence: int
) -> list[ObjectReference]:
    changed: list[ObjectReference] = []
    cursor = None
    while True:
        page = production.objects_changed_since(sequence, limit=100, cursor=cursor)
        changed.extend(page.items)
        cursor = page.next_cursor
        if cursor is None:
            return changed
fn objects_changed_after(production: &SqliteProduction, sequence: u64) -> Result<Vec<ObjectRef>> {
    let mut changed = Vec::new();
    let mut cursor = None;
    loop {
        let page =
            production.objects_changed_since(sequence, &QueryPageRequest::new(100, cursor)?)?;
        changed.extend_from_slice(page.items());
        cursor = page.next_cursor().cloned();
        if cursor.is_none() {
            return Ok(changed);
        }
    }
}
postproject --json revisions changed production.pproj \
  --after "$BEFORE_RENDER" --limit 100 |
  jq -r '.items[] | "\(.kind) \(.id)"'

Use it to refresh cached objects after reconnecting. It answers what changed, not how; read the revision feed when event order and meaning matter.

Traverse dependencies

Forward queries start at a representation. Reverse queries start at an asset or representation and return the representations that depend on it. Both directions require a maximum depth and a visited-representation bound. Each match includes its shortest observed depth.

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

traversal_truncated and next_cursor answer different questions. A truncated traversal stopped at a graph bound and may omit matches. A next cursor means another output page exists within the graph that was actually traversed.

Filter and page jobs

Job queries accept optional exact state and open-world kind filters. This example requests two jobs, then drains one-item pages using the same filters on every call:

static pp_error_code_t request_and_page_jobs(pp_production_t *production,
                                             const pp_representation_id_t *input_id,
                                             const pp_asset_id_t *output_asset_id,
                                             pp_error_t **error) {
  const char *kind = "org.example:generate-proxy";
  pp_transaction_t *transaction = NULL;
  pp_job_id_t job_ids[2];
  pp_error_code_t status =
      pp_production_begin_edit(production, NULL, NULL, &transaction, error);
  for (uint32_t i = 0; status == PP_OK && i < UINT32_C(2); ++i) {
    status = pp_transaction_request_job(
        transaction, kind, input_id, UINT64_C(1), *output_asset_id,
        PP_REPRESENTATION_PROXY, NULL, &job_ids[i], error);
  }
  if (status == PP_OK) {
    pp_commit_receipt_t commit_receipt;
    status = pp_transaction_commit(transaction, &commit_receipt, error);
  }
  pp_job_set_t *first_job = NULL;
  pp_job_t job = {0};
  if (status == PP_OK) {
    status = pp_production_job(production, job_ids[0], &first_job, error);
  }
  if (status == PP_OK) {
    status = pp_job_set_get(first_job, UINT64_C(0), &job, error);
  }
  if (status == PP_OK && job.state != PP_JOB_REQUESTED) {
    status = PP_ERROR_INTERNAL;
  }
  pp_job_set_release(first_job);

  char cursor_storage[2049] = {0};
  const char *cursor = NULL;
  uint64_t count = 0;
  while (status == PP_OK) {
    pp_job_set_t *page = NULL;
    status = pp_production_jobs(production, PP_JOB_REQUESTED, kind,
                                UINT32_C(1), cursor, &page, error);
    count += status == PP_OK ? pp_job_set_count(page) : 0;
    const char *next =
        status == PP_OK ? pp_job_set_next_cursor(page) : NULL;
    if (next == NULL) {
      pp_job_set_release(page);
      break;
    }
    const int copied = snprintf(cursor_storage, sizeof cursor_storage, "%s", next);
    pp_job_set_release(page);
    if (copied < 0 || (size_t)copied >= sizeof cursor_storage) {
      status = PP_ERROR_INTERNAL;
      break;
    }
    cursor = cursor_storage;
  }
  if (status == PP_OK && count != UINT64_C(2)) {
    status = PP_ERROR_INTERNAL;
  }

  pp_transaction_release(transaction);
  return status;
}
void request_and_page_jobs(postproject::Production &production,
                           const postproject::RepresentationId &input_id,
                           const postproject::AssetId &output_asset_id) {
  const postproject::JobRequest request{
      "org.example:generate-proxy", {input_id}, output_asset_id,
      postproject::RepresentationKind::proxy, std::nullopt};
  auto transaction = production.edit().value();
  const auto job_id = transaction.requestJob(request).value();
  transaction.requestJob(request).value();
  transaction.commit().value();
  require(production.job(job_id).value().stateKind() == postproject::JobState::requested,
          "requested job read back by identity");

  std::optional<std::string> cursor;
  std::size_t count = 0;
  do {
    const auto page = production.jobs(
        1, cursor, postproject::JobState::requested,
        std::string_view("org.example:generate-proxy")).value();
    count += page.items.size();
    cursor = page.next_cursor;
  } while (cursor.has_value());
  require(count == 2, "two requested proxy jobs");
}
def request_and_page_jobs(
    production: Production,
    input_id: RepresentationId,
    output_asset_id: AssetId,
) -> None:
    request = JobRequest(
        "org.example:generate-proxy",
        (input_id,),
        output_asset_id,
        RepresentationKind.PROXY,
    )
    with production.edit() as transaction:
        job_id = transaction.request_job(request)
        transaction.request_job(request)
        transaction.commit()
    assert production.job(job_id).state is JobState.REQUESTED

    cursor = None
    count = 0
    while True:
        page = production.jobs(
            limit=1,
            cursor=cursor,
            state=JobState.REQUESTED,
            kind="org.example:generate-proxy",
        )
        count += len(page.items)
        cursor = page.next_cursor
        if cursor is None:
            break
    assert count == 2
fn request_and_page_jobs(
    production: &mut SqliteProduction,
    input_id: RepresentationId,
    output_asset_id: AssetId,
) -> Result<()> {
    let kind = JobKind::new("org.example:generate-proxy")?;
    let output = RequestedJobOutput::new(output_asset_id, RepresentationKind::Proxy, None)?;
    let requested = [
        Job::new(JobId::new(), kind.clone(), vec![input_id], output.clone())?,
        Job::new(JobId::new(), kind.clone(), vec![input_id], output)?,
    ];
    {
        let mut transaction = production.begin_transaction()?;
        for job in &requested {
            transaction.request_job(job)?;
        }
        transaction.commit()?;
    }
    assert_eq!(production.job(requested[0].id())?, requested[0]);

    let query = JobQuery::new(Some(JobStateKind::Requested), Some(kind));
    let mut cursor = None;
    let mut count = 0;
    loop {
        let page = production.jobs(&query, &QueryPageRequest::new(1, cursor)?)?;
        count += page.items().len();
        cursor = page.next_cursor().cloned();
        if cursor.is_none() {
            break;
        }
    }
    assert_eq!(count, 2);
    Ok(())
}
JOB_ID=$(postproject --json job request production.pproj \
  org.example:generate-proxy "$ASSET_ID" proxy --input "$ORIGINAL_ID" | jq -r .id)
postproject job request production.pproj org.example:generate-proxy \
  "$ASSET_ID" proxy --input "$ORIGINAL_ID"
postproject --json job show production.pproj "$JOB_ID" | jq -r .state
CURSOR=
while :; do
  ARGS=(--json job list production.pproj --state requested \
    --kind org.example:generate-proxy --limit 1)
  [[ -n "$CURSOR" ]] && ARGS+=(--cursor "$CURSOR")
  PAGE=$(postproject "${ARGS[@]}")
  jq '.items[] | {id, kind, state}' <<<"$PAGE"
  CURSOR=$(jq -r '.next_cursor // empty' <<<"$PAGE")
  [[ -z "$CURSOR" ]] && break
done

Cursor ownership

The C cursor borrows its result-set handle and must be copied before releasing that handle. C++, Python, and Rust return owned cursor values. The CLI emits the cursor as next_cursor in its JSON page object.

Passing a cursor to another named query, root, filter set, page size, or traversal bound is an invalid argument. Treat cursor contents as private and do not parse or synthesize them.