Skip to content

Engine changelog

View Markdown

Updates to the database engine, query APIs, and developer tools. For the hosted console, accounts, and billing, see the Cloud changelog.

POST /v1/query/ask now stops at a 60-second read deadline. Other reads keep 25 seconds. A question that needs several model turns and queries now has time to answer.

While the model writes a turn, the question holds no server slot. Other requests can run in that time. If the server stays full after a turn, the loop stops and returns the best rows it read. Its error says the server stayed busy. No client change is needed. The SDKs already wait up to 120 seconds.

DELETE /v1/files/expiry?sha256= removes the expires_at of a generated file, one that a workflow run wrote. The file then stays until a person deletes it, and GET /v1/files lists it. A repeat answers the same record. An upload of those bytes answers 409 file_generated. The bytes are not a source. No route writes a generated file yet.

A commit under a workflow turn must write only the properties its workflow lists in owns.properties. A property outside them answers 403 write_outside_owns with the IRIs in details, and nothing is written. A ValueSource entity may carry property, value and support whoever the owner is. A commit with property_merge: "replace" is refused too. Replace erases fields of the entity that the workflow may not own.

At first the server writes such a commit as before. It only counts what it would refuse. No client change is needed.

POST /v1/workflows/activities/ground checks values that a model proposes. Each item names a file, a place in it, a property, a value and a quote. The server answers one verdict per item, in order, with no model call.

  • Checks. The file is live and its upload is the one the item read. The place exists. The quote is in the text there. The property is in the ontology. The value takes its type, and a number comes in the unit of its property, converted with a fixed factor.
  • Places. page=N for a parsed PDF, line=a,b for text and Markdown, and row=a-b for CSV and TSV.
  • Evidence. A grounded item carries the typed value and the evidence of its quote, with the box of the lines on a PDF page.
  • Bounds. A request holds 16 items from 8 files. It scans 4 MiB, and an item scans 1 MiB of its file. A PDF’s stored pages may hold 16 MiB. A place past a bound gets scan_budget. A request past 10 seconds answers 503 ground_timeout with no verdict.
  • Values. A value is at most 100 bytes and a unit 24 bytes. Only a number is read apart from its unit. A text keeps its words.
  • Open to all. The route calls no managed model, so Free accounts use it.

Remove the uncalled Rust helpers lbb_fts::search_bm25 and GraphStore::warm_object. This is a downstream Rust source-API break for Git, path or other crate consumers; external use is unknown. The retained Bm25Index::build(documents)?.search(query, top_k) performs the same legacy BM25 operation. Current operator warming keeps its bounded graph plan; it is not equivalent to an arbitrary-key whole-object read.

No HTTP/SDK route, package version, persisted format, current RDF/class embedding search or shared ANN/reranking/Ask behavior changes. This source notice does not announce package publication, deployment or completion of a retirement window.

28 October: Hosted workers parse files in a parse slot

Section titled “28 October: Hosted workers parse files in a parse slot”

On a hosted cell the workers no longer start pdftotext and pdfimages. Each worker process has a parse slot: a container with its own user, its own process view, no network, a read-only root and a 16 MiB work directory. The slot runs the program with an empty environment, no capability, and limits on memory, CPU time, file size, open files and processes. It kills the program at its wall limit and empties the work directory after every file.

  • Checked result. The worker checks what the slot returns before it stores a page: valid UTF-8, at most 2,000 pages in order, a page size that is a number of points, line ids and texts that match their words, and boxes from 0 to 1. A result that fails a check is a parse failure and stores nothing.
  • Output cap. A file whose text output passes 128 MiB now fails with the parser wrote more than 134217728 bytes. The cap was 256 MiB.
  • New settings. LBB_PARSE_MODE and LBB_PARSE_SLOT_DIRS for the worker. LBB_SERVING_PARSE for the server: off makes the serving, ingest and maintenance processes start no Poppler. It defaults to on.
  • Roll order. Roll the pool: the slots start first, then the workers. Turn LBB_SERVING_PARSE off only after the workers parse. roll-cell.sh refuses a deploy that does not set LBB_WORKER_WORKFLOWS to include documents.parse.
  • Not covered yet. The XLSX reader still runs in the serving process.

