# HTTP API & SPARQL protocol

Source: https://docs.littlebigbrain.com/sdks/http-api/

Every SDK wraps one HTTP API. Call it directly from any language. Standard
SPARQL clients connect to the same stack through the SPARQL 1.1 Protocol.

The TypeScript and Python SDKs are generated from the same API contract. Their
request and response shapes match. To see the calls in order, run the
[quickstart](https://docs.littlebigbrain.com/start/quickstart/) with its cURL tabs.

The examples use the quickstart's `support` graph of tickets, bugs and
releases. The RDF examples use a second graph, `catalog`.

The generated contract also includes experimental `/v1/cdc/*` capture/apply
routes. They remain disabled by default and require a reviewed server rollout;
they are not yet a hosted PostgreSQL connector. Their short-lived worker tokens
are role- and source-scoped and cannot be used for ordinary graph or workflow
requests. Captured, graph-applied and published positions are separate: capture
acknowledgement alone does not imply query visibility.
The experimental capture and apply modes are independent. A capture pause
fences new source writes while allowing already captured batches to reach the
graph. An interrupted snapshot requires a new attempt; resume never guesses a
replacement snapshot boundary.

## Discover the APIs and specifications

Fetch [the API catalog](https://littlebigbrain.com/.well-known/api-catalog)
to discover the graph/search API and the developer integrations API. It returns
`application/linkset+json`, with `service-desc` links to OpenAPI 3.1 JSON,
`service-doc` links to documentation and `status` links to public health checks.
The homepage's `Link` response header also advertises the catalog.

The [graph/search specification](https://littlebigbrain.com/openapi/data-plane.json)
contains the same paths and schemas used to generate the SDKs. Its server
variables are documentation examples: use your complete stack endpoint from
**Connect** for actual requests. The catalog's EU service anchor identifies
the data-plane service; its health link reports the cell's status.
The [integrations specification](https://littlebigbrain.com/openapi/integrations.json)
describes the `/v1/integrations/*` routes on `api.littlebigbrain.com`; see
[integrations for your customers](https://docs.littlebigbrain.com/guides/integrations-for-your-customers/).

## Conventions

- **Base URL.** Use `$LBB_URL`, the complete stack endpoint from the console's
  **Connect** view. It has the form
  `https://<tenant-short-id>--<stack-slug>.db.eu.littlebigbrain.com`. See
  [Base URLs](https://docs.littlebigbrain.com/start/authentication/#base-urls).
- **Auth.** Send `Authorization: Bearer $LBB_API_KEY` on every request.
  `GET /version` needs no key.
- **Graph.** `?graph=<name>` selects a graph in the stack. `?graph_id=` is an
  alias, and the default is `main`. A name has 1 to 32 characters: lowercase
  letters, digits, `-` and `_`. It starts with a letter or a digit.
- **One history per graph.** A graph has no branches. `branch=main` is
  accepted. Any other `branch` or `branch_id` value returns
  `400 branch_not_supported`.
- **Version.** Prepared source contract `2026-10-27` is unreleased; inspect
  `GET /version` for the running binary. Send `Lbb-Version: 2026-10-27` to pin
  this prepared contract. Matching source SDKs send it on every request. The
  advertised dates are `2026-07-23`, `2026-10-21` and `2026-10-27`. A request without the header gets the current
  contract. A request with a date before `2026-10-21` gets the list shapes
  of `2026-07-23` (see [Lists](#lists)). The value is a date `YYYY-MM-DD`;
  anything else answers `400 invalid_api_version`.
- **Content type.** Requests and responses are JSON, except on the RDF and
  SPARQL routes. Those take their own media types.
- **Compression.** Send `Accept-Encoding: zstd` or `gzip` to get compressed
  responses. Most HTTP clients do this for you. Bodies under 1 KiB stay plain.
- **Body size.** A JSON body can be up to 1 MiB. NDJSON import, RDF import,
  SPARQL and the Graph Store route take up to 64 MiB. A durable import job
  takes up to 32 GiB. A larger body returns `400`. A file upload takes up to
  1 GiB by default; a larger one returns `413 file_too_large`.

## Idempotency

An `Idempotency-Key` header makes a write safe to retry. A retry with the same
key returns the first result and writes nothing new. The response marks it as
a replay.

- These routes require a key: `POST /v1/graph/commit` (except with
  `?dry_run=true`), `POST /v1/graph/import-jobs` and
  `POST /v1/ontology/drafts/promote`. Without one they return
  `400 missing_idempotency_key`.
- Other writes, such as RDF import, NDJSON import and SPARQL Update, accept an
  optional key. Without one, the server makes a key for that request.
- A key has 1 to 255 characters: letters, digits, `-`, `_`, `.`, `:` and `/`.
  Any other key returns `400 invalid_idempotency_key`.
- A commit that reuses a key returns the first result, even when its body
  changed. Use a new key for a new payload.
- An import job whose key is bound to other content returns
  `409 idempotency_conflict`.

The SDKs add a key when you pass none.

## Lists

A list route returns one page in the list envelope:

```json
{
  "object": "list",
  "data": [{ "object": "workflow", "name": "notes", "created": "2026-10-21T09:00:00Z" }],
  "has_more": true,
  "next_cursor": "notes"
}
```

`data` holds the items of the page. `has_more` says whether more follow.
Pass `next_cursor` as `cursor` for the next page; it is absent on the last
page. `limit` sets the page size. Treat a cursor as an opaque string.

The workflow lists use the envelope: `GET /v1/workflows/named`, its
`runs`, `queue` and `documents`, and `GET /v1/workflows/instances/runs`. A list can add fields of its own beside
the envelope: `total` on the documents, the queue's `pending` and `shards`.
The SDKs page through each one (`all()`, `allRuns()` and `allDocuments()` in
TypeScript; `all()`, `all_runs()` and `all_documents()` in Python).

A request with an `Lbb-Version` before `2026-10-21` (`2026-07-23` and
earlier) gets these lists in the shapes of its date. The items are in
`workflows`, `runs`, `items` or `documents`. The cursor is `next_before` (a
run id, passed as `before`) or `next_after` (passed as `after`; a number on
the queue). These requests take `before` and `after` as well as `cursor`.

## Responses and errors

Every response carries these headers when they apply:

| Header | Meaning |
| --- | --- |
| `X-Request-Id` | The id of this request. Quote it when you report a problem. |
| `Retry-After` | Seconds to wait before you retry a retryable error. |
| `X-LBB-Throttle-Ms` | Set on a write that waited in a queue. See [Back-pressure](#back-pressure). |
| `Lbb-Version` | The API version marker, the same value as `version` from `GET /version`. |
| `Lbb-Build-Commit` | The build of the server that answered. |
| `Server-Timing` | Timings of the request's stages, on routes that report them. |

An error has a JSON body with one `error` object:

```json
{
  "error": {
    "type": "invalid_request_error",
    "code": "missing_idempotency_key",
    "message": "missing Idempotency-Key header",
    "param": "Idempotency-Key",
    "request_id": "89ccd5ab-e545-4744-9909-769b393bb6d1",
    "doc_url": "https://docs.littlebigbrain.com/sdks/errors/#missing_idempotency_key"
  }
}
```

Handle errors by `code`. `doc_url` links the code's entry on
[Error codes](https://docs.littlebigbrain.com/sdks/errors/), which lists every code with how to fix it.
`param` names the field, query parameter or header to change, when the
error has one. A retryable error also carries `retryable: true` and
`retry_after_seconds`. A path that matches no route returns `404 not_found`
with `did_you_mean`, a list of close routes. The SDKs raise this body as
`LbbError`.

| Status | `code` | Meaning | Retry |
| --- | --- | --- | --- |
| 400 | `invalid_request` | The request is malformed. The `message` names the problem. | No |
| 400 | `missing_idempotency_key`, `invalid_idempotency_key` | See [Idempotency](#idempotency). | No |
| 400 | `branch_not_supported` | The request names a branch other than `main`. | No |
| 400 | `rdf_native_graph` | A JSON record write on an RDF-native graph. See [RDF-native graphs](#rdf-native-graphs). | No |
| 400 | `historical_generation_unavailable` | The pinned commit is not retained. See [Time travel](#time-travel). | No |
| 400 | `scan_too_wide`, `result_too_large` | The query reads or returns too much. Narrow it. | No |
| 400 | `invalid_ask_request` | A field of `/v1/query/ask` is outside its limits, or belongs to the removed one-shot mode (`run`, `previous`, `mode: "rewrite"`). `param` names it. | No |
| 400 | `invalid_names_request`, `invalid_describe_request`, `invalid_commit_at_request`, `invalid_compare_request` | A field of `/v1/query/names`, `/v1/query/describe`, `/v1/graph/commit-at` or `/v1/query/compare` is outside its limits. `param` names it. | No |
| 400 | `commit_not_found` | `/v1/query/compare`: the server cannot place a date or a moment (before the first commit, or commits without a time). | No |
| 401 | `unauthorized` | The key is missing, wrong or revoked. | No |
| 403 | `stack_endpoint_mismatch` | Hosted stacks: the key belongs to another stack. Copy the endpoint and key from the same stack. | No |
| 403 | `write_outside_owns` | A commit under a workflow turn writes a property its workflow does not own. Or it replaces the whole property set of an entity. `details` names the IRIs. Nothing was written. | No |
| 404 | `not_found` | No such graph, record, job or route. | No |
| 409 | `idempotency_conflict` | The key is bound to different import content. | No |
| 409 | `conflict` | A write precondition failed, or the resource already exists. | No |
| 409 | `embedding_not_ready` | The embedding's first version still builds. | Yes |
| 409 | `workflow_fence_lost` | A commit's `workflow_fence` no longer holds its turn: the lease ended or another worker holds it. Nothing was written. | No |
| 409 | `turn_lease_lost` | A turn route's `token` no longer holds the turn: another attempt holds it, its lease ended, or it finished. Stop this attempt. | No |
| 410 | `pagination_cursor_expired` | A SPARQL page cursor can no longer be served. Restart the listing. | Restart |
| 421 | `stack_endpoint_required` | Hosted stacks: the host is missing or malformed. | No |
| 429 | `server_busy`, `ingest_busy` | The server or the graph is busy. See [Back-pressure](#back-pressure). | Yes |
| 429 | `read_your_writes_pending`, `strong_read_pending` | The read waits for a commit. See [Read consistency](#read-consistency). | Yes |
| 429 | `index_busy` | An index build for this graph is running, or the server is at its build limit. | Yes |
| 429 | `embedding_budget_exceeded` | The monthly model budget is spent. | After the reset date |
| 429 | `rewrite_limit` | The environment used its questions of the UTC day on this server (`/v1/query/ask`). | The next UTC day |
| 503 | `query_deadline_exceeded` | The read ran past its deadline. | Yes |
| 503 | `storage_degraded` | Object storage is unreachable. | Yes |
| 503 | `rewrite_model_unavailable` | The model of `/v1/query/ask` did not answer, or the server has none. | Later |

## Routes

Routes that read or write a graph take `?graph=`. The sections below and the
linked guides describe the main routes in more detail.

### Stack and graphs

| Method & path | Purpose |
| --- | --- |
| `GET /version` | Build identity, API version and capability list. No key needed. |
| `GET /v1/status` | Server and graph status. `graph_status.truth` is `property_graph` or `rdf_native`. |
| `GET /v1/usage` | Usage counters for your stack. |
| `GET /v1/graphs` | Lists the graphs in the stack. Takes `?limit=` and `?cursor=`. |
| `POST /v1/graph/create` | Creates the graph with an empty schema. `POST /v1/ontology/define` also creates it. |
| `POST /v1/graph/fork?src=<graph>&dst=<new>&confirm=<new>` | Copies a graph to a new graph id in the same stack, as a background job. `dst` must be a new graph id. The id of a deleted graph is refused with `409`. The copy keeps the data, its history, the schema and the shapes. It also keeps the embeddings with their vectors and the rewrite profile. It publishes and takes writes on its own. Poll `GET /v1/graph/metadata?graph=<new>`. |
| `POST /v1/graph/delete?graph=<graph>&confirm=<graph>` | Deletes the graph. Safe to repeat. Storage is freed in the background. A graph created again under the same name starts empty, and late work from the deleted graph cannot change it. |
| `GET /metrics` | Prometheus metrics for the people who run the servers. A stack key gets `401` on hosted stacks. |

### Write

| Method & path | Purpose |
| --- | --- |
| `POST /v1/graph/commit` | Writes links and field values as JSON in one commit. Needs `Idempotency-Key`. See [Write records as JSON](#write-records-as-json). |
| `POST /v1/graph/import` | Imports NDJSON records inside the request, up to 64 MiB. |
| `POST /v1/graph/import-jobs` | Starts a durable NDJSON import job, up to 32 GiB. Needs `Idempotency-Key`. See [Bulk NDJSON ingest](#bulk-ndjson-ingest). |
| `GET /v1/graph/import-jobs?job_id=<id>` | Job state, progress and result. |
| `DELETE /v1/graph/import-jobs?job_id=<id>` | Asks the job to stop. |
| `POST /v1/graph/import/rdf` | Loads N-Triples, Turtle, N-Quads or TriG, up to 64 MiB per request. See [Load RDF](#load-rdf). |
| `POST /v1/graph/retract` | Retracts `edges`, or every current link of the records in `entities`. `purge_entity_properties: true` also clears those records' fields. Past commits keep the data. |
| `POST /v1/graph/reload?confirm=<graph>` | Makes the graph's current state match an NDJSON payload in one step. Records absent from the payload leave the current state. `?dry_run=true` previews the change. Also takes `strict` and `observed_at`. |
| `POST /update`, `POST /v1/update` | SPARQL Update. `INSERT DATA` only. See [SPARQL Update and graph documents](#sparql-update-and-graph-documents). |
| `POST /rdf-graph-store?default` | Appends a Turtle or N-Triples document to the graph. |

### Query with SPARQL

| Method & path | Purpose |
| --- | --- |
| `GET`, `POST /sparql` | The SPARQL 1.1 Protocol endpoint. Results follow the `Accept` header. See [Native SPARQL 1.1 Protocol](#native-sparql-11-protocol). |
| `POST /v1/query/dataset` | SELECT/ASK over one primary graph and up to four authorized reference graphs. Takes explicit `references` or an immutable `view_revision`; see [Reference datasets and saved views](#reference-datasets-and-saved-views). |
| `POST /v1/query/sparql-text` | A SPARQL SELECT or ASK query in a JSON body. Returns SPARQL Results JSON as a string in `results`. |
| `POST /v1/query/sparql` | A structured query as JSON: patterns, filters, grouping, aggregates, ordering and ASK. |
| `POST /v1/query/ask` | A question in plain words. A model runs queries in a short loop and answers. Returns the answer with the records it cites, the route, and the query whose rows hold the answer, with its rows. `POST /v1/query/rewrite` was removed on 2026-10-05. |
| `POST /v1/query/names` | The records a text names: per name its candidates, the one to prefer first. No model call. |
| `POST /v1/query/describe` | The classes and properties a question needs: how many sampled instances hold each property, the values of small classes, examples and schema statements. No model call. |
| `GET /v1/graph/commit-at` | The commit of `date=YYYY-MM-DD` (by the end of that day, UTC) or `moment=<RFC 3339>`. |
| `POST /v1/query/compare` | One SELECT at two points (`before`, `after`: a commit, a date or a moment), the rows paired by `key` into `added`, `removed` and `changed`, with totals and a cursor. |

Compare's `next_cursor` is an opaque, transient continuation of the same query,
row budget and two snapshot commits in one tenant, graph, branch and graph
incarnation. Send it unchanged with the same comparison request. Later writes in
that incarnation do not move its snapshots. Against a live graph, an unsupported
cursor version or a token for another resource or a prior incarnation returns
`400 invalid_compare_request` with `param: "cursor"`; restart by omitting
`cursor`. A graph that is already missing or deleted keeps the existing `404`
response. An incarnation change during the fenced comparison reads returns
`409 graph_epoch_changed`. Do not parse cursors, use them as identifiers, or reuse
them across graphs. The version2 scope digest binds identity; it does not grant
access.

`/v1/query/sparql-text` takes these body fields:

| Field | Meaning |
| --- | --- |
| `query` | The SPARQL text. CONSTRUCT and DESCRIBE return `400`; send them to `/sparql`. |
| `as_of_commit_seq` | Reads a past commit. See [Time travel](#time-travel). |
| `limit`, `offset` | Row window over the SELECT result. |
| `cursor` | Page through an ordered result. Send `""` to start, then the `next_cursor` of the previous page with the same query. The query needs one ordered `LIMIT` from 1 to 10,000, no `OFFSET`, and no `limit` or `offset` field. |
| `entailment` | `none` (default), `subclass`, `rdfs` or `owl`. See [Reasoning](https://docs.littlebigbrain.com/guides/reasoning/). |
| `reason` | `true` returns an error. Stored rules already ran when the data was published. |
| `request` | The user's words behind the query. The response then carries a `trace_id` for [evals](#evaluate-answers). |
| `route` | With `request`: how your app routed the question, such as the `route` of `/v1/query/ask` with `mode: "route"`. The eval trace keeps it. It does not change the query. |

The response has `results` and `row_page`, with `limit`, `offset`,
`returned`, `total` and `has_more`. It adds `next_cursor` when a next page
exists, and `snapshot` on an eventual read.

`/v1/query/ask` takes `question` and the optional `context`, `route`,
`mode`, `limit`, `as_of_commit_seq`, `today`, `include_grounding`, `anchor`
and `timeline`. Its response has `answer` (`text`, `citations`, `chart`), `steps`,
`route`, `query`, `rationale`, `attempts`, `grounding`, `models` and
`timings`. `result` is the response of `/v1/query/sparql-text` for the query
whose rows hold the answer, with a `trace_id`. When the loop stopped before
it answered, `error` says why. `linked` lists the names of the question the
server found in the graph, and `anchors` what it read about each IRI of
`anchor`. A history answer adds `history`: the commit of its date, and for a
comparison the rows `added`, `removed` and `changed`, with `totals`.
`mode: "route"` returns the route alone. With `Accept: text/event-stream`
the steps come as server-sent events, and the last event, `done`, holds the
response. [Answer questions in plain words](https://docs.littlebigbrain.com/guides/answer-questions/)
covers each field and [each step](https://docs.littlebigbrain.com/guides/answer-questions/#show-each-step).

```sh
curl --fail-with-body -sS -X POST "$LBB_URL/v1/query/ask?graph=support" \
  -H "Authorization: Bearer $LBB_API_KEY" -H 'Content-Type: application/json' \
  --data '{"question": "Which release fixed the login bug?"}'
```

### Reference datasets and saved views

Reference datasets and saved views are optional and disabled by default. Each
requires its deployment feature to be enabled. The quickstart still needs only
a graph and a token; an administrator may save a selection when an app reuses it.

| Method & path | Purpose |
| --- | --- |
| `POST /v1/catalog/views` | Environment administrator only. Save an immutable source definition; returns `{revision, definition}`. The same definition in the same environment returns the same revision. |
| `GET /v1/catalog/view?revision=<revision>` | Environment administrator only. Retrieve that exact saved definition. |
| `POST /v1/query/dataset?graph=<primary>` | Query an explicit selection or saved revision. The query credential must authorize every source; the view grants no access. |

The save body names the primary graph, its exact epoch, and at most four
reference graphs in the same environment:

```json
{
  "primary_graph_id": "project",
  "expected_graph_epoch": "7",
  "references": [
    { "name": "urn:standards", "graph_id": "standards", "expected_graph_epoch": "3" }
  ]
}
```

Epochs are decimal strings. A reference's `name` is the absolute IRI used in
`GRAPH`, not an access grant. The primary is the default graph. The definition
and each reference may include `as_of_commit_seq` to require an exact published
commit; saving a view does not retain older generations or copy graph data.
A transient publication failure returns a retryable 503; retrying the same
definition converges on its immutable revision. A missing-view lookup returns 404.

Keep the returned revision. To query it, send this body to
`/v1/query/dataset?graph=project`, replacing `<saved revision>` with that value:

```json
{
  "view_revision": "<saved revision>",
  "references": [],
  "expected_graph_epoch": "7",
  "query": "SELECT ?standard WHERE { GRAPH <urn:standards> { ?standard <urn:status> <urn:approved> } }"
}
```

The `references` field is always required; use `[]` with `view_revision`. The primary graph and epoch must match
the saved definition; if the query supplies `as_of_commit_seq`, it must also
match. Every source must remain authorized and have the required published RDF
generation. The response contains SPARQL Results JSON in the string `results`,
independently pinned `sources`, and `charged_read_bytes`. It is not an atomic
cross-graph snapshot. A view revision is immutable; there is no mutable latest
pointer, namespace subtree selection or implicit authority grant.

SDK methods are TypeScript `saveDatasetView`, `getDatasetView`, `sparqlDataset`
and Python `save_dataset_view`, `get_dataset_view`, `sparql_dataset`. See
[How to model your app](https://docs.littlebigbrain.com/start/model-your-app/#save-a-reference-selection-for-reuse).

### Read records

| Method & path | Purpose |
| --- | --- |
| `GET /v1/graph/entity` | One record: typed `attributes` and current links, read at one commit. See [Read one record](#read-one-record). |

There is no route that lists every record or every link. Use SPARQL and point
reads.

### Search by meaning

| Method & path | Purpose |
| --- | --- |
| `GET /v1/embeddings` | The graph's embeddings with their status: serving and building version, progress, watermark and lag. `?name=` returns one. |
| `PUT /v1/embeddings` | Declares or changes the embedding of a class: its `class`, `from` fields, `model` and `title`. A new recipe builds a new version while the current one serves. |
| `DELETE /v1/embeddings?name=<name>&confirm=<name>` | Removes an embedding and its vectors. |
| `POST /v1/embeddings/preview` | Shows what a declaration would embed: resolved fields, candidate facts and sample texts. Stores nothing and calls no model. |
| `PUT /v1/embeddings/model` | Moves every embedding of the graph to another model. |
| `POST /v1/embeddings/refresh?name=<name>` | Runs one bounded step of the embed job now. |
| `POST /v1/search` | Search by meaning over every searchable class, with filters by class and by link. Every hit is checked against one published commit. |

[Search by meaning](https://docs.littlebigbrain.com/guides/search-by-meaning/) covers the request fields,
the response and the errors.

### Workflows

A workflow has a name, unique on the graph. The routes under
`/v1/workflows/named` read and change a workflow by that name. A workflow
made from one of LBB's templates (a starter workflow) is created with
`PUT /v1/workflows/triggered`, and previewed and calibrated there too.

| Method & path | Purpose |
| --- | --- |
| `GET /v1/workflows/starters` | The catalog: every starter workflow this server offers, with its group, its settings, the graph kinds it supports, and whether it can run here. |
| `PUT /v1/workflows/triggered` | Creates a workflow from a starter workflow on a class, or changes one: its `starter`, `watch` (`class`, `fields`), `params` and `name`. A developer workflow (`starter: "workflow"`) also takes `workflow` (`workflow_type`, `version`), `owns` and `batch`; `documents.link` and `values.extract` take `gate` and `budget_usd_per_month`. The same request again changes nothing. |
| `POST /v1/workflows/triggered/preview` | Shows what creating a workflow would do: the resolved watch, the instance count and the starter workflow's own preview. Writes nothing. `propose: true` runs the models of `ontology.fit` on the sample. |
| `GET /v1/workflows/triggered/calibration` | The gates of the gated workflows (`documents.link`, `review`, `values.extract`), measured from review decisions, with a proposed gate. |
| `POST /v1/workflows/triggered/calibration/apply` | Sets a gate from its measurement: `{starter, name, gate?}`. A person in the console only. |
| `GET /v1/workflows/named` | The graph's workflows by name, in the list envelope (`limit` 1 to 200, default 100; `cursor`): each one's `object` (`workflow`), `created`, target workflow type, shards, routing field, window, trigger, status and shard inboxes. LBB's own workflows list here too, with an `lbb.*` target. `?name=` returns one workflow. |
| `PUT /v1/workflows/named` | Creates or changes a workflow of your worker: `name`, `workflow`, one trigger, and `shards`, `route_by`, `window`, `batch`, `owns`, `on_exhausted` (`block` or `skip`), `budget_usd_per_month` and `fan_in` (a fan-in identity `starter.<name>.<fan_in>`). The trigger is a `watch`, a `content` trigger (`{"content": {"media_types": ["application/pdf"]}}`), which sends each upload of those media types, one file per message, without a wait for a publication, a `schedule` (`{"schedule": {"cron": "0 3 * * *", "timezone": "Europe/Berlin"}}` or `{"schedule": {"every": "1h"}}`), which sends one message per slot, or a `stream` (`{"stream": {"max_wait": "10s"}}`), which sends the events you post in batches. A change of `shards` waits for empty inboxes (`409 workflow_shards_busy`). |
| `POST /v1/workflows/named/run?name=<name>` | Runs the workflow now and answers at once with `queued`. |
| `POST /v1/workflows/named/pause?name=<name>` | Body `{"paused": true}` pauses the trigger (a watch, a content trigger, a schedule or a stream) and its workflow instances. `{"paused": false}` resumes both. A paused schedule skips its slots; a paused stream takes events and sends none. |
| `POST /v1/workflows/named/events?name=<name>` | Body `{"events": [{id, key?, type?, time?, data?}]}`, 1 to 500 events: sends them to a workflow with a stream. Each `id` is the event's idempotency key, so the route takes no `Idempotency-Key`. `429 workflow_stream_rate_limited` or `workflow_stream_full`, with `Retry-After`. |
| `DELETE /v1/workflows/named?name=<name>&confirm=<name>` | Deletes the workflow. What it wrote stays. |
| `GET /v1/workflows/named/queue?name=<name>` | What the trigger has not sent yet, in the list envelope (`limit` 1 to 100, default 50; `cursor`): each record with its shard and origin. Beside the envelope: `pending`, the pass in progress, and each shard's messages. |
| `GET /v1/workflows/named/runs?name=<name>` | The workflow's runs, newest first, in the list envelope: origin, outcome, error, the queue before and after, what each run finished, its steps and phases, and the usage of its model calls. `cursor`, `limit` (1 to 200, default 50) and `outcome` page and filter them. Kept 30 days. |
| `GET /v1/workflows/named/runs/get?name=<name>&run=<run id>` | One run with its model calls in time order. |
| `GET /v1/workflows/named/runs/stats?name=<name>&hours=<n>` | The workflow's runs by hour (24 hours by default, at most 168): runs, units, calls, tokens, cost, failed runs, queue and time. |
| `GET /v1/workflows/named/runs/results?name=<name>` | What the runs did with each input, newest run first, in the list envelope: the input, its outcome (`written`, `review`, `no_value`, `failed`, `duplicate`) and its values with their quotes. `run`, `outcome` (a comma list), `iri`, `cursor` and `limit` (1 to 200, default 50). With `run`: `run_counts` beside the envelope. Kept 30 days. See [Run results](https://docs.littlebigbrain.com/guides/workflows/#run-results). |
| `GET /v1/workflows/named/documents?name=<name>` | The documents of a `documents.link` or `documents.map` workflow, in the list envelope, with `total`, the documents that match `q`. `q` finds them by name, `sort` is `review`, `name` or `recent`, `cursor` and `limit` (1 to 200, default 50) page them. |
| `GET /v1/workflows/instances/runs?id=<instance id>` | The runs of one workflow instance, in the list envelope. Also `/runs/get` and `/runs/stats`. |
| `POST /v1/workflows/named/requeue?name=<name>` | Body `{workflow_id, turn, items: [{instance, error}]}`: sends the instances a turn could not do back to the watch, after 1, 4, then 16 minutes, at most 3 times. The turn completes, so the shard goes on. |
| `POST /v1/workflows/blobs?workflow_id=<identity>` | Stores the request body (at most 8 MiB) for a workflow and returns its `key`, `<workflow>/<sha256>`. The same bytes again return the same key. |
| `GET /v1/workflows/blobs?key=<key>` | The bytes of a blob. Blobs go with their workflow and with the graph. |
| `POST /v1/workflows/derivations/check` | Body `{sha256, workflow_type, version, settings_digest}`: whether this version of the workflow made something of the file with these settings (`exists`, `derivation`), and the upload of the live file (`content_uploaded_at`, absent for a deleted file). |
| `POST /v1/workflows/derivations/record` | The same key with `run_id`, `outputs` (at most 16 KiB of JSON) and the check's `content_uploaded_at`. Keeps the first record of each key; a second record answers it with `recorded: false`. When the file was deleted since the check, it records nothing: `409 derivation_upload_changed`. |

The workflows of `search.embed` and `ontology.fit` read and write the
documents of `/v1/embeddings` and `/v1/ontology/fit-sources`. A developer
workflow sends the instances to a workflow of your worker and needs a
server with the workflow engine. The workflows of `documents.parse`,
`documents.link` and `values.extract` read the graph's files and text and write
the facts they own. [Starter workflows](https://docs.littlebigbrain.com/guides/starter-workflows/) covers
the fields, the status and the errors, and
[From documents to a graph](https://docs.littlebigbrain.com/guides/documents-to-graph/) the document
workflows.

### Files

| Method & path | Purpose |
| --- | --- |
| `POST /v1/files?name=<name>` | Uploads a file: the request body is its bytes, at most 1 GiB by default (`413 file_too_large` above it), with its `Content-Type`. An empty body is an empty file. Stores the bytes once under their SHA-256 and commits a `Document` record for them, with a `Content` record of the bytes' `sha256` and `byte_length` (linked by `HAS_CONTENT`). Optional `language` (at most 64 characters) and `schema` (at most 256) are stored on the `Document`. Without a `Content-Type` (or with `application/octet-stream`) the server detects PDF, PNG, JPEG, GIF and WebP by their bytes, `.xlsx`, `.docx` and `.pptx` by the ZIP signature and the name, and UTF-8 text named `.csv`, `.tsv`, `.md`, `.json` or `.txt` by the name. The same bytes under the same name again change nothing (`created: false`). `409 file_generated` for the bytes of a generated file (one a workflow run wrote). |
| `GET /v1/files?limit=<n>&after=<next>` | One page of the graph's files, the newest upload first: `sha256`, `name`, `content_type`, `language` and `schema` when given, `bytes`, `uploaded_at`, the `document` IRI, and `parse` once the parse workflow read the file. Without `limit`, the newest 1,000 (`limit` is 1 to 1,000). `next` is the `after` of the next page and is absent at the end; a page can hold fewer than `limit` files while `next` is set. `parse_status=<status>` (`parsed`, `unsupported`, `failed`, or `waiting` for none) and `name=<text>` (a part of the name, in any case) filter the list over the `Document` records of the published generation, with the commit it holds (`as_of_commit_seq`). |
| `GET /v1/files/record?sha256=<hex>` | One file's record: the same fields as one file of the list. `404 file_not_found` for a SHA-256 the graph has no file for, or a deleted one. With `deleted=true`, a deleted file whose bytes are not freed yet answers with `deleted_at`, and each answer holds `cited_by`: the facts whose evidence names the file, at most 20. |
| `GET /v1/files/counts` | The graph's files by parse status (`files`, `by_parse_status`; `waiting` before the parse), counted over the `Document` records of the published generation, with the commit it holds (`as_of_commit_seq`). |
| `GET /v1/files/content?sha256=<hex>` | The file's bytes, with its content type, streamed from storage. One `Range: bytes=a-b`, `bytes=a-` or `bytes=-n` returns `206` with that part and `Content-Range`; a range with no byte of the file returns `416 range_not_satisfiable`. `ETag` is the SHA-256 in quotes. The response sends `X-Content-Type-Options: nosniff` and `Content-Disposition: attachment` with the file name (`filename="<ascii>"` and the RFC 8187 `filename*=UTF-8''<name>`), so a browser downloads the bytes and does not render them. |
| `HEAD /v1/files/content?sha256=<hex>` | The file's `Content-Length`, `Content-Type`, `ETag` and the `X-Content-Type-Options` and `Content-Disposition` headers of the GET, with no body. |
| `GET /v1/files/pages?sha256=<hex>&from=<n>&to=<n>` | The parsed pages `from` up to `to` (0-based, `to` excluded; default 10 pages, at most 50): each page's `width` and `height` in PDF points and its lines, each with its `id`, `text`, `box` and `words`. A box is `[x0, y0, x1, y1]` normalized to the page, origin top left. `404 file_not_parsed` before the parse. |
| `GET /v1/files/sheets?sha256=<hex>&sheet=<name>&from=<n>&to=<n>` | The sheets of an XLSX workbook, or the rows of one sheet. Without `sheet`, every sheet with its name, state and size and no rows. With `sheet`, its rows `from` to `to` (A1 rows from 1, both included; default 100 rows, at most 1,000 and 20,000 cells; `next` names the row to go on from). A cell has its stored `value` as text and its `formula` apart; nothing is calculated. The first read parses the workbook in a bounded child process. `400 file_not_spreadsheet`, `400 xlsx_refused`, `404 sheet_not_found`, `503 xlsx_busy`. |
| `PUT /v1/files/pages?sha256=<hex>&parser=<id>` | Stores what a parser outside the server made of a file. The JSON body is `{"uploaded_at": "...", "status": "parsed", "pages": [...]}` with every page in the shape `GET /v1/files/pages` serves, or `{"uploaded_at": "...", "status": "failed", "error": "..."}` (also `unsupported`). `uploaded_at` is the file record's, so the pages belong to that upload: a file deleted and uploaded again since answers `409 file_upload_changed`. The server checks the pages and stores them once under the parser id (1 to 64 letters, digits, `.`, `_` or `-`). The same pages again store nothing (`stored: false`), so a retry is safe; other pages under the same id answer `409 file_pages_conflict`. The file's `parse` follows. Your parser writes the `Document`'s properties with its own commit. |
| `DELETE /v1/files/expiry?sha256=<hex>` | Keeps a generated file: removes its `expires_at`, so it stays until a person deletes it and shows in `GET /v1/files`. Answers the file record. A repeat answers the same record. `404 file_not_found` for no file or a deleted one. |
| `DELETE /v1/files?sha256=<hex>&confirm=<hex>` | Removes the file from the list and retracts its `Document` and `Content` records. The bytes and what the workflows made of the file stay for 30 days. Then they go, unless a fact's evidence still names the file. `purge=true` also retracts those facts (`purged_edges`). An upload of the same bytes while they go answers `429 file_reclaim_in_progress`; retry it. |

Files work on records graphs and RDF-native graphs. On an RDF-native graph
the `Document` gets the same statements and IRIs a records graph shows. An
unknown SHA-256 answers `404 file_not_found`.

### Evaluate answers

| Method & path | Purpose |
| --- | --- |
| `GET /v1/evals` | Summary: settings, golden counts, unlabeled traces, the latest run and the score by commit. |
| `GET /v1/evals/traces` | Recent traces, newest first. Takes `?limit=` and `?unlabeled=`. |
| `GET /v1/evals/trace?id=<id>` | One trace: the request, its query and its type, its results and their labels. |
| `POST /v1/evals/label?trace=<id>` | Labels results of a trace as relevant or not. On a question, `valid` alone labels the answer: `true` makes its query the golden query, `false` with `sparql` makes that query the golden query. `route` with such a label names the route the question expects; `true` alone takes the trace's route. |
| `POST /v1/evals/judge` | Lets the managed judge label one trace (`?trace=`) or a batch (`?limit=`). |
| `GET /v1/evals/goldens` | The golden suite: questions, searches and stored queries, with their golden queries and judged results. |
| `POST /v1/evals/goldens` | Freezes a stored query or a search and the results it returns now as a golden. A question becomes a golden through a label. |
| `POST /v1/evals/goldens/accept?id=<id>` | Accepts what a golden returns now as its new reference. A question is asked again, and the final query of Ask's answer loop becomes its golden query. |
| `DELETE /v1/evals/goldens?id=<id>` | Deletes a golden. |
| `POST /v1/evals/run` | Checks every golden at the newest commit and records the verdicts. A question is asked again through Ask (`POST /v1/query/ask`); the run checks the type of the loop's final query, its rows against the golden query and the judged results. The verdict shows the answer and the records it cites (`reply`). |
| `POST /v1/evals/run/answers` | Checks your own agent's answers to the questions: one answer per golden, with the query that holds the answer and the route. The server runs each query and checks it as it checks Ask's answer, and the router's pick against the golden's route (`route_check`). See [Test your agent's answers](https://docs.littlebigbrain.com/guides/answer-questions/#test-your-agents-answers). |
| `GET /v1/evals/results` | Eval runs, newest first. Takes `?limit=`, and `?runner=` for the runs of one agent. |
| `GET`, `PUT /v1/evals/settings` | Reads or writes the graph's evals settings. |

A trace starts when a `/v1/query/sparql-text` request carries a `request`
field, or when `/v1/query/ask` answers from a query. Both are traces of a
question, and every trace of one question shares one golden, whatever query
answered it. A trace of `/v1/query/ask` also keeps the inputs of the request
(`context`, `anchor`, `route`, `today`, `timeline`), so a run asks the same
way, and the answer with its citations (`reply`).

### Schema and validation

| Method & path | Purpose |
| --- | --- |
| `GET /v1/ontology` | The declared types, links and fields. Each class in `entity_type_defs` has a fixed `stable_id`, its query `iri` and its direct `super_types`. `?counts=true` adds exact `relation_defs[].edge_count` values. |
| `POST /v1/ontology/define` | Puts the graph on an ontology document. Creates the graph when needed. See [Define your own ontology](#define-your-own-ontology). |
| `POST /v1/ontology/evolve` | Applies ordered schema changes. `?dry_run=true` previews them. See [Evolve the schema](#evolve-the-schema). |
| `GET /v1/schema` | The active ontology, the SHACL shapes as the validator parsed them, and the enforcement mode. |
| `POST /v1/schema/publish` | Publishes SHACL shapes with `desired_mode`: `off`, `warn` or `reject`. |
| `GET /v1/ontology/conformance` | The SHACL report of the latest published commit. |
| `GET /v1/graph/schema-summary` | The schema observed in the latest published data. |
| `POST /v1/ontology/drafts` | Builds a schema proposal from sample records. See [Ontology drafts](#ontology-drafts). |
| `GET /v1/ontology/drafts?draft_id=<id>` | Reads a draft. |
| `POST /v1/ontology/drafts/validate?draft_id=<id>` | Checks a draft again at its pinned commit. |
| `POST /v1/ontology/drafts/promote?draft_id=<id>` | Publishes a validated draft. Needs `Idempotency-Key`. |
| `POST /v1/ontology/drafts/reject?draft_id=<id>` | Records a rejection with a `reason`. The schema does not change. |
| `GET /v1/ontology/starters` | Lists the starters (`crm`, `documents`, `work`) with each one's status on the graph. Works before the graph exists. See [Ontology starters](#ontology-starters). |
| `GET /v1/ontology/starters/detail?starter=<id>` | Reads one starter, its status and the operations applying it would run. |
| `POST /v1/ontology/starters/apply` | Adds what the graph lacks of a starter in one ontology version. `dry_run: true` writes nothing. |
| `POST /v1/ontology/starters/update` | Files what the graph lacks of a starter as one change suggestion. |
| `POST /v1/ontology/suggestions` | Files a schema change suggestion, or updates the one with the same `key`. See [Ontology change suggestions](#ontology-change-suggestions). |
| `GET /v1/ontology/suggestions` | Lists suggestions, newest first, with counts per status. |
| `GET /v1/ontology/suggestions/detail?suggestion_id=<id>` | Reads one suggestion with its evidence, dry run and discussion. |
| `POST /v1/ontology/suggestions/validate?suggestion_id=<id>` | Runs the dry run again against the current schema. |
| `POST /v1/ontology/suggestions/accept?suggestion_id=<id>` | Applies the change, or an edited change, to the current schema. |
| `POST /v1/ontology/suggestions/dismiss?suggestion_id=<id>` | Declines a suggestion with a `reason`. The schema does not change. |
| `POST /v1/ontology/suggestions/supersede?suggestion_id=<id>` | Withdraws a suggestion as its producer, with a `reason`. |
| `POST /v1/ontology/suggestions/comment?suggestion_id=<id>` | Adds a comment to the discussion. |
| `GET /v1/ontology/fit-sources` | The graph's fit sources with their status: progress, watermark and lag, proposals kept and dropped, suggestions filed, this month's spend. `?name=` returns one. See [Suggest ontology changes from text](https://docs.littlebigbrain.com/guides/suggest-ontology-changes-from-text/). |
| `PUT /v1/ontology/fit-sources` | Declares or changes the fit source of a class: its `class`, the `from` fields that hold the text, and a `context` sentence. |
| `DELETE /v1/ontology/fit-sources?name=<name>&confirm=<name>` | Removes a fit source. Its suggestions stay. |
| `POST /v1/ontology/fit-sources/preview` | Shows the text the fit would read. With `propose: true`, runs the models on up to three instances and returns the proposals without filing them. |
| `POST /v1/ontology/fit-sources/refresh?name=<name>` | Asks the fit job to run now and returns the status at once. |
| `POST /v1/ontology/fit-text` | Reads one text once: pasted `text`, or the parsed pages of an uploaded file (`document`). Files the proposals as suggestions, or nothing with `dry_run: true`. |

`POST /v1/schema/publish` never rewrites or checks existing data during the
call. A new mode publishes the shapes again as a new version. The report
follows in the background.

Current data may break the new shapes. Then publish in `warn` first, and
repair the data. Switch to `reject` once the report is clean. See
[Query and validate](https://docs.littlebigbrain.com/guides/sparql-and-shacl/).

`GET /v1/ontology/conformance` reports `conforms`, `result_count`,
`validated_at_seq`, `ontology_version`, and `shapes_version` when shapes are
published. `?limit=` bounds the result rows (default 200, maximum 2,000).

`conforms` and `result_count` count every result, even past the limit.

`GET /v1/schema` lists the parsed shapes in `shapes`. Each has an `id`,
its `targets`, a `path` for a property shape, and its `constraints`. The
terms have the same form as the report. A result's `source_node_shape`,
`result_path` and `component` find the rule that produced it.
`?consistency=strong` asks for a report that covers the newest commit. Until
one exists, it returns a retryable `429 strong_read_pending`.

`GET /v1/graph/schema-summary` reads the latest published commit. It returns
class populations and a bounded list of OWL and RDFS statements. Per
predicate, it counts links in `resource_predicate_counts` and values in
`literal_predicate_counts`.

### Publication and status

| Method & path | Purpose |
| --- | --- |
| `GET /v1/graph/publication-status` | How far publication has come. Poll it to wait for a write. See [Publication status](#publication-status). |
| `GET /v1/graph/activity` | The background work on the graph, for a status view. See [Graph activity](#graph-activity). |
| `GET /v1/graph/read-snapshot` | What eventual reads serve now, and how far that is behind the newest commit. |
| `GET /v1/graph/summary` | Graph counts at one published commit. |
| `GET /v1/graph/metadata` | Storage diagnostics for the graph. |
| `GET /v1/graph/index-upgrade` | The plan for optional index upgrades. See [Upgrade indexes on existing graphs](#upgrade-indexes-on-existing-graphs). |
| `POST /v1/graph/index-upgrade` | Plans, or with `"dry_run": false` queues, an index upgrade. |
| `GET /v1/graph/index-upgrade/job?job_id=<id>` | Progress of an index upgrade. |
| `POST /v1/graph/export/job` | Starts a full export as NDJSON. See [Export a graph](#export-a-graph). |
| `GET /v1/graph/export/job?job_id=<id>` | Progress and result of an export. |

`GET /v1/graph/summary` returns class and link populations, `entity_count`,
`current_edge_count` and `observation_count`. `relation_adjacency` counts the
links per source type, link and target type, exactly. `?consistency=strong`
returns a retryable `429 strong_read_pending` until publication covers the
newest commit. `?min_indexed_seq=` sets a read floor, as in
[Read consistency](#read-consistency).

`GET /v1/graph/metadata` reports the newest commit, the schema version and
storage sizes. Before the first publication it returns `200` with
`index_caught_up: false` and no `snapshot.served_at_seq`. To wait for a write,
use publication status.

The default metadata read follows the graph head's RDF publication reference,
validating its immutable root and mandatory F3 header. `snapshot.commit_seq`
remains the newest observed head commit; `served_at_seq` and
`published_lag_commits` describe that RDF base. A named but missing or damaged
RDF artifact is an error, not first-publication pending. `index_caught_up`
reports RDF commit coverage even with `include_indexes=false`; it does not
prove class embeddings or model/configuration changes are ready.

`include_indexes=true` opts in to deprecated BM25/ANN family lineage. Those
legacy watermarks are separate from current class-declared embeddings and
never determine RDF readiness. Missing legacy roots leave their fields empty;
a damaged legacy root fails the explicit inspection. The default read does
not consult the old family publication pointer.

Each RDF root and F3-header diagnostic read has a 16 MiB control-object
budget. A larger object returns `400 graph_metadata_budget`; this is a
diagnostic limit, not an RDF manifest format limit. Ordinary RDF reads can
remain valid. Use `GET /v1/graph/publication-status` for the publication
workflow's readiness/progress readout where appropriate.

### Operator demand diagnostics

Opt-in operator readouts are `GET /api/ops/read-demand` for the serving node and
`GET /api/ops/read-demand/fleet` for the collecting runtime. Single mode uses its
configured API token; SaaS requires the ops token. Environment and scoped graph
tokens do not grant access. The local observer and fleet collector default off
(`LBB_READ_DEMAND_ENABLED=false`, `LBB_SCHEDULER_READ_DEMAND=off`).

The maintenance runtime exposes the authenticated fleet GET; ingest exposes
neither readout. Fleet evidence is held in memory on the collector, so request it
directly there. These diagnostics distinguish request completion from actual
capacity release and report missing or partial evidence. They do not change
replicas or establish serving readiness or a latency guarantee.

## Write records as JSON

`POST /v1/graph/commit` writes links (`triplets`) and field values
(`entity_properties`) in one commit. The graph must declare the types, links
and fields first. See [Define your own ontology](#define-your-own-ontology).

```sh
curl --fail-with-body -sS -X POST "$LBB_URL/v1/graph/commit?graph=support" \
  -H "Authorization: Bearer $LBB_API_KEY" \
  -H "Idempotency-Key: support-4821-1" \
  -H "Content-Type: application/json" \
  -d '{
    "triplets": [{
      "source": {"type": "Ticket", "key": "4821", "name": "login fails after 3.1"},
      "relation": "REPORTED_AS",
      "target": {"type": "Bug", "key": "AUTH-112", "name": "session token expiry"},
      "evidence": "Support triage, 2 Sep"
    }],
    "entity_properties": [{
      "type": "Ticket", "key": "4821", "name": "login fails after 3.1",
      "properties": {"description": "Since 3.1, people are sent back to the login screen."}
    }]
  }'
```

The response reports `commit_seq`, the number of the new commit. It also lists
the ids of the records it wrote.

| Body field | Meaning |
| --- | --- |
| `triplets` | Links. Each has `source`, `relation` and `target`, and optional `evidence`, `confidence`, `valid_time` and `properties`. |
| `entity_properties` | Field values. Each item names a record by `type` plus `key` or `name`, then gives `properties`. `remove_fields` drops named fields. `entity_id` addresses a record by its id. |
| `entity_embeddings` | Vectors you supply for records. |
| `property_merge` | `merge` (default) keeps the fields you do not send. `replace` makes the sent fields the record's whole set. |
| `edge_idempotency` | `append` (default) records every link you send. `skip_unchanged` skips a link that is already current, for re-runnable backfills. |
| `observed_at` | An RFC 3339 time. The commit is recorded as of that time, for backfills. |
| `preconditions` | `expected_head_commit_seq`, `expected_ontology_version` or `expected_snapshot_token`. The commit fails with `409` if one does not match. |
| `replace_owned` | The links and fields a writer owns on some records. The same commit removes the owned ones it does not write again. See [Owned outputs](#owned-outputs). |

`properties` is a flat object, such as `{"description": "…"}`. The server
converts each value to the field's declared type. A list of
`{"field": …, "value": {"<type>": …}}` items sets the wire type explicitly.

`?dry_run=true` checks the commit and writes nothing. It needs no
`Idempotency-Key`. A value of the wrong type fails the whole commit. So does an
unknown field or an unknown type.

**Record identity.** A record is its type plus its `key`. Without a `key`, it
is its type plus its `name`. The record id is a hash of those values only,
never of the fields. So `name` is a display label.

To update a record, send the same `key` with the fields that changed. Field
writes merge by default, so omitted fields stay. Two mistakes create a second
record instead:

- **A missing `key`.** A write that omits a keyed record's `key` resolves by
  `name`. It writes to a second record. Always send the `key`.
- **A backfill without keys.** A backfill may know each record's `entity_id`
  (32 hex characters) and not its `key`. Send that `entity_id` on each
  `entity_properties` item. It skips resolution by type, key and name.

`property_merge` changes which fields are kept. It never changes which record
an item resolves to.

### Owned outputs

A writer that derives facts from records, such as a
[developer workflow](https://docs.littlebigbrain.com/guides/starter-workflows/#developer-workflows), writes
them again when a record changes. Its old links and fields must go. A commit
with `replace_owned` does both in one step:

```json
{
  "entity_properties": [{ "type": "Note", "name": "Kickoff", "properties": { "word_count": 8 } }],
  "replace_owned": {
    "subjects": [{ "type": "Note", "name": "Kickoff" }],
    "properties": ["word_count"],
    "relations": ["MENTIONS"]
  }
}
```

| Field | Meaning |
| --- | --- |
| `subjects` | The records this write is for. |
| `relations` | Owned relations. Each current link of a subject with one of them that this commit does not write again is retracted. |
| `properties` | Owned fields. Each one this commit does not set on a subject is removed. |
| `retract_entities` | Records the writer made and no longer makes. Their current links and fields go, except the ones this commit writes again. |

The removals and the new facts land in one commit. A rerun with the same
output keeps the same facts (send `edge_idempotency: "skip_unchanged"` to
record no new link events), and a rerun with less output removes the rest. The
response reports `owned`: `edges_retracted`, `fields_removed` and
`entities_retracted`. A commit may carry only `replace_owned`, for example
to clear what a writer wrote for records that left its class.

- Ownership is by relation and field. A retracted link is gone for every
  writer, so own relations that no other writer uses on the same records.
- At most 1,000 records, 64 relations and 64 fields. An unknown type,
  relation or field is a `400`, as is a record that is both a subject and in
  `retract_entities`.
- On an RDF-native graph the commit writes the statements a records graph
  shows for its facts, under the same IRIs. It removes what the same writer
  wrote before for each subject and no longer writes. The writer is named by
  its owned relations and fields. `entity_embeddings`, `preconditions` and
  `?dry_run=true` are refused there.

A trigger message names records by IRI (`https://littlebigbrain.com/e/`
and 32 hexadecimal characters). To name such a record in a commit, a
subject or a link endpoint, set its `type` to its class and its `key` to
`"\u0000lbb-eid:"` followed by those 32 characters: the commit addresses
that record by its id, a keyed record included. The TypeScript SDK builds
the reference with `entityRef(iri, type, name?)`, the Python SDK with
`entity_ref(iri, type, name=None)`. A link endpoint or a field row writes its
`name` as the record's display name, so pass the record's name there.

## Load RDF

`POST /v1/graph/import/rdf` loads RDF documents. RDF is the W3C data model
behind every graph; see [RDF under the hood](https://docs.littlebigbrain.com/start/concepts/#rdf-under-the-hood).
The first import into a new graph creates it. [Load RDF data](https://docs.littlebigbrain.com/guides/load-rdf/)
walks through a full load.

| Format | `Content-Type` | `?format=` |
| --- | --- | --- |
| N-Triples (the default) | `application/n-triples` | `ntriples`, `nt`, `n-triples` |
| Turtle | `text/turtle` | `turtle`, `ttl` |
| N-Quads | `application/n-quads` | `nquads`, `nq`, `n-quads` |
| TriG | `application/trig` | `trig` |

Save a first chunk as `chunk-1.nt`:

```text
<https://example.com/sku/1> <http://www.w3.org/2000/01/rdf-schema#label> "Trail shoe" .
<https://example.com/sku/1> <https://example.com/brand> <https://example.com/brand/acme> .
```

Then import it into the `catalog` graph:

```sh
curl --fail-with-body -sS -X POST "$LBB_URL/v1/graph/import/rdf?graph=catalog" \
  -H "Authorization: Bearer $LBB_API_KEY" \
  -H "Idempotency-Key: catalog-chunk-1" \
  -H "Content-Type: application/n-triples" \
  --data-binary @chunk-1.nt
```

The response counts the triples read, imported and duplicated. It lists each
predicate and reports `committed_commit_seq`. `published_generation` shows
whether publication was queued.

Small imports that reach one graph at the same time can share one commit.
Each response then reports the shared `committed_commit_seq`. A read at that
sequence sees the whole request.

The import keeps each predicate IRI and each literal as written. SPARQL returns
the original terms. The import does not declare a link type per predicate.
Every statement is stored under one fixed relation, `RDF_TRIPLE`, which the
response names.

The console's Data view loads N-Triples and Turtle, including `.ttl` files.

| Parameter | Default | Meaning |
| --- | --- | --- |
| `batch` | 100,000 | Triples per commit, up to 1,000,000. One request can span several commits, so a request as a whole is not atomic. |
| `build` | `true` | `false` skips publication after this request. The server still publishes after 8 unpublished commits or 32 MiB of unpublished data, so later chunks stay writable. Leave the final chunk at `true`. |
| `blank_node_scope` | none | A document id for blank-node labels. Up to 255 characters: letters, digits, `-`, `_`, `.`, `:` and `/`. |
| `edge_idempotency` | `skip_unchanged` | Skips statements that are already stored, for re-runnable imports. `append` suits a one-time load that an `Idempotency-Key` protects. |
| `strict` | `false` | `true` fails the whole request at the first bad statement. Otherwise bad statements are counted in `error_count`, with up to 100 examples. |
| `base_iri` | none | The base for relative IRIs in Turtle and TriG. |
| `observed_at` | now | An RFC 3339 time to record the commits at, for backfills. |
| `graph_uri` | none | Always refused. See the named-graph rule below. |

Four rules apply to a large load:

- **Split on line boundaries.** N-Triples and N-Quads have one statement per
  line. A chunk must end at a line end. Each Turtle or TriG request must be a
  complete document.
- **Keep each request at 64 MiB or less.** A larger body returns `400`.
- **Duplicates are dropped.** A statement repeated within one request is stored
  once, across commit boundaries. The response counts
  `duplicate_resource_triples` and `duplicate_literal_triples`.
- **Blank-node labels keep their identity.** In one document, a label names
  the same node across chunks and retries. Separate documents can reuse labels
  such as `_:b0`. Give each document its own `?blank_node_scope=`, on every
  chunk of that document.

A graph serves the default RDF graph only, so the import refuses named graphs.
In an N-Quads or TriG document, the import skips each named-graph statement
and counts it in `error_count`. One `errors` entry names how many it skipped
in each named graph. `?strict=true` fails the request at the first one.
`?graph_uri=` returns `400` for the same reason.

### RDF-native graphs

A graph whose first write is RDF is **RDF-native**. Write it with RDF import
and SPARQL Update from then on. These routes return `400 rdf_native_graph` on
it:

- `POST /v1/graph/commit` without `replace_owned`, and `?dry_run=true`
- `POST /v1/graph/import`
- `POST /v1/graph/retract`
- `POST /v1/graph/reload`
- `POST /v1/graph/export/job`, which exports records. Read the statements with a
  SPARQL `CONSTRUCT` query on `/sparql` instead.

`POST /v1/graph/import-jobs` accepts the upload, and the job then fails. Do not
send NDJSON import jobs to an RDF-native graph.

Reads and `POST /v1/ontology/evolve` work as usual. `GET /v1/status` reports
the model as `graph_status.truth`: `property_graph` or `rdf_native`.

The rule runs one way. A graph first written with `commit` or NDJSON accepts
RDF import too. To mix records and statements in one graph, load the records
first.

### Validation of RDF writes

SHACL shapes are validation rules for the graph. A **focus node** is a record
that a shape checks.

In `reject` mode, RDF import and SPARQL Update check each write before it is
applied. The check covers every focus node the new statements can affect. That
includes a record that points at a changed value.

A violation returns `400`, and the graph does not change.

Two refusals belong to this check:

- `shapes_not_write_enforceable`: the shape set has an `sh:sparql` constraint
  or a recursive shape reference.
- `shacl_write_validation_budget`: one write affects more than 250,000 focus
  nodes. The limit on index lookups to find them is 2,000,000.

Publish the shapes in `warn` mode for a load that meets either. Or split the
write into smaller requests when the budget is the limit.

`POST /v1/schema/publish` reports this before the first write. Its
`write_enforcement` field lists each root shape and whether writes can be
checked against it. It also names the constraint that prevents a check.

On an RDF-native graph, a `reject` publication of such a shape set returns
`400 shapes_not_write_enforceable`. The active shapes stay unchanged.

## Bulk NDJSON ingest

For a large load of records, send newline-delimited JSON (NDJSON) to
`POST /v1/graph/import-jobs`. Each line is one link or one record's fields. The
shapes match the items of `triplets` and `entity_properties` in a commit.

Save the lines as `records.ndjson`:

```json
{"source": {"type": "Ticket", "key": "4823", "name": "CSV export times out"}, "relation": "REPORTED_AS", "target": {"type": "Bug", "key": "DATA-58", "name": "export runs on one thread"}, "evidence": "Support triage, 4 Sep"}
{"type": "Ticket", "key": "4823", "name": "CSV export times out", "properties": {"description": "Exports with more than 10,000 rows stop after 30 seconds."}}
```

Submit the job, then poll it with the `job_id` from the response:

```sh
curl --fail-with-body -sS -X POST "$LBB_URL/v1/graph/import-jobs?graph=support" \
  -H "Authorization: Bearer $LBB_API_KEY" \
  -H "Idempotency-Key: support-import-1" \
  -H "Content-Type: application/x-ndjson" \
  --data-binary @records.ndjson

curl --fail-with-body -sS "$LBB_URL/v1/graph/import-jobs?graph=support&job_id=<job_id>" \
  -H "Authorization: Bearer $LBB_API_KEY"
```

The server checks the graph incarnation before storing the upload. A
background worker then commits it in groups and saves a checkpoint after each
group. A never-created graph can be created by the job. A deleted graph returns
`409 graph_epoch_changed` at submission, before an upload or job is stored.

To reset an existing graph, wait for
`POST /v1/graph/delete?graph=support&confirm=support` to succeed, call
`POST /v1/graph/create?graph=support`, install its schema, then submit with a
new `Idempotency-Key`. New jobs remain bound to the incarnation accepted at
submission, including on retries. A deletion racing submission can still stop
an accepted job; it cannot redirect that job into a recreated graph. Reusing
an old key on a new incarnation returns `409 idempotency_conflict`. Inspect an
old job with GET instead of resubmitting it. Legacy jobs without a stored
incarnation refuse recreated graphs; their POST replay can also conflict after
upgrade.

- **States.** `queued`, `running`, `cancellation_requested`, `cancelled`,
  `succeeded` or `failed`.
- **Progress.** `progress.stage` is `reading_payload`, `committing_group`,
  `persisting_checkpoint`, `enqueueing_publication` or `completed`. While a
  group commits, progress also reports its index, record count and bytes.
- **Groups.** A group holds at most 32 MiB and up to `?batch=` records. The
  batch is 1 to 10,000, and 10,000 by default. One line can be up to 32 MiB.
- **Options.** `?strict=true` stops at the first bad line. `?observed_at=`
  records every commit at an RFC 3339 time.
- **Failure.** A failed job reports `failure` with `code`, `message`,
  `retryable`, `stage`, `attempts` and an opaque `diagnostic_id`. A storage or
  network failure can use up its retries. It is then reported as retryable,
  never as bad input.
- **Cancel.** `DELETE` with the `job_id` asks the job to stop.
- **Retention.** A job's record stays readable for 14 days after it ends.

Each group is checked against the current graph for duplicates, link
cardinality and SHACL shapes.

A succeeded job reports `committed_commit_seq`. Publication follows in the
background. To wait for it, poll `GET /v1/graph/publication-status`. Stop when
`state` is `current` and `published_seq` is at least that commit. Wait once,
after the final job.

`POST /v1/graph/import` runs the same commit loop inside the request, for
bodies up to 64 MiB. It commits `?batch=` records at a time (default 1,000,
maximum 10,000). It takes `strict` and `observed_at`, and an optional
`Idempotency-Key`. This synchronous route retains its existing automatic
creation/recreation behavior, as do synchronous commit and reload. Use the
explicit create-and-submit sequence above when switching to durable jobs.

Both NDJSON routes refuse an RDF-native graph. See
[RDF-native graphs](#rdf-native-graphs).

## Read one record

For full RDF IRIs, exact literal forms and independent claim metadata, use the additive [`records/1` read](https://docs.littlebigbrain.com/guides/records/): `GET /v1/graph/entity?version=records/1&iri=…`. The same read is `client.entities.retrieve` in TypeScript/Python and `lbb_inspect action=entity iri=…` in MCP. The default native entity view below stays unchanged.

`GET /v1/graph/entity` reads one record by `id`, by `type` and `key`, or by
`type` and `name`. It reads the same data as SPARQL.

```sh
curl --fail-with-body -sS "$LBB_URL/v1/graph/entity?graph=support&type=Ticket&key=4821" \
  -H "Authorization: Bearer $LBB_API_KEY"
```

- **Fields.** `attributes` holds the typed field values.
- **Links.** `rdf_relations.outgoing` and `rdf_relations.incoming` hold the
  current links. Each has a `relation`, the linked `entity`, the link's
  `properties` and its `evidence`.
- **Link properties.** `properties` holds the link's own field values, typed
  like `attributes`. A link without properties leaves it out. In `append`
  mode a link can be stated more than once. `properties` shows the newest
  statement, and SPARQL returns all of them. The response's
  `edge_properties` is `complete` or `partial`, by the same rules as
  `evidence` below.
- **Link cap.** `?edges=` caps each direction: default 1,000, range 1 to
  10,000. A capped direction carries `outgoing_truncation` or
  `incoming_truncation`, with `returned` and a lower bound `total_observed`.
- **Evidence.** Each link lists its evidence, newest commit first: `text`,
  `source_id`, `observation_id`, `confidence`, `commit_seq`, and a `region`
  when the evidence has one. `?evidence=` sets how many entries per link:
  default 5, range 0 to 20. A link with more carries `evidence_truncated`.
  The response's `evidence` is `complete` or `partial`. On a `partial` read,
  some links were written before the platform kept evidence in the graph, and
  an empty list does not prove a link has none. A graph created before then
  reads as `partial` until the platform rebuilds its evidence; after the
  rebuild, reads at or after its commit are `complete` and older links show
  their evidence. Ask support to rebuild a graph.
- **Consistency.** Eventual is the default. `?consistency=strong` reads your
  own write at once. `?as_of_commit_seq=` reads a retained commit.
  `snapshot.served_at_seq` names the commit read.
- **Valid time.** `?as_of=` returns `400`. Use `as_of_commit_seq`.

`unavailable_sections` lists the sections this read leaves empty:
`metadata_provenance`, `current_state`, `history` and `observations`. It also
lists `outgoing` and `incoming` when the evidence is `partial`. The links are
in `rdf_relations`. `metadata.object_kind` is `unavailable`.

A link with properties and evidence looks like this:

```json
{
  "relation": { "id": 3, "name": "REPORTED_AS" },
  "entity": { "id": "7d02…", "type": "Bug", "name": "session token expiry" },
  "properties": { "status": "confirmed" },
  "evidence": [
    {
      "text": "Support triage, 2 Sep",
      "source_id": "inline:fixture-1:0",
      "observation_id": "3e6f0c…",
      "confidence": 1.0,
      "commit_seq": 12
    }
  ]
}
```

Nulls, bytes, vectors, non-finite numbers and empty sets have no RDF form.
`attributes` leaves them out. To read an RDF resource that is not a record,
use SPARQL.

## Read consistency

After a commit, the platform publishes it for queries and search. This usually
takes a second or two. Reads take a `consistency` option:

- `eventual` is the default. It reads the latest published commit. It is fast,
  and it can be a little behind the newest write.
- `strong` also applies the newest writes. Use it to read your own write at
  once.

To wait for one write, pass its `commit_seq` as `min_indexed_seq`. The read
then covers that commit or a later one.

| Route | How to set it |
| --- | --- |
| `/sparql`, `/v1/query/sparql-text` | `?consistency=` and `?min_indexed_seq=` on the URL. A floor without `consistency` reads as `strong`. |
| `/v1/query/sparql` | `consistency` and `min_indexed_seq` in the JSON body. |
| `/v1/graph/entity` | `?consistency=` |
| `/v1/graph/summary` | `?consistency=` and `?min_indexed_seq=` |
| `/v1/ontology/conformance` | `?consistency=` |

An unknown `consistency` value returns `400`.

**Responses.** An eventual response has a `snapshot` block.
`snapshot.served_at_seq` is the commit it read. Some routes also set
`stale: true` when that commit is older than the newest one. A strong
`/v1/query/sparql-text` response has no `snapshot` block.

**Strong SPARQL.** A strong read through `/sparql` or `/v1/query/sparql-text`
serves the newest commit at once. It does not wait for publication. It never
returns an older answer as a success.

**Retries.** Three `429` codes mean the read must wait. Retry the same request
after `Retry-After`:

| `code` | When |
| --- | --- |
| `read_your_writes_pending` | The floor in `min_indexed_seq` (or `min_commit_seq` on `/v1/query/names`) is not covered yet. A floor above the newest commit also gets it, until a commit reaches it. |
| `strong_read_pending` | A strong read on the summary, conformance or structured-query route waits for publication to cover the newest commit. |
| `ingest_busy` | An eventual read on a graph that has nothing published yet. |

`curl --retry 10` retries a `429` and honors `Retry-After`.

Poll `/v1/graph/publication-status` only when a workflow needs publication
itself. An example is a search right after a write. See
[Publication status](#publication-status).

## Time travel

A read can name a past commit with `as_of_commit_seq`. The answer is then the
graph as it was at that commit. It stays the same while the commit is
retained. See [Replay an answer](https://docs.littlebigbrain.com/guides/time-travel-audit/).

| Route | Where to pass it |
| --- | --- |
| `/sparql` | `?as_of_commit_seq=` |
| `/v1/query/sparql-text` | `as_of_commit_seq` in the body |
| `/v1/query/sparql` | `as_of_commit_seq` in the body. It keeps MCP structured cursor pages on one commit. |
| `/v1/graph/entity` | `?as_of_commit_seq=` |
| `POST /v1/graph/export/job` | `?as_of_commit_seq=` |

```sh
curl --fail-with-body -sS -G "$LBB_URL/sparql?graph=support&as_of_commit_seq=1" \
  -H "Authorization: Bearer $LBB_API_KEY" \
  -H "Accept: application/sparql-results+json" \
  --data-urlencode 'query=SELECT (COUNT(*) AS ?n) WHERE { ?s ?p ?o }'
```

- **A commit after the newest one** returns `400 invalid_request`.
- **A commit that is not retained** returns
  `400 historical_generation_unavailable`, with `param` set to
  `as_of_commit_seq`. A retry does not help. The server never serves a nearby
  commit instead.
- **Valid time.** SPARQL does not rebuild the graph at a point in world time.
  `?as_of_valid_time=` returns `historical_generation_unavailable`. Only
  `as_of_commit_seq` pins a read.

## Current status diagnostics

With a matching server and SDK build, request
`GET /v1/status?version=status%2F1` for a typed `status/1` response. In TypeScript
use `client.status({version: "status/1"})`; in Python use
`client.status(version="status/1")` or typed `client.status_model()` (also
awaitable on the async client). These helpers refuse an older server's legacy
200 response instead of treating an ignored selector as current diagnostics.

`graph_status.rdf.publication_caught_up` describes RDF publication coverage.
`graph_status.class_embeddings` separately reports graph model configuration,
recipe count, and each inspected serving/building version and watermark.
`ok` means the status request completed; `complete` means diagnostic coverage,
not readiness. Unknown counts are `null`.
A missing class roster is unknown; a valid empty roster proves zero recipes.

The read inspects at most 16 recipes and 16 MiB of class control-object metadata;
a larger or unreadable inventory remains partial or unavailable. RDF control
objects have a separate 16 MiB per-object diagnostic budget. These budgets do
not limit ordinary RDF reads, embedding listing or search. Status calls no model,
performs no graph/configuration/job writes and never consults legacy BM25/ANN roots. The components share
one graph incarnation but are not an atomic snapshot across concurrent changes.

The prepared `2026-10-27` source defaults to `status/1`. Explicit
`version=legacy` is compatibility only and lasts exactly one released unified
API contract generation and is refused in its immediate successor. Release identities and migration
receipts remain a release gate; this source addition is not a deployed change.
Existing SDK `status()` calls remain unversioned/raw: they follow the server
default, which can differ on an older server. Explicit `status/1` is checked.

## Publication status

A commit is durable once it is acknowledged. Publication then makes it the
latest published data, which eventual reads and search use.
`GET /v1/graph/publication-status` reports how far publication has come. It
answers before the first publication too.

A graph that is already `current` verifies its own head, desired target and
published manifest without reading unrelated active jobs. Other states retain
the job progress and blocked-work diagnostics below.

```sh
curl --fail-with-body -sS "$LBB_URL/v1/graph/publication-status?graph=support" \
  -H "Authorization: Bearer $LBB_API_KEY"
```

| Field | Meaning |
| --- | --- |
| `state` | `current`, `queued`, `compacting`, `building`, `publishing` or `blocked`. |
| `head_seq` | The newest commit. |
| `target_seq` | The commit that publication works toward. |
| `published_seq` | The newest published commit. |
| `lag_commits` | The number of commits not yet published. |
| `current_stage` | The step in progress, when one runs. |
| `last_progress_at_micros` | When publication last made progress. |
| `retry` | `retry_after_ms`, `eventual_read_available` and a `message`. |
| `epoch` | Changes when the graph is deleted and created again. |

To wait for a write, poll until `state` is `current`. `published_seq` must be
at least the write's `commit_seq`. Stop on `blocked` and read `retry.message`.
The SDKs do this in `waitForPublished` and `wait_for_published`.

Strong reads stay exact in every state. Eventual reads serve the older
published commit until publication is current.

`GET /v1/graph/read-snapshot` shows what eventual reads serve now. It needs a
first publication.

| Field | Meaning |
| --- | --- |
| `snapshot.served_at_seq` | The commit eventual reads serve. |
| `head_seq` | The newest commit. |
| `query_lag_commits` | Commits that eventual reads do not cover yet. |
| `base_current` | `true` when the served data matches the newest commit, including schema, shape and index changes. |
| `conformance_lag_commits` | Commits that the SHACL report does not cover yet. |

## Graph activity

`GET /v1/graph/activity` lists the background work on one graph. A status
view can poll it every few seconds. `GET /version` advertises it as
`graph_activity_v1`.

```sh
curl --fail-with-body -sS "$LBB_URL/v1/graph/activity?graph=support" \
  -H "Authorization: Bearer $LBB_API_KEY"
```

| Field | Meaning |
| --- | --- |
| `idle` | `true` when no job is queued, waiting or running, and publication and embeddings are current. A failed embedding also counts as idle: no work runs until it is fixed, and its `error` names the cause. |
| `head_seq` | The newest commit. |
| `published_seq` | The newest published commit. |
| `lag_commits` | The number of commits not yet published. |
| `publication` | The `state` of [Publication status](#publication-status). |
| `write_limit` | The commits not yet published (`pending_commits`, `pending_records`, `pending_bytes`) and the limits on them. Past a `soft_pending_*` limit (256 commits, 4 M records, 1 GiB) each write waits a little, up to 1 s near the hard limit. At a `max_pending_*` limit (1,024 commits, 64 M records, 8 GiB) the server refuses new writes with a retryable `429 ingest_busy` until the index catches up. |
| `embeddings` | One entry per embedding: `state`, `lag_commits`, `progress` and `error`. |
| `items` | Up to 25 jobs. Queued, waiting and running jobs come first, newest first. |

An item is one job. `kind` names the work: `publish`, `compaction`,
`statistics`, `validation`, `embeddings`, `import`, `export`, `fork` or
`index_upgrade`. `state` is `queued`, `waiting`, `running`, `succeeded`,
`failed` or `cancelled`.

`stage` names the step in progress. For a waiting job, it names the condition.
`progress` has `done`, an optional `total` and a `unit`. The units are
`phases`, `partitions`, `lines`, `bytes`, `entities`, `commits` and `tables`.

A failed job has an `error` with a stable `code` and a short `message`. A job
that waits for a retry, or runs the try after a failed one, has one too, with
`retryable: true`. Imports and embeddings have codes of their own. Other jobs
use these:

| `code` | `message` |
| --- | --- |
| `storage_unavailable` | A request to object storage failed. |
| `write_conflict` | Another update changed the graph first. |
| `storage_data_error` | Stored data did not pass an integrity check. |
| `data_missing` | Stored data the job needs is missing. |
| `server_busy` | The server was busy with other work. |
| `job_failed` | An internal error stopped the job. |

For other detail, give the item `id` to support.

An embedding is `ready`, `backfilling`, `behind` or `failed`. While it
backfills, `progress.done` counts the instances it read.

One call reads at most 30 objects and lists nothing. Each server keeps the
jobs that finished for five minutes. A poll of an idle graph reads about five
objects.

## Back-pressure

Two kinds of busy response exist. Both are `429` with `Retry-After`. Both are
safe to retry with the same idempotent request. Retry on the HTTP status and
`Retry-After`; the `code` names the kind.

- **`429 server_busy`: this server is briefly busy.** It is short of memory,
  or of room for request bodies. That room is 128 MiB by default. Or the server that owns
  this graph's writes is too busy to take one. A refused write did not run.
- **Large queries.** A query too large for the server's free memory also gets
  `server_busy`. Queries that are already prepared are not affected. The same
  query succeeds once memory frees up.
- **`429 ingest_busy`: this graph's writes are catching up.** No server can
  accept the write until they drain. One cause is a full queue of unpublished
  writes. Others are many writers on the same graph at once, or a large
  rebuild in progress.
- **Slower writes come first.** As unpublished writes pile up, each new write
  waits a little before it commits. The wait grows with the queue. Strong reads
  still see every write; they get slower, not refused.
- **One import over many commits.** Prefer one streamed import over many
  concurrent single-record commits. Writes never block reads.

`400 scan_too_wide` is not pressure. The query's plan needs more reads than one
request may make. The same query fails the same way every time, so a retry
does not help.

To fix it, narrow the pattern. Bind a subject or a class, or split the query.
A `LIMIT` helps when the engine can apply it early. `400 result_too_large`
means the result is larger than one response may hold. It also means one join
step inside the query built too many rows, often a pattern that shares no
variable with the rest.

A successful write can carry an `X-LBB-Throttle-Ms` header and `throttle_ms`
and `drain_pressure` fields. They appear when the write waited in a queue.
Lower your write concurrency as they rise, to avoid `server_busy` and
`ingest_busy`.

## Define your own ontology

An ontology is the set of types, links and fields a graph declares.
`POST /v1/ontology/define` imports an ontology document and puts the graph on
it. Writes are then checked against your types and links.

| Body field | Meaning |
| --- | --- |
| `source` | The document text. |
| `format` | `auto` (default) detects the format. Also `spec`, `lbb_json`, `json_ld`, `turtle`, `rdf_xml`, `csv` and `tsv`. |
| `merge_default` | `true` keeps the built-in vocabulary underneath your types. |
| `dry_run` | `true` previews the result. It creates no graph and changes nothing. `?dry_run=true` does the same. |

This call declares the quickstart's schema:

```sh
curl --fail-with-body -sS -X POST "$LBB_URL/v1/ontology/define?graph=support" \
  -H "Authorization: Bearer $LBB_API_KEY" -H "Content-Type: application/json" \
  -d '{"format": "spec", "merge_default": false,
       "source": "{\"entity_types\": [\"Ticket\", \"Bug\", \"Release\"], \"relation_types\": [{\"name\": \"REPORTED_AS\", \"source\": \"Ticket\", \"target\": \"Bug\"}, {\"name\": \"FIXED_IN\", \"source\": \"Bug\", \"target\": \"Release\"}]}"}'
```

The call is safe to repeat. The script that creates a graph and defines its
schema can run on every deploy.

On an existing graph, the document is compared with the active ontology. The
response reports what happened:

- `graph_created` is true only when this call created the graph.
- `changed` is true only when a new ontology version was written. A call that
  applied something never answers `changed: false`.

| Difference from the active ontology | Result |
| --- | --- |
| none | `200`, `changed: false`. Nothing is written and `ontology_version` stays the same |
| additive: new entity types, new direct `rdfs:subClassOf` parent links, new relations, renames that keep an existing type's identity, a wider relation domain or range, a new property field | `200`, `changed: true`, with the applied entries in `changes`. Stored records keep resolving, because every existing type and relation keeps its identity |
| a relation's declared domain or range is narrower | `400`, code `ontology_restrictive_change`. The message lists the narrowed relations. Confirm a narrowing with `POST /v1/schema/publish` and `confirm_restrictive: true`, or apply it one operation at a time with `POST /v1/ontology/evolve` and `narrow_relation`, which reports the records it would affect before publishing |
| an entity type or relation the graph still defines is missing | `400`, code `ontology_identity_breaking_change`. The message lists the dropped subjects. Stored records resolve through those definitions, so define keeps them. Publish an ontology that keeps them (`merge_default: true` keeps the built-in vocabulary underneath your own), use a different graph id, or delete the graph with `POST /v1/graph/delete?confirm=<graph_id>` and define it again |
| a stated change no additive operation expresses: a property field the graph declares given a new type or a new allowed-value set, or a declared relation domain or range opened up to any type | `400`, code `ontology_unsupported_change`. The message lists the subjects. Apply them with `POST /v1/ontology/evolve`, which names each change as its own operation |

Every refusal writes nothing. A rejected call leaves the active ontology as it
was.

Two things in a document are read as absence, and the graph keeps them:

- **An omitted field.** A property field the graph holds stays, because no
  operation removes a field. Fields enter a document through the `lbb_json`
  format or through RDF data properties.
- **A different `required` flag.** A `required` flag that disagrees on an
  existing field is left alone. It is advisory, and no check acts on it.

An `owl:DatatypeProperty` imports as a property field with the matching value
type, never as a relation. So does an untyped property whose every
`rdfs:range` is a datatype.

### Evolve the schema

`POST /v1/ontology/evolve` grows a live schema one operation at a time. The
body is `{"ops": [...]}`. Operations apply in order, so a type added early is
visible to a later operation. `?dry_run=true` previews the result and the
records each change would affect.

```sh
curl --fail-with-body -sS -X POST "$LBB_URL/v1/ontology/evolve?graph=support" \
  -H "Authorization: Bearer $LBB_API_KEY" -H "Content-Type: application/json" \
  -d '{"ops": [{"op": "add_property", "name": "description", "value_type": "text"}]}'
```

| `op` | Fields | Effect |
| --- | --- | --- |
| `add_entity_type` | `name` | Declares a new type. |
| `add_super_types` | `entity_type`, `super_types` | Adds direct parent types. Each link is stored as an `rdfs:subClassOf` statement, which `entailment` reads. Declare new parents earlier in the same request. Repeating a link does nothing. Removing a parent is not additive. |
| `add_relation` | `name`, `domain`, `range`, optional `cardinality`, `inverse_name`, `transitive`, `symmetric`, `temporal_semantics`, `reducer` | Declares a new link type. |
| `widen_relation` | `relation`, `add_domain`, `add_range` | Adds existing types to a link's allowed sources or targets. |
| `add_property` | `name`, `value_type`, `required`, `allowed_values`, `unit` | Declares a field. `value_type` is `text` (default), `keyword`, `bool`, `i64`, `f64`, `date_time` or `bytes`. `allowed_values` is enforced on write for `keyword`, `text`, `i64` and `bool`. `unit` names the unit of an `i64` or `f64` field, such as `mm`, `cm`, `kg` or `m2`. |
| `set_property_constraint` | `property`, `allowed_values` | Sets or clears the allowed values of an existing field. Stored values are not checked again. |
| `set_property_unit` | `property`, `unit` | Sets the unit of an existing `i64` or `f64` field. Without `unit`, it clears it. Stored values are not converted. |
| `rename_entity_type`, `rename_relation` | `from`, `to` | Renames. Stored records keep resolving to the new name. |
| `set_relation_inverse` | `relation`, `inverse_name` | Sets a second name for reading a link in reverse. |
| `set_relation_cardinality` | `relation`, `cardinality` | `one_to_one`, `one_to_many`, `many_to_one` or `many_to_many`. A narrowing is checked against current links first. |
| `narrow_relation` | `relation`, `remove_domain`, `remove_range` | Removes allowed sources or targets. Blocked while current links use them. |
| `remove_entity_type`, `remove_relation` | `name` | Refuses new writes of the type or link. Stored data stays readable. Blocked while current data uses it. |

Each operation accepts only the fields in its row. An unknown or misspelled
field answers `400` with code `invalid_request`. The message names the field,
and nothing is written.

A blocked change reports each conflict with its `kind`, the exact `affected`
count and sample ids. Migrate or retract the affected data, then apply the
change.

## Ontology drafts

A draft proposes a schema for a new data source. Its records need not be in
the graph yet. `POST /v1/ontology/drafts` takes 1 to 100 sample records and
never imports them.

The draft stores evidence references and proposed additive operations. It
analyzes your competency questions, with query outlines. It also reports
coverage, pitfalls and a confidence. It is pinned to one commit and one
ontology version.

Check it with `/validate` at that pin. Then promote it with an
`Idempotency-Key`, or reject it with a `reason`. A stale draft fails
validation. The server does not move it to a newer commit.

## Ontology starters

A starter is a versioned base ontology for a domain: `crm`, `documents` or
`work`. Apply one to start a graph from it. The
[Ontology starters guide](https://docs.littlebigbrain.com/guides/ontology-starters/) lists their classes.

```sh
curl --fail-with-body -sS -X POST "$LBB_URL/v1/ontology/starters/apply?graph=sales" \
  -H "Authorization: Bearer $LBB_API_KEY" -H 'Content-Type: application/json' \
  -d '{ "starter": "crm" }'
```

Apply adds what the graph lacks in one ontology version. A relation the graph
already has gains the starter's classes. Applying again answers
`no_op: true`. `dry_run: true` answers the same without writing.
`expected_ontology_version` refuses with `409 conflict` when the graph moved.

A graph can hold a term differently: a property with another value type, a
relation with another cardinality or inverse, or a class with another parent.
Apply then refuses with `409 starter_conflict` and lists the terms in
`error.details.conflicts`. Nothing is written.

`GET /v1/ontology/starters` and `/detail` answer for a graph that does not
exist yet: every starter is `absent` and `ontology_version` is `null`. Apply
and update need the graph and answer `404 graph_not_found` without it.

`POST /v1/ontology/starters/update` with `{ "starter": "crm" }` files what a
`partial` graph lacks as one change suggestion, keyed `starter:crm/1.0.0`.
Asking again returns the same `suggestion_id`. An `absent` starter answers
`409 starter_not_applied`.

## Ontology change suggestions

A suggestion proposes a schema change for a person to review. Integrations,
agents and people file the same object. It holds a title, a rationale, the
evolve operations, and evidence such as a record count.

`POST /v1/ontology/suggestions` files it and stores a dry run of the change.
Filing never changes the schema. The same `key` updates the same suggestion,
so a producer can file again safely.

```sh
curl --fail-with-body -sS -X POST "$LBB_URL/v1/ontology/suggestions" \
  -H "Authorization: Bearer $LBB_API_KEY" -H 'Content-Type: application/json' \
  -d '{
    "key": "hubspot-main/deals",
    "title": "Add class Deal",
    "origin": { "kind": "integration", "id": "hubspot-main" },
    "change": [{ "op": "add_entity_type", "name": "Deal" }]
  }'
```

A person then accepts, dismisses or comments on it. Accept applies the change
to the schema as it is now. The suggestion is not pinned to a commit. Pass
`change` in the accept body to apply an edited change. A change that conflicts
with current data returns `400`, and the schema does not change.

A suggestion can also hold `facts`: links and field values in the shapes of
`POST /v1/graph/commit`, with an optional `replace_owned` and a `summary`.
Accepting commits them after the change, and the suggestion records the
commit in `facts_commit`. `change` may be empty when `facts` is set. See
[Ontology suggestions](https://docs.littlebigbrain.com/guides/ontology-suggestions/#facts).

## Export a graph

`POST /v1/graph/export/job` starts a durable export of the whole graph as
NDJSON. It keeps every record, link and field. A link gets one line per piece
of evidence it carries, so every piece comes back when you import the file into
a new graph with `POST /v1/graph/import`. Importing the same file again adds
nothing. Poll `GET /v1/graph/export/job?job_id=<id>` until it completes.

The job writes zstd-compressed NDJSON parts and a `manifest` to your stack's
storage. Its progress reports the parts with their bytes and lines, and the
exported commit. `?as_of_commit_seq=` exports a past commit. There is no
synchronous whole-graph export.

For an RDF document of the current data, run this query against `/sparql`:

```sparql
CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }
```

`Accept: application/n-triples` returns N-Triples. Any other `Accept`
returns Turtle. The result is the default RDF graph, the only one a graph
serves. Bound it with `LIMIT` and `OFFSET`, and use the export job for a whole
graph.

## Native SPARQL 1.1 Protocol

Your stack serves the SPARQL 1.1 Protocol at `/sparql`. Standard clients, such
as YASGUI, Protégé and RDFLib's `SPARQLWrapper`, connect directly.

- `GET /sparql?query=<encoded>`
- `POST /sparql` with a form body (`query=`), or with an
  `application/sparql-query` body
- SELECT and ASK results: SPARQL Results JSON (the default), XML, CSV or TSV.
  Choose with `Accept` or `?format=json|xml|csv|tsv`.
- CONSTRUCT and DESCRIBE results: Turtle (the default) or N-Triples. Choose
  with `Accept: application/n-triples` or `?format=nt|ttl`.

```sh
curl --fail-with-body -sS -G "$LBB_URL/sparql?graph=support" \
  -H "Authorization: Bearer $LBB_API_KEY" \
  -H "Accept: application/sparql-results+json" \
  --data-urlencode 'query=SELECT ?s ?p ?o WHERE { ?s ?p ?o } LIMIT 5'
```

Types, links and fields you declare have fixed IRIs:
`https://littlebigbrain.com/class/<name>`, `https://littlebigbrain.com/r/<name>`
and `https://littlebigbrain.com/p/<name>`. The names are **lowercase**.

Records written as JSON get IRIs under `https://littlebigbrain.com/e/`. IRIs
loaded through RDF import keep their original form.
[Core concepts](https://docs.littlebigbrain.com/start/concepts/#graph-queries-with-sparql) maps the names.

The [TypeScript](https://docs.littlebigbrain.com/sdks/typescript/#sparql) and [Python](https://docs.littlebigbrain.com/sdks/python/#sparql)
SDKs return parsed rows. You do not match `head.vars` to bindings by hand.

`/sparql` takes these extension parameters:

| Parameter | Meaning |
| --- | --- |
| `consistency` | `eventual` (default) or `strong`. See [Read consistency](#read-consistency). An unknown value returns `400`. |
| `min_indexed_seq` | A read floor: the read covers this commit or a later one. Without `consistency`, the read is `strong`. |
| `as_of_commit_seq` | Reads a past commit. See [Time travel](#time-travel). |
| `entailment` | `none` (default) returns the facts as written. `subclass`, `rdfs` or `owl` adds facts that follow from the schema of the commit read. An unknown value returns `400`. See [Reasoning](https://docs.littlebigbrain.com/guides/reasoning/). |
| `reason` | `true` returns an error. Stored rules already ran when the data was published. |
| `max_graph_triples` | Lowers the CONSTRUCT and DESCRIBE limit for this request. |

CONSTRUCT and DESCRIBE return at most 1,000,000 triples by default. The server
operator sets this limit with `--sparql-max-graph-triples` or
`LBB_SPARQL_MAX_GRAPH_TRIPLES`.

`max_graph_triples` can only lower the limit. A value above it returns `400`
with the server's limit. A result over the limit returns `400`. The check runs
before any byte is sent, so a `200` graph response is always complete.

Multi-hop questions use SPARQL 1.1 property paths through `/sparql` or
`/v1/query/sparql-text`. Predicate, inverse, sequence, alternative and closure
paths are supported. They read one commit and apply the same filters as other
patterns. There is no separate traversal route. Record reads stay one hop.

## SPARQL Update and graph documents

Send an `application/sparql-update` body to `/update` or `/v1/update`. Only
`INSERT DATA` is accepted. One request becomes one commit. Other update forms
return `400`: `DELETE`, `WHERE`, `CLEAR`, `DROP`, `COPY`, `MOVE`, `ADD`,
`CREATE` and `LOAD`.

```sh
curl --fail-with-body -sS "$LBB_URL/update?graph=catalog" \
  -H "Authorization: Bearer $LBB_API_KEY" \
  -H "Content-Type: application/sparql-update" \
  -H "Idempotency-Key: catalog-v1" \
  --data-binary 'INSERT DATA { <https://example.com/sku/2> <http://www.w3.org/2000/01/rdf-schema#label> "Road shoe" }'
```

A successful update returns `204` with no body. An `Idempotency-Key` makes a
retry safe, and a malformed key is rejected.

`/sparql` also accepts an `update=` form field, with the same 64 MiB limit. A
form with both `query=` and `update=` returns `400`.

`POST /rdf-graph-store?default` appends a document to the graph. It takes
`text/turtle` or `application/n-triples`.

```sh
curl --fail-with-body -sS -X POST "$LBB_URL/rdf-graph-store?default&graph_id=catalog" \
  -H "Authorization: Bearer $LBB_API_KEY" \
  -H "Content-Type: text/turtle" \
  --data-raw '@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> . <https://example.com/sku/3> rdfs:label "Trail sock" .'
```

On this route, `?graph=` names an RDF graph by IRI, as the Graph Store
Protocol defines. Use `?graph_id=` to select the little big brain graph.

- `?graph=<IRI>` returns `400`, because a graph serves the default RDF graph
  only.
- A request without `?default` or `?graph=` returns `400`.
- `PUT` and `DELETE` return `400`, and there is no `GET`. Read with SPARQL and
  retract with the graph write routes.

## Upgrade indexes on existing graphs

A server update can add optional indexes that make some queries faster. A graph
that receives no writes does not get them by itself. The index upgrade routes
build them for one graph. They never change how your data is stored.

1. **Plan.** `GET /v1/graph/index-upgrade?graph=support` returns the plan for
   `target=rdf-value-order-v3`. It lists the tables, the `missing_tables` and
   `outdated_tables`, and `max_additional_bytes`.
2. **Queue.** `POST` the same route with
   `{"dry_run": false, "target": "rdf-value-order-v3"}`. Without
   `"dry_run": false`, the POST only plans.
3. **Follow.** Poll `GET /v1/graph/index-upgrade/job?job_id=<id>` in the same
   graph. Follow `progress.next_job_id` until it is absent. Then check whether
   the last `outcome` is `complete` or `partial`, and read the plan again.

Queries keep running during the build. The new indexes take effect together
when the build publishes.

A query that a new index cannot serve runs exactly as before. A build can skip
a table that exceeds its size or memory limits. The result then reports
partial coverage.

To retry after a failure or a partial result, send a new `retry_id`. The
switch to new indexes can expire SPARQL page cursors. On `410`, restart the
listing.

Upgrade one graph first, and check its results and query speed. Then upgrade
the rest.

## Operator serving preparation

`POST /api/ops/serving-readiness?graph=<id>&epoch=<epoch>` prepares the exact
current published RDF source on the contacted serving node. It is disabled by
default (`LBB_SERVING_READINESS_ENABLED=true` opts in); SaaS callers need the
operator token and `storage_tenant_id`, while single mode uses its configured
API token. Environment keys and scoped graph tokens do not grant this SaaS
operator operation. Maintenance and ingest runtimes refuse it.

Preparation reads the graph directly, verifies a bounded set of immutable
ranges into the local cache, and checks source identity again. The response
includes source/epoch, node/boot/storage identity, warm coverage and currently
available execution slots. `prepared`, `partial`, `stale` and `unknown` are
instantaneous observations; `action_eligible` is always false. The default preparation does not reserve capacity or cache residency. Later reads retain ordinary snapshot pins.

The default and maximum call budget is 64 MiB and 10 seconds, lowered with
`max_bytes` and `max_millis`. The plan is independently limited to 4,096 extents
and 128 table entries. Repeated calls can warm a larger plan; plans exceeding
these bounds remain partial. An optional `source_digest` pins the source from
a previous response. Reference graphs, history, search, placement authority,
failure headroom, general query latency and automatic scaling are outside this
profile. No durable source or control records are changed.

The same route supports `operation=reserve|revalidate|release`. Reserve requires
the exact source digest, configured node/cell identity, fresh native placement,
and a finite admission cap. It holds one real execution slot for at most ten
seconds; revalidation cannot renew it. No token can route a query, and cache
residency and snapshot pins are not leased. All results remain
`action_eligible=false`. SaaS callers still require the operator token.

`POST /api/ops/serving-capacity` reviews up to four explicit local reservations
and ephemeral min/desired/max node intents with failure headroom. It requires a
configured operator credential, the same default-off readiness gate, and at most
16 KiB of JSON; its total budget is ten seconds. It revalidates local holds and
reports deficits, but the current best-effort inventory cannot certify fleet
completeness. It therefore offers no executable scaling or NVMe proposal. It
never scans all environments, persists intent, changes placement or provisions
nodes. These tools are preparation evidence; automatic capacity control remains
unfinished.

## Current RDF demo checks

On a matching unreleased server/client build in local single mode,
`POST /v1/demo/run?version=demo%2F1&dataset=semantic_graph` checks three exact RDF
relationships in the checked-in fixture. It requires the existing enabled demo
route; SaaS and disabled servers retain their refusal. Loading data and waiting
for RDF publication are separate steps.
The prepared `2026-10-27` source also selects this contract when query version
is omitted. Only explicit `version=legacy` retains the old demo for the first
released unified generation; its successor refuses it. Source preparation does
not establish a deployed or elapsed release window.

TypeScript `client.demoRun()` and Python `client.demo_run()` / typed
`client.demo_run_model()` explicitly send `version=demo/1` and validate the
response (also awaitable in Python). An older server's ignored selector and
legacy 200 response are refused. The guards reject empty/partial success,
inconsistent counts/outcomes, mixed epoch/commit pins and wrong exact terms.

`passed` requires all three cases at one published commit. Missing, pending,
unported, malformed, timed-out and partial cases cannot pass. `complete` means
all cases produced interpretable answers, so a fully observed wrong answer is
complete but failed. Unknown publication/case-definition fields are `null`.
Expected answers are full IRI terms; actual evidence is the exact reported
SPARQL-results JSON string, preserving lexical RDF values.

The current run calls no provider, changes no durable graph/configuration state,
enqueues no work and reads no legacy search root. It reports class embedding
checks as `not_requested`. Its bounds are three queries, two rows per case,
8 KiB query/result documents, 1 MiB ontology diagnostics and existing separate
RDF control-object/query budgets. Its five-second cooperative/elapsed budget
refuses late success; it does not forcibly preempt a running OS thread.

Unversioned responses retain legacy compatibility during source preparation.
The first unified API release must default to `demo/1`; explicit legacy lasts
only that contract generation and is refused in its immediate successor.
Release identities and caller migration receipts remain gates; this source
addition is not a deployed change. Known datasets without current definitions
return unavailable; no ranking, model readiness or broad fixture coverage is
implied by the current three-case run.

Interrupted runs withhold all case payloads, epoch/commit pins and completed
counts because they did not reach the final same-incarnation proof. The explicit
`case_payloads: "withheld"` marker and null completed count mean unknown evidence,
not zero matches or zero work. Timeout and snapshot-change issues remain distinct.
A known recreation never returns retired-incarnation rows, including after a
completed case. Only a run that reaches the final epoch check can return evidence.

Current SDK helpers first require `rdf_demo_checks_v1` from `/version`, before
any demo POST, then validate the explicit response version and complete shape.
A missing capability sends no demo/mutation POST. Capability means the protocol
is understood, not authorization, an enabled dataset route, fixture presence,
publication readiness or successful checks. Existing server gates still apply.

### Explicit bounded RDF import

`POST /v1/graph/import/rdf?expected_graph_epoch=<original-epoch>` (also
`/api/graph/import/rdf`) selects one atomic support-aware N-Triples mutation on
an existing supported graph. Keep the original epoch and required
`Idempotency-Key` unchanged when an outcome is unknown. The qualified predecessor without this binding refuses the explicit epoch;
this does not qualify arbitrary historical binaries. Use a qualified server and
never fall back by removing the selector. The selector neither creates
nor converts a graph, and no support activation endpoint is provided.

Use `Content-Type: application/n-triples` with an optional UTF-8 charset.
`format=nt`, `ntriples` and `n-triples` are equivalent optional controls. The
whole request uses application/default support. Custom owners/scopes, bulk
batch/build options, Turtle/N-Quads/TriG, relative-IRI bases, named graphs and
blank nodes are unsupported in this slice and refuse before acceptance. Source
admission keeps the existing 16 MiB mutation input, 100,000 parsed-statement
work, 4,096 distinct support-change and common state/SHACL budgets. A request
past a bound is refused as a whole; it is never silently split.

Successful selected requests return `object: "rdf_import_commit"`, an original
`receipt` (decimal-string epoch, commit sequence, visibility token, captured
schema revision, no-op and actual added/removed projection counts), and a
separate `idempotent_replay` flag. Receipt fields come from retained durable
CoreV1 acceptance and stay unchanged on replay, including after later writes
or configuration changes. Equivalent RDF order/comments/duplicates denote the
same semantic intent. A changed intent under the same key conflicts. Existing
retention is at least seven days; do not promise retries after its retained
receipt window. Publication and class embeddings follow separately. Acceptance
is not a claim that either is caught up.

Without `expected_graph_epoch`, the existing ordinary bulk-import path,
statistics, batching, parse-error handling and format support remain unchanged.
A support-enabled head continues to refuse that unselected writer. This finite
binding adds no durable-import job, large-source compatibility, SDK helper or
capability-marker promise. Larger source parity remains a separate activation,
use/migration and U2-completion gate.

## Legacy model consumer transition

API2026-10-27 source prepares one finite legacy-consumer notice; it is not yet a
published release or delivered announcement. Capability
`legacy_model_consumers_deprecation_v1` in the existing `/version` capability list
recognizes the policy/Link response headers below. `/version` adds no release,
configuration, job or transition metadata. The capability is not a graph census,
a live rollout clock or retirement completion; absence on an older server does
not prove producers stopped. Actual evidence and window state are kept in the
operational release receipt.

Deterministic `fusion`, `suggest_ranker` and BM25 `calibration` ticks/jobs/auto
selection, legacy semantic `shadow-eval` and raw old record-vector `models/query`
are deprecated for new adoption. Their selected responses add
`LBB-Legacy-Model-Policy` and a deprecation `Link`; existing bodies, statuses,
requests and retry/idempotency behavior remain unchanged. No automatic conversion
to class-declared search or invented Sunset date is provided. GET job/config and
registry history remain supported. Current managed Models/Evals/trials, shared
registry/history and class embeddings/ANN/reranking/Ask are separate.

Compatibility lasts one actually released unified generation, shared with the
status/demo/calibration transition. The immediate successor is planned to refuse
new legacy selection/replay/query, preserve configuration disable/history/job
reads and stop automatic legacy scheduling. Exact release identities, delivered
notice, caller migration, saved configuration decisions and job disposition must
be qualified before that successor release; source landing/deployment alone does
not start or complete the window. No state or model history is removed here.
