Querying provenance

Trace lineage from chat, MCP tools, sandbox code, or the REST API.

There are three ways to interrogate the provenance graph: ask the assistant in plain language, call the tools yourself from an MCP client or from code running in the sandbox, or hit the REST API. All three walk the same server-recorded trails — this page shows the effective patterns for each.

Asking in plain language

The in-app assistant (and any connected MCP client) traces lineage with the provenance_read tool under the hood, so questions like these just work:

  • "Where did this figure come from?"
  • "What would a recall of lot 24-088 touch downstream?"
  • "Show me everything that depends on the Animals table."
  • "Was any AI involved in producing this report, and was it reviewed?"

The assistant walks the recorded graph — it cannot substitute its own memory of what it did for what the server observed.

The provenance_read tool

One read-only tool, eight actions. Pick by the question:

ActionQuestion it answers
originWhere did this come from? (walk backward)
impactWhat is downstream if this is wrong? (walk forward — the retraction tool)
runWhat did this one run do?
cellsWhat happened in this code session, cell by cell?
verifyDo the integrity checks pass over this closure?
exportRender the closure as an auditor-ready document.
review_queueWhich AI-authored outputs still owe a human review?
review_policyDoes this workspace gate exports on review?

The seeding rule: pass the entity's id and nothing else about its type — the backend resolves whether it is a file, document, object, or table from the id alone. Omit version_key to walk every version ever recorded.

# Where did this artifact come from?
provenance_read(action="origin", entity_id="<artifact-file-id>")

# Retraction analysis: everything downstream of a suspect source
provenance_read(action="impact", entity_id="<source-table-id>")

# One run's recorded activity; a session's sealed cells in order
provenance_read(action="run", run_id="<run-id>")
provenance_read(action="cells", session_id="<session-id>")

Every edge in the response carries quality (verified / declared / inferred) and precision (observed-exact / coarse) — read them, don't treat all edges as equal trust. A coarse edge is a deliberate over-approximation; a declared input is someone's statement, not an observation.

# Integrity verdict only — structural checks plus the honest `anchored` flag
provenance_read(action="verify", entity_id="<file-id>")

# Render and store an audit bundle (lands in your "Provenance Exports" folder)
provenance_read(
    action="export", entity_id="<file-id>", format="biocompute",
    context_of_use="Supporting evidence for IND section 5.3.",
)

verify returns only the verdict — check anchored before relying on it: the structural checks detect rewrites inside the chain, but only external anchoring rules out truncation of the tail. export stores the file rather than inlining it (a submission-sized closure doesn't fit a tool result) and returns {fileId, preview, scope, verification, aiInvolvement}.

An empty edge list can be truncation, not absence

Deep walks trim by whole depth layers when they exceed the result cap. Before reading a short or empty edges array as "no lineage", check data.truncated and data.truncatedAtDepth. To go deeper, re-seed the walk with a node_id from the deepest returned hop.

Data pulls from code

When code in the sandbox pulls workspace data, each call is recorded as a child Data pull run of the cell — the exact query plan, content-addressed, plus a hash over the full result before any truncation. If the pull cannot be recorded, the call fails: no evidence, no data. Large pulls group their touched objects into a per-origin cohort with an exact count, so a thousand-row pull stays legible in the trail.

One boundary to know: a cell's own writes enter the graph when the cell seals, so an execution cannot see itself in its own trail — but its data pulls are already recorded, and a later cell (or action="cells") can audit them.

Recording external runs

Computation that happens outside Dalea — an AlphaFold fold, an RNA-seq pipeline — can still enter the graph honestly. The rule: Dalea can only grade as verified what it witnessed, and it witnesses the release of bytes (your fetch), never the external computation. So the loop is fetch → cite → deposit:

  1. Fetch each input through Dalea (download_file, or a query execution). The response carries a receipt_id — a signed egress receipt binding the exact version and content hash to that release.
  2. Run your external tool on the returned bytes.
  3. Deposit the result with record_external_run, citing the receipts.
download_file(file_id="<input-file-id>")
# → { receipt_id: "r1", egress_receipt: {minted: true}, content: … }

record_external_run(
    external_system_id="claude-science", external_run_id="af-2026-08-19-001",
    tool_name="alphafold", tool_version="2.3.2",
    output_filename="predicted.pdb", output_media_type="chemical/x-pdb",
    output_bytes_base64="<base64>",
    cited_receipts=["r1"],                 # ← becomes a VERIFIED input edge
    determinism="nondeterministic", outputs_complete="claimed",
)
# → { runId, outputFileId, replayed, verifiedInputs }

cited_receipts become real, verified input edges. Inputs you did not fetch through Dalea go in declared_inputs instead — recorded, but shown in the trail as "claimed, not witnessed", never as verified lineage. Never invent a receipt id; a receipt you did not receive is rejected on deposit.

The deposit is idempotent on (external_system_id, external_run_id): recording the same pair twice returns the same run without duplicating the artefact.

REST API

The same walks are available over HTTP for pipelines and integrations. Seed a trail with an entity, node, or run reference:

const res = await fetch('https://dalea.app/api/v1/provenance/trail', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${token}`,
    'X-Workspace': workspaceId,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    ref: { kind: 'entity', entityId: artifactId },   // type resolved from the id
    direction: 'origin',
    maxDepth: 20,
  }),
});
const { nodes, runs, edges, truncated } = await res.json();

Two neighbours worth knowing: POST /api/v1/provenance/neighborhood returns the complete first-degree neighbourhood of a seed — every run that read or wrote it, consumers included, which a directional trail deliberately omits — and POST /api/v1/provenance/overview serves the workspace-wide run feed the Provenance page renders. The full schemas are in the live OpenAPI reference.

Behaviour to code against: an in-workspace entity with no provenance returns 200 with an empty trail (not an error), a seed you cannot see returns 404, and failed runs are excluded unless you pass includeFailed: true.

Verifying a receipt offline

An auditor with no Dalea access can verify an egress receipt years later:

  1. Fetch the receipt: GET /api/v1/receipts/{receipt_id}{payload, signatureB64, keyId}. The payload binds workspace, entity, version, content hash, and recipient.
  2. Fetch the public keys from GET /.well-known/dalea/egress/jwks.json (unauthenticated) and pick the JWK whose kid matches keyId.
  3. Re-canonicalise the payload (sorted-key compact JSON) and verify the Ed25519 signature.
  4. Match the receipt's version and content hash against the receipt_input edge in the artefact's origin trail.

A tampered payload fails step 3; a receipt pinned to a superseded version still verifies, honestly, as a pin to that version.

What's next