27 October: Read XLSX workbooks by sheet and cell

Section titled “27 October: Read XLSX workbooks by sheet and cell”

GET /v1/files/sheets serves an XLSX workbook. It gives the outline of the sheets, or the rows of one sheet by A1 row. See Spreadsheets. A cell has the value its file stores and its formula apart. LBB calculates nothing.

  • Bounded read. The first read parses the workbook in a child process. The child has an empty environment.
  • Hard limits. A limit refuses the whole workbook with xlsx_refused. The limits cover the file, its expansion and its XML elements. They cover sheets, rows, columns, cells and cell text. They also cover the output and the CPU time.
  • Few parses at a time. A node parses two workbooks at once, and a tenant one. The parse ends with the read that asks for it.
  • New codes: xlsx_refused, xlsx_busy, file_not_spreadsheet and sheet_not_found.
  • Places. Evidence names a place in a sheet with …#Sales!B2:D9. The console opens the file at it. The rows template reads XLSX files and writes #Sales!A5:F5 for a row.
  • Server setting. LBB_PYTHON3 names the interpreter. The server image installs python3.
  • SDKs. files.sheets in TypeScript and Python. Helpers write and read the fragment of a place.

27 October: Grade the values no label judges

Section titled “27 October: Grade the values no label judges”

Sometimes the arms of an experiment differ on a value that the dataset does not label. A person can now grade that value in the Review inbox. The answer becomes a label of the dataset (Grade the values no label judges). Little Big Brain files these items only when the operator turns the feature on.

  • New review origin evals:datasets, kind grade, options right and wrong. ReviewEvalOrigin has the kind dataset and the fields item, property, value and stated. eval_id filters by dataset.
  • New field source on a label of a dataset item: {kind, id, at}. The kind is test or review. The latest answer of a person wins. A label without a source is never replaced.
  • Changed scoring. A property with only leave_out rows of values judges those values. Every other value stays unlabeled. Before, it was wrong. A report scored before this release reads as stale, because labels_etag now ends in #g2. The next score turn scores it again.
  • New error code eval_dataset_items_too_large (400): a label would make a dataset’s items larger than 8 MiB.
  • New flag LBB_REVIEW_EVAL_DATASETS (off by default; needs LBB_REVIEW_EVAL_ITEMS). It gates filing, never an answer.
  • New metrics lbb_review_eval_datasets_enabled{mode} and lbb_review_eval_items_waiting{origin="dataset", reason}. The counter lbb_review_eval_items_total has the origin dataset.
  • Roll first: an earlier node reads a dataset item as unknown. It answers the item with no label and drops source from labels. Roll every node before you turn the flag on.

A scanned PDF page is a picture of text. Its text layer is empty or nearly so. The documents.parse workflow now flags such a page ocr_required: true. A page gets the flag when two things hold. It has fewer than 16 characters of text. Its images cover at least 40% of it.

  • New fields: ocr_required on a page of GET /v1/files/pages. Also ocr_pages in the parse of a file and on its Document.
  • Parser id: pages with the flag are stored under poppler-<version>-scan1. Pages parsed before keep their id and no flag.
  • Console: a page that needs OCR shows a warning. The file list shows how many pages need OCR.
  • Run results: with the Rust workflow, a file of which every page needs OCR is a result with no value. OCR itself is not built.
  • New setting: LBB_PDFIMAGES names the Poppler program that lists the images. The server image and the worker image already contain it.
  • One file cannot hold the queue. A file whose parse stops goes to the back. At its third stop it is stored as failed, with the reason.
  • Roll order: roll the server first, then the workers. An old server drops the flag and stores pages without it under the new id. The new server then answers 409 for the same pages with the flag.
  • Parse programs: pdftotext and pdfimages now run with an empty environment and a fixed PATH. No key or token of the server or the worker reaches them. They also run with limits on memory, CPU time and file size. Before, they inherited the environment of their host. The server set no limit.

