HTTP API & SPARQL protocol
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 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
Section titled “Discover the APIs and specifications”Fetch the 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
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
describes the /v1/integrations/* routes on api.littlebigbrain.com; see
integrations for your customers.
Conventions
Section titled “Conventions”- Base URL. Use
$LBB_URL, the complete stack endpoint from the console’s Connect view. It has the formhttps://<tenant-short-id>--<stack-slug>.db.eu.littlebigbrain.com. See Base URLs. - Auth. Send
Authorization: Bearer $LBB_API_KEYon every request.GET /versionneeds no key. - Graph.
?graph=<name>selects a graph in the stack.?graph_id=is an alias, and the default ismain. 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=mainis accepted. Any otherbranchorbranch_idvalue returns400 branch_not_supported. - Version. Prepared source contract
2026-10-27is unreleased; inspectGET /versionfor the running binary. SendLbb-Version: 2026-10-27to pin this prepared contract. Matching source SDKs send it on every request. The advertised dates are2026-07-23,2026-10-21and2026-10-27. A request without the header gets the current contract. A request with a date before2026-10-21gets the list shapes of2026-07-23(see Lists). The value is a dateYYYY-MM-DD; anything else answers400 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: zstdorgzipto 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 returns413 file_too_large.
Idempotency
Section titled “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-jobsandPOST /v1/ontology/drafts/promote. Without one they return400 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 returns400 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.
A list route returns one page in the list envelope:
{ "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
Section titled “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. |
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:
{ "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, 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. | 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. | No |
| 400 | historical_generation_unavailable |
The pinned commit is not retained. See 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. | Yes |
| 429 | read_your_writes_pending, strong_read_pending |
The read waits for a commit. See 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
Section titled “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
Section titled “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. |
| 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. |
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. |
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. |
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. |
POST /rdf-graph-store?default |
Appends a Turtle or N-Triples document to the graph. |
Query with SPARQL
Section titled “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. |
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. |
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. |
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. |
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. |
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
covers each field and each step.
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
Section titled “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:
{ "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:
{ "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.
Read records
Section titled “Read records”| Method & path | Purpose |
|---|---|
GET /v1/graph/entity |
One record: typed attributes and current links, read at one commit. See Read one record. |
There is no route that lists every record or every link. Use SPARQL and point reads.
Search by meaning
Section titled “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 covers the request fields, the response and the errors.
Workflows
Section titled “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. |
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 covers
the fields, the status and the errors, and
From documents to a graph the document
workflows.
| 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
Section titled “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. |
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
Section titled “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. |
POST /v1/ontology/evolve |
Applies ordered schema changes. ?dry_run=true previews them. See 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. |
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. |
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. |
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. |
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.
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
Section titled “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. |
GET /v1/graph/activity |
The background work on the graph, for a status view. See 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. |
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. |
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.
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
Section titled “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
Section titled “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.
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. |
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’skeyresolves byname. It writes to a second record. Always send thekey. - A backfill without keys. A backfill may know each record’s
entity_id(32 hex characters) and not itskey. Send thatentity_idon eachentity_propertiesitem. 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
Section titled “Owned outputs”A writer that derives facts from records, such as a
developer workflow, writes
them again when a record changes. Its old links and fields must go. A commit
with replace_owned does both in one step:
{ "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 inretract_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,preconditionsand?dry_run=trueare 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
Section titled “Load RDF”POST /v1/graph/import/rdf loads RDF documents. RDF is the W3C data model
behind every graph; see RDF under the hood.
The first import into a new graph creates it. Load RDF data
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:
<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:
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.ntThe 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_triplesandduplicate_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
Section titled “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/commitwithoutreplace_owned, and?dry_run=truePOST /v1/graph/importPOST /v1/graph/retractPOST /v1/graph/reloadPOST /v1/graph/export/job, which exports records. Read the statements with a SPARQLCONSTRUCTquery on/sparqlinstead.
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
Section titled “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 ansh:sparqlconstraint 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
Section titled “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:
{"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:
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,succeededorfailed. - Progress.
progress.stageisreading_payload,committing_group,persisting_checkpoint,enqueueing_publicationorcompleted. 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=truestops at the first bad line.?observed_at=records every commit at an RFC 3339 time. - Failure. A failed job reports
failurewithcode,message,retryable,stage,attemptsand an opaquediagnostic_id. A storage or network failure can use up its retries. It is then reported as retryable, never as bad input. - Cancel.
DELETEwith thejob_idasks 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.
Read one record
Section titled “Read one record”For full RDF IRIs, exact literal forms and independent claim metadata, use the additive records/1 read: 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.
curl --fail-with-body -sS "$LBB_URL/v1/graph/entity?graph=support&type=Ticket&key=4821" \ -H "Authorization: Bearer $LBB_API_KEY"- Fields.
attributesholds the typed field values. - Links.
rdf_relations.outgoingandrdf_relations.incominghold the current links. Each has arelation, the linkedentity, the link’spropertiesand itsevidence. - Link properties.
propertiesholds the link’s own field values, typed likeattributes. A link without properties leaves it out. Inappendmode a link can be stated more than once.propertiesshows the newest statement, and SPARQL returns all of them. The response’sedge_propertiesiscompleteorpartial, by the same rules asevidencebelow. - Link cap.
?edges=caps each direction: default 1,000, range 1 to 10,000. A capped direction carriesoutgoing_truncationorincoming_truncation, withreturnedand a lower boundtotal_observed. - Evidence. Each link lists its evidence, newest commit first:
text,source_id,observation_id,confidence,commit_seq, and aregionwhen the evidence has one.?evidence=sets how many entries per link: default 5, range 0 to 20. A link with more carriesevidence_truncated. The response’sevidenceiscompleteorpartial. On apartialread, 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 aspartialuntil the platform rebuilds its evidence; after the rebuild, reads at or after its commit arecompleteand older links show their evidence. Ask support to rebuild a graph. - Consistency. Eventual is the default.
?consistency=strongreads your own write at once.?as_of_commit_seq=reads a retained commit.snapshot.served_at_seqnames the commit read. - Valid time.
?as_of=returns400. Useas_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:
{ "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
Section titled “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:
eventualis the default. It reads the latest published commit. It is fast, and it can be a little behind the newest write.strongalso 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.
Time travel
Section titled “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.
| 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= |
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, withparamset toas_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=returnshistorical_generation_unavailable. Onlyas_of_commit_seqpins a read.
Current status diagnostics
Section titled “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
Section titled “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.
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
Section titled “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.
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. |
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
Section titled “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
Section titled “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:
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_createdis true only when this call created the graph.changedis true only when a new ontology version was written. A call that applied something never answerschanged: 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_jsonformat or through RDF data properties. - A different
requiredflag. Arequiredflag 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
Section titled “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.
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
Section titled “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
Section titled “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 lists their classes.
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
Section titled “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.
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.
Export a graph
Section titled “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:
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
Section titled “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 /sparqlwith a form body (query=), or with anapplication/sparql-querybody- SELECT and ASK results: SPARQL Results JSON (the default), XML, CSV or TSV.
Choose with
Acceptor?format=json|xml|csv|tsv. - CONSTRUCT and DESCRIBE results: Turtle (the default) or N-Triples. Choose
with
Accept: application/n-triplesor?format=nt|ttl.
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 maps the names.
The TypeScript and Python
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. 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. |
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. |
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
Section titled “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.
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.
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>returns400, because a graph serves the default RDF graph only.- A request without
?defaultor?graph=returns400. PUTandDELETEreturn400, and there is noGET. Read with SPARQL and retract with the graph write routes.
Upgrade indexes on existing graphs
Section titled “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.
- Plan.
GET /v1/graph/index-upgrade?graph=supportreturns the plan fortarget=rdf-value-order-v3. It lists the tables, themissing_tablesandoutdated_tables, andmax_additional_bytes. - Queue.
POSTthe same route with{"dry_run": false, "target": "rdf-value-order-v3"}. Without"dry_run": false, the POST only plans. - Follow. Poll
GET /v1/graph/index-upgrade/job?job_id=<id>in the same graph. Followprogress.next_job_iduntil it is absent. Then check whether the lastoutcomeiscompleteorpartial, 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
Section titled “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
Section titled “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
Section titled “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
Section titled “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.