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, nine 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?
reviewsWho signed off on this run, and what did they attest? (seed with run_id)

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>")

Read the grades on each edge rather than treating them as equal trust. Every edge carries quality (verified / declared / inferred). derivation edges carry precision (observed-exact / coarse) as well; io edges, a run's plain reads and writes and the bulk of any trail, carry quality only. 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. A large pull pins a deterministic head/tail sample of the object versions it served, capped at 200 refs, and carries the exact pre-cap total on the run, so a thousand-row pull stays legible without the sample pretending to be the whole set.

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 real, receipt-backed edge
    determinism="nondeterministic", outputs_complete="claimed",
)
# → { runId, outputFileId, replayed, verifiedInputs }

cited_receipts become real input edges in the graph. On the wire they carry quality: "declared" with precision: "observed-exact": the signed receipt is the evidence, not a derivation Dalea watched happen. Inputs you did not fetch through Dalea go in declared_inputs instead: recorded as a facet claim and shown in the trail as "claimed, not witnessed", never as an edge at all. Never invent a receipt id; a bad or unknown receipt rejects the whole 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 who already holds a receipt document can verify it years later with no Dalea account: the only call the check itself needs is the public key fetch. Getting the receipt out of Dalea, and matching it back against a trail, are authenticated steps the depositor performs.

  1. Read the receipt: GET /api/v1/receipts/{receipt_id} → {receipt: {payload, signatureB64, keyId}, citations: [{runId, at}]}. The payload binds workspace, entity, version, content hash, byte length and recipient; citations is the append-only history of runs that cited it. This call is authenticated and workspace-scoped, so hand the receipt object to the auditor.
  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. Steps 2 and 3 need nothing but the receipt document and the published keys.
  4. Match the receipt's version and content hash against the receipt_input edge in the artefact's origin trail. That is an authenticated trail read.

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