values.extract takes the parameter repair. With repair: 1, an input whose first answer fails a check gets one more attempt. The retry sends the same text and the failed checks. See Retry a failed reading once. repair: 0 is the default and changes nothing.

  • New parameter repair (0 or 1). It is part of the settings revision. A test on a named revision reads it from there.
  • New fields: attempt on a logged call and on a call list row (2 on the retry). attempt on a value of a test or run result. settings.repair on a test. attempt on a reason activity request.
  • New counter retried in the workflow’s status.
  • Roll first: an earlier node refuses repair on a PUT. It also refuses attempt on a reason request.

27 October: See whether a revision was tested

Section titled “27 October: See whether a revision was tested”

A revision of a values.extract workflow shows whether a workflow test read inputs with exactly its settings (See whether a revision was tested).

  • New field last_tested on GET /v1/workflows/named/revisions and …/revisions/get: {test, finished, counts} for the newest test that counts, null when no test is recorded for these settings, and no key for a workflow without tests. An experiment’s arm is such a test and adds experiment (id, arm). It shows what happened and gives no verdict. Activation is never refused.
  • A test counts when it ended done and the model read at least one input. Tests from before this release do not count. In a model experiment only arm A records a fact, because B runs another model than the workflow’s.
  • The console marks each row of the history and warns before it uses settings with no recorded test.

26 October: Compare two versions of a workflow

Section titled “26 October: Compare two versions of a workflow”

An experiment runs a values.extract workflow twice on the items of a dataset: as it is (A) and with one change (B). B is the model of one step (extract or decide) or a kept settings revision. Each output is checked against the dataset’s values. The report gives the difference with a 95% interval (Compare two versions of a workflow).

  • New routes POST /v1/evals/experiments, POST /v1/evals/experiments/estimate, GET /v1/evals/experiments, GET /v1/evals/experiments/get and POST /v1/evals/experiments/cancel.
  • New fields experiment on PUT /v1/models/choices and on a revision activation. The write applies the experiment’s B, and the server refuses it with experiment_outdated when the workflow no longer runs A.
  • New codes experiment_arms_mixed, experiment_outdated and experiments_instance_taken.
  • An experiment writes nothing to the graph and changes no workflow. Its model calls count in the environment’s monthly workflow budget, as a test’s do.

22 October: Choose the model of each workflow step

Section titled “22 October: Choose the model of each workflow step”

The AI steps of workflows can run another model, for the whole environment or for one workflow (Choose the model of a workflow step). extract is the reasoning model (values.extract reads values, the reason activity), and decide the decision model (the support of a value, the decide activity).

  • New routes GET, PUT and DELETE /v1/models/choices, with the codes model_choice_unsupported and model_choice_unavailable.
  • New fields: a logged call’s choice (why it ran its model), models on a test create to test other models, and a test’s models.
  • A run’s fingerprint names its models, so runs before and after a change have different fingerprints.

22 October: A workflow’s settings keep their history

Section titled “22 October: A workflow’s settings keep their history”

Each change of a workflow’s settings is a revision, named by a hash of the settings: the same settings always get the same id. A workflow’s history lists who changed its settings, when, and what changed, and an earlier revision can be used again (Settings history of a workflow).

  • New routes GET /v1/workflows/named/revisions, GET /v1/workflows/named/revisions/get and POST /v1/workflows/named/revisions/activate, with the codes workflow_revision_conflict, workflow_revision_stale and workflow_revision_not_found.
  • New fields: a workflow’s active_revision, a run summary’s and a test’s revision, and revision on a test create, to test earlier settings without using them.
  • PUT /v1/workflows/named and PUT /v1/workflows/triggered keep their bodies and answers.

21 October: Every error code has an entry, and a lost turn lease has its own code

Section titled “21 October: Every error code has an entry, and a lost turn lease has its own code”

Each error body’s doc_url now links to the code’s entry on Error codes: its status, its type, what it means and what to do. Each code the data plane sends has one entry, and CI checks it.

