Skip to content

HTTP API & SPARQL protocol

View Markdown

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.

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.

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

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

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

The SDKs add a key when you pass none.

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.

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 that read or write a graph take ?graph=. The sections below and the linked guides describe the main routes in more detail.

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.
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.

Terminal window
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 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.

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.

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.

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.

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

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.

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.

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.

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.

Terminal window
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’s key resolves by name. It writes to a second record. Always send the key.
  • A backfill without keys. A backfill may know each record’s entity_id (32 hex characters) and not its key. Send that entity_id on each entity_properties item. It skips resolution by type, key and name.

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

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 in retract_entities.
  • On an RDF-native graph the commit writes the statements a records graph shows for its facts, under the same IRIs. It removes what the same writer wrote before for each subject and no longer writes. The writer is named by its owned relations and fields. entity_embeddings, preconditions and ?dry_run=true are refused there.

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

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:

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

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

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

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

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

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

Four rules apply to a large load:

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

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

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

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

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

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

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

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

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

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

Two refusals belong to this check:

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

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

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

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

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:

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

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

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

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

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

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

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

Both NDJSON routes refuse an RDF-native graph. See RDF-native graphs.

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.

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

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

A link with properties and evidence looks like this:

{
"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.

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

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

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

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

An unknown consistency value returns 400.

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

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

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

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

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

Poll /v1/graph/publication-status only when a workflow needs publication itself. An example is a search right after a write. See Publication status.

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=
Terminal window
curl --fail-with-body -sS -G "$LBB_URL/sparql?graph=support&as_of_commit_seq=1" \
-H "Authorization: Bearer $LBB_API_KEY" \
-H "Accept: application/sparql-results+json" \
--data-urlencode 'query=SELECT (COUNT(*) AS ?n) WHERE { ?s ?p ?o }'
  • A commit after the newest one returns 400 invalid_request.
  • A commit that is not retained returns 400 historical_generation_unavailable, with param set to as_of_commit_seq. A retry does not help. The server never serves a nearby commit instead.
  • Valid time. SPARQL does not rebuild the graph at a point in world time. ?as_of_valid_time= returns historical_generation_unavailable. Only as_of_commit_seq pins a read.

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.

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.

Terminal window
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.

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.

Terminal window
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.

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.

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:

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

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

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

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

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

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

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

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

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.

Terminal window
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.

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.

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.

Terminal window
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.

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.

Terminal window
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.

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.

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

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

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.

Terminal window
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.

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

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

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

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

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

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

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

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

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

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.

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.

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.

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.