What changes in the error bodies:

  • New code turn_lease_lost (409, conflict_error, retryable: false, param: "token"). A /v1/workflows/turns/* route answers it when the token no longer holds the turn: another attempt holds it, its lease ended, it waits after a sleep or signal checkpoint, or it finished. Before, these answered 409 conflict. The SDK workers stop on both codes. A worker that calls these routes itself and stops on conflict must stop on turn_lease_lost too.
  • A 402 without a code of its own answers quota_exceeded (quota_error). Before, its code was api_error, the name of a type.
  • param names the field to change on more errors: a request body’s missing, unknown or doubled field, a missing query parameter of the workflow, starter and evals routes, the review list’s cursor, and the review answer’s Idempotency-Key.
  • request_id is in every error body, and the X-Request-Id header has the same value, also on a request refused before it reaches a route (a bad header, a busy server). A path that no route serves answers 404 not_found in the error shape, not as text.

No code changed its type: a code keeps the type it was first sent with.

5 October: Questions get one route, POST /v1/query/ask

Section titled “5 October: Questions get one route, POST /v1/query/ask”

POST /v1/query/ask answers a question in plain words. A model runs queries in a short loop, reads the rows, and answers with the records it cites.

Terminal window
curl -X POST "$LBB_URL/v1/query/ask?graph=services" \
-H "Authorization: Bearer $LBB_API_KEY" -H 'Content-Type: application/json' \
--data '{"question": "How many services does each team run?"}'

This change breaks clients of POST /v1/query/rewrite:

  • POST /v1/query/rewrite is gone. It answers 404 and names the new route.
  • The one-shot mode is gone: mode: "rewrite", run and previous answer 400. The loop always runs its queries and corrects them itself.
  • mode: "route" stays: the route alone, in about 0.3 seconds.
  • The stream events query, run, rows and repair are gone. The stream sends step per tool call, then answer.
  • history.before and history.after are gone. A comparison fills history.added, history.removed and history.changed.
  • The loop’s steps include compare, for “what changed” questions, and a describe that shows how many records hold each property and the values of small classes.

The SDKs change with it. TypeScript has query.ask and query.askStream, Python query.ask and query.ask_stream. query.rewrite and the rewrite streams are gone. MCP lbb_query mode=question always answers. To write queries in your own loop, use the tools.

A SPARQL search can now rerank its hits. Add search:rerank true to the search triples.

  • The rerank model reads the best hits with the query text. The query keeps the hits that answer it best.
  • search:relevance ?r binds the model’s answer, from 0 to 1. Order by it with ORDER BY DESC(?r).
  • A query reranks only when it asks. The graph’s rerank setting applies to POST /v1/search only.
  • See Rerank inside a query.

A search can now rerank its best hits. Jev, a decision model, reads each hit with your words. The hits that answer them come first.

  • Turn it on for a graph with PUT /v1/search/settings and {"rerank": true}. One search can set rerank for itself.
  • Each hit gets relevance, the probability that it answers the words. score stays the similarity.
  • On BEIR SciFact, nDCG@10 rose from 0.713 to 0.797 over 300 test queries. A rerank adds about 0.3 s.
  • A rerank never fails a search. Without an answer in time, the hits keep the similarity order.
  • The SDKs add searchSettings() and setSearchSettings() (search_settings() and set_search_settings() in Python). MCP’s lbb_query mode=search takes rerank.

See Search by meaning.

4 October: Questions in plain words become SPARQL queries

Section titled “4 October: Questions in plain words become SPARQL queries”

POST /v1/query/rewrite turns a question into one checked SPARQL query. With run: true, it also runs the query and returns the rows.

Terminal window
curl -X POST "$LBB_URL/v1/query/rewrite?graph=services" \
-H "Authorization: Bearer $LBB_API_KEY" -H 'Content-Type: application/json' \
--data '{"question": "How many services does each team run?", "run": true}'
  • The server describes your graph: classes, properties, schema statements and the classes you can search by meaning.
  • TypeSafe’s Jev picks the kind of query: lookup, aggregate, search, history, schema or unanswerable. Claude Sonnet 5.5 writes the query.
  • The server checks the query and adds missing PREFIX lines. A query that fails is corrected once.
  • A run records an eval trace with the question as its request.
  • previous carries earlier attempts, context carries your app’s notes, and mode: "route" returns the kind of query only.
  • TypeScript and Python add query.ask and query.rewrite. MCP lbb_query adds mode: "question".

LBB runs both models with its own keys. The question, your notes and a description of the graph, with short example values, go to the model providers. See Answer questions in plain words.

3 October: Search by meaning inside SPARQL

Section titled “3 October: Search by meaning inside SPARQL”

A SPARQL query can now search by meaning. Any other pattern on the hit filters it: a link, a value, a date or a type.

PREFIX search: <https://littlebigbrain.com/search#>
PREFIX dr: <https://littlebigbrain.com/r/>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
SELECT ?x ?score WHERE {
?x search:similarTo "card payments" ; search:score ?score .
?x dr:calls ?callee . ?callee rdfs:label "payment-service" .
}
ORDER BY DESC(?score) LIMIT 5
  • The query is a text, a record (“more like this”) or your own vector.
  • The engine picks a plan by cost. A narrow filter is read first, and only its records are scored. A broad filter is applied to the nearest part of the index.
  • The response’s search object names the plan, both costs and the time of each phase.
  • as_of_commit_seq reads the graph at an older commit.
  • FILTER(?score > 0.8) keeps only close hits.

The syntax is standard SPARQL, so existing clients send these queries unchanged. See Search by meaning.

suggest_ontology_change takes up to 128 operations in change, the same limit as the server. MCP 0.8.0 refused more than 64.

3 October: EXISTS under aggregates and in OPTIONAL filters

Section titled “3 October: EXISTS under aggregates and in OPTIONAL filters”

Under an aggregate, a BIND with EXISTS left its variable unbound. This query returned 0. Now it counts the users that are not persons:

SELECT (SUM(?p) AS ?n) WHERE {
?x a ex:User
BIND(IF(NOT EXISTS { ?x a ex:Person }, 1, 0) AS ?p)
}

Two other shapes returned an error. One is EXISTS inside a larger FILTER under an aggregate. The other is EXISTS in the filter of an OPTIONAL. Both shapes now run.

2 October: TypeScript and Python 0.17.0, MCP 0.8.0

Section titled “2 October: TypeScript and Python 0.17.0, MCP 0.8.0”

TypeScript @littlebigbrain/client and Python littlebigbrain 0.17.0 add the ontology starters. ontology.starters lists, applies and updates crm, documents and work. Typed constants give each starter term its SPARQL IRI.

A refusal now carries the reasons for each item in LbbError.details. An example is the conflicts of 409 starter_conflict.

Version 0.16.0 came out the same day. It added these methods:

  • ontology.suggestions reads and decides ontology change suggestions.
  • query.sparql takes profile and returns how the query ran. See See how a query ran.
  • plannerStats returns the statistics of the query planner.
  • modelActivity reports what each managed model did in one month.
  • workflows.deleteInstance deletes a workflow instance and its history.

Python uses snake case names, such as planner_stats. Version 0.16.0 drops ontology.induce, whose route the server no longer has.

The Python client now leaves unset optional fields out of a model request body. It sent them as null, and the server refused some requests with HTTP 400.

MCP 0.8.0 adds list_starters, apply_starter and suggest_ontology_change to lbb_configure. lbb_inspect lists suggestions with action: "ontology_suggestions".

The cast xsd:dateTime("2026-10-01T00:00:00Z") left its variable unbound. Now it returns "2026-10-01T00:00:00Z"^^xsd:dateTime, the same value as STRDT. The casts xsd:date() and xsd:time() work too.

  • A string casts to each of the three datatypes.
  • An xsd:dateTime casts to xsd:date and xsd:time.
  • An xsd:date casts to xsd:dateTime, at the start of that day.
  • xsd:string() of a date or time returns its canonical form.
  • An invalid value, such as month 13, is a type error. A BIND stays unbound.

Every cast now ignores spaces around a string, as XPath prescribes. So xsd:integer(" 42 ") returns 42.

2 October: Entailment applies inside EXISTS and NOT EXISTS

Section titled “2 October: Entailment applies inside EXISTS and NOT EXISTS”

With entailment=subclass, rdfs or owl, the pattern inside EXISTS matched only stored statements. The main pattern and MINUS used the entailed graph. Now every pattern in the query uses the entailed graph. This includes an EXISTS in a FILTER, BIND, IF, projected expression or HAVING.

Here ex:User is a subclass of ex:Person. The query now returns no users, the same answer as MINUS { ?x a ex:Person }:

SELECT ?x WHERE { ?x a ex:User FILTER NOT EXISTS { ?x a ex:Person } }

2 October: A date with a very long year is stored

Section titled “2 October: A date with a very long year is stored”

A date with a year past 9223372036854775807 failed the write or the query. An example is "99999999999999999999-01-01"^^xsd:date. Now the write stores the value unchanged, and a query reads it back. The value is out of range, so a comparison or YEAR() treats it as a type error.

2 October: GROUP BY keeps a fixed subject or object

Section titled “2 October: GROUP BY keeps a fixed subject or object”

A GROUP BY over one triple pattern ignored a fixed subject or object. This query returned each subject that has a type:

SELECT ?x (COUNT(*) AS ?n) WHERE { ?x a ex:User } GROUP BY ?x

Now it returns only the members of ex:User. The fix also applies to ?x ex:knows ?x, which repeats the grouped variable. It applies to a count sorted with ORDER BY and LIMIT, and to two joined patterns.

Search by meaning is now available for every account. Declare an embedding on a class. The platform builds one text per record from the fields you choose, embeds it with the graph’s model, and keeps the vectors current after each published commit. A search embeds your question with the same model and returns the nearest records. Each hit is checked against one published commit, so a deleted or re-typed record does not come back.

  • POST /v1/embeddings/preview shows the text of sample records and the facts you can choose. It stores nothing and calls no model.
  • PUT /v1/embeddings declares an embedding and starts the embed job. GET /v1/embeddings lists the embeddings and their state, and DELETE /v1/embeddings removes one.
  • POST /v1/search searches every searchable class. A filter limits the hits to a class and its subclasses, or to records linked to a named record. With "explain": true, the search returns its plan and calls no model.
  • PUT /v1/embeddings/model moves every class of a graph to a new model. The graph switches when every class is ready, so a search never mixes models.

The TypeScript and Python SDKs add embeddings methods, including search. The MCP server adds lbb_embeddings, lbb_embeddings_manage and lbb_embeddings_delete, and lbb_query gets mode=search. Vectors count as stored data; model tokens are not billed. See Search by meaning.

27 September: BIND(EXISTS …) returns a value in a joined group

Section titled “27 September: BIND(EXISTS …) returns a value in a joined group”

In a braced group joined with other patterns, BIND(EXISTS { … } AS ?e) left ?e unbound. Now ?e is true or false. The EXISTS uses the variables that its own group binds. NOT EXISTS works the same way.

SELECT ?k ?e WHERE {
?k ex:memberOf ex:platform .
{ ?k a ?c BIND(EXISTS { ?k ex:knows ?o } AS ?e) }
}

26 September: owl:sameAs no longer makes a record a type

Section titled “26 September: owl:sameAs no longer makes a record a type”

With entailment=owl, SELECT ?t WHERE { ex:alice a ?t } listed ex:alice as a type. It also listed each owl:sameAs alias of ex:alice. The query now lists only classes.

A BIND in a join could replace a value that another pattern bound. Now the two values must be equal, or the row is dropped. In an OPTIONAL, a left row with a different value keeps its value and stays unmatched.

26 September: JSON type hierarchies reach queries

Section titled “26 September: JSON type hierarchies reach queries”

A type’s super_types in a JSON schema publish one rdfs:subClassOf statement per parent. So does the add_super_types evolve operation. With entailment=subclass, rdfs or owl, a query for a type also matches its subtypes. SELECT ?c ?p WHERE { ?c rdfs:subClassOf ?p } lists the links.

A strong read applies a new parent at once. An eventual read applies it after the graph republishes. Republication starts when the schema change is applied. See Add a type hierarchy to JSON records.

25 September: Record-history routes removed

Section titled “25 September: Record-history routes removed”

These read routes are removed:

  • GET /v1/graph/entity/metadata and GET /v1/graph/entity/neighborhood
  • GET /v1/graph/entities/sample and GET /v1/graph/changes
  • POST /v1/query/state, /v1/query/history, /v1/query/transitions, /v1/query/why and /v1/query/conflicts

They read data that hosted graphs do not publish. So they always answered 429 ingest_busy, and a client retried them forever. They now return 404. The did_you_mean field of the error names the route to use.

Read one record with its fields and links with GET /v1/graph/entity. Read a past commit with SPARQL and as_of_commit_seq. Replay answers and history shows how.

The TypeScript and Python SDKs drop the matching methods. The MCP server drops the state, history, transitions and why actions of lbb_inspect.

Graphs have no branches. Each graph keeps one line of commits.

The branch fork, merge and delete routes are removed. So are the branch query parameter and observe (POST /v1/memory/observe). The SDKs and the MCP server drop their branch and observe methods and tools.

Items in GET /v1/graphs drop the branches field. Graph metadata drops parent. The graph delete response drops deleted_branches.

A request with branch=main still works, and the server ignores the value. Any other branch value returns 400 branch_not_supported.

6 September: OPTIONAL keeps its columns under entailment

Section titled “6 September: OPTIONAL keeps its columns under entailment”

A SPARQL query with an OPTIONAL block, run with entailment=owl or entailment=rdfs, returned rows missing variables bound by the required pattern whenever the optional pattern matched an entailed triple, and those rows collapsed into duplicates. The same happened, without entailment, for a subquery inside OPTIONAL. Every row now carries the required pattern’s bindings, and a subquery’s unprojected variables no longer take their values from the outer row.

OPTIONAL under entailment=owl also read far more than it should on hosted graphs whose ontology declares owl:sameAs identities: each row of the required pattern re-read the optional pattern’s blocks, so a small graph could exhaust its per-request read budget and answer 429 ingest_busy. The optional side now folds once for the whole result, including an OPTIONAL over an entailed-only property and the OPTIONAL { ?x ?p ?y } shape, and a query that does exceed the read budget answers 400 scan_too_wide instead of a retryable backlog code.

31 August: Import typed ontology properties

Section titled “31 August: Import typed ontology properties”

Ontology imports recognize owl:DatatypeProperty declarations as property fields. Their declared ranges map to native text, numeric, boolean, and temporal types. Object properties remain relationships between classes.

TypeScript @littlebigbrain/client 0.13.0 and Python littlebigbrain 0.13.0 add typed schemaSummary() and schema_summary() methods for inspecting a graph’s schema.

28 August: Preserve imported class hierarchies

Section titled “28 August: Preserve imported class hierarchies”

Additive ontology definitions preserve rdfs:subClassOf relationships. The ontology and schema APIs expose direct superclass identifiers and canonical query IRIs, including when imported vocabularies share local class names.

SPARQL accepts entailment=owl for supported OWL reasoning, including equivalent classes and properties, inverse and transitive relationships, and bounded property chains. Reasoning applies when you query published data.

See Reasoning & inference for the supported rules and their limits.

24 August: RDFS reasoning and clearer validation

Section titled “24 August: RDFS reasoning and clearer validation”

SPARQL accepts entailment=rdfs for subclass, subproperty, and domain/range reasoning, or entailment=subclass for the class-only subset. Asserted facts remain the default query mode.

SHACL shape publishing rejects unsupported or malformed constraints with clear errors. SPARQL constraints support declared prefixes, validation messages can include the offending values, and nested violations identify the constraint that failed.

TypeScript and Python 0.12.0 support publishing RDF SHACL shapes and reading ontology conformance reports. See Query & validate.

24 August: Clear errors for named-graph input

Section titled “24 August: Clear errors for named-graph input”

RDF imports, SPARQL writes, and Graph Store requests report unsupported named-graph input. Each database graph serves one default RDF graph. Use separate database graphs and branches for isolation, and check import error counts or enable strict import validation.

See HTTP & SPARQL.

22 August: Reusable ontology definitions and publication waiters

Section titled “22 August: Reusable ontology definitions and publication waiters”

New graphs start with an empty ontology. Reapplying the same definition is a no-op; additive definitions evolve the ontology while incompatible changes return an error. The starter vocabulary can be added explicitly with merge_default: true.

TypeScript and Python 0.11.1, and MCP 0.4.1, improve publication waiting with dedicated deadlines and retries. Waiters finish when the requested data is queryable.

21 August: SPARQL APIs and large RDF imports

Section titled “21 August: SPARQL APIs and large RDF imports”

The public query API is SPARQL, with native protocol endpoints and TypeScript, Python, and MCP clients. TypeScript and Python 0.11.0, and MCP 0.4.0, align with that query surface. See HTTP & SPARQL.

Large RDF imports can defer publication while a chunked load is in progress, then make the completed import queryable. The bulk loader supports resumable input. See Load RDF data.

Selective relationship queries and multi-hop SPARQL patterns also execute faster while preserving their results.

20 August: Reliable joins across RDF term orders

Section titled “20 August: Reliable joins across RDF term orders”

Join results are independent of the order in which RDF terms entered a graph.

19 August: Extract years and months from temporal literals

Section titled “19 August: Extract years and months from temporal literals”

YEAR() accepts xsd:gYear and xsd:gYearMonth; MONTH() accepts xsd:gYearMonth. Aggregates can use these typed values directly. An aggregate whose inputs all produce type errors returns an unbound result.

18 August: Faster filters and clearer temporal comparisons

Section titled “18 August: Faster filters and clearer temporal comparisons”

Numeric, date, and string filters avoid scanning unrelated values. Comparisons between supported temporal values distinguish an undefined comparison from a valid comparison that matches no rows.

See FILTER comparisons and datatypes.

15 August: Faster schema summaries and statistics

Section titled “15 August: Faster schema summaries and statistics”

Grouped statistics, property inventories, and schema summaries respond faster on large graphs.

Large scans use bounded working memory, and queries report explicit errors when they exceed their resource or result limits.

13 August: Faster counting and entity lookups

Section titled “13 August: Faster counting and entity lookups”

COUNT, COUNT(DISTINCT ...), and queries anchored at a known entity perform less work on large graphs.

9 August: Resume interrupted index updates

Section titled “9 August: Resume interrupted index updates”

Interrupted index updates resume their progress. Unused index data is reclaimed according to retention rules.

6 August: Incremental indexing and bounded neighborhoods

Section titled “6 August: Incremental indexing and bounded neighborhoods”

Indexes incorporate updates incrementally. Entity neighborhood reads apply explicit limits when exploring highly connected data.

Large import jobs continue across restarts and report progress. TypeScript and Python 0.9.0 stream NDJSON request bodies to reduce client memory usage.

Active SHACL shapes remain attached when an ontology evolves. Common minimum and maximum count constraints validate data on large graphs.

Reads default to the published snapshot. Applications can request strong consistency and specify the write sequence that must be visible.

See Core concepts.

The HTTP API and TypeScript and Python 0.7.0 clients support forking a graph and replacing its contents through a reload operation.

SPARQL SELECT, ordered queries with limits, and repeated queries execute more efficiently on large datasets.

Graph summary, conformance, and metadata requests respond faster. Bounded exploration responses report the limits they applied.

Overloaded requests return 429 with Retry-After. TypeScript and Python 0.6.0 retry these requests within a deadline.

Graphs export as NDJSON for backup and transfer between stacks.

10 July: Query and validate persisted graphs

Section titled “10 July: Query and validate persisted graphs”

SPARQL queries and SHACL validation operate on graphs stored in object storage. Set-valued properties support fields such as tags and categories.

6 July: Validated branch merges and historical reads

Section titled “6 July: Validated branch merges and historical reads”

Branch merges validate proposed changes and report conflicts. Rejected writes identify the violated constraint so clients can correct their input.

Applications can pin reads to a retained commit for reproducible results. See History & time travel for supported selectors and retention boundaries.

Bulk ingest throughput improved by 3x over the previous implementation.

26 June: Aggregation and repeatable imports

Section titled “26 June: Aggregation and repeatable imports”

SPARQL supports grouped COUNT, SUM, and AVG queries over typed values. NDJSON imports can use stable external keys to update existing entities, and retractions remove previously asserted facts.

Subclass reasoning lets a query for a broad class include its subclasses.

Conformance reports check published data against the active ontology and are available through the HTTP API and SDKs.

Ontologies can add entity types and widen relationship domains or ranges while preserving existing records.

Numeric and temporal literals support server-side filtering and aggregation.

Supported SHACL rules derive additional facts for querying. See Reasoning & inference for configuration and limits.

Graph snapshots keep query results consistent while new data arrives.