Skip to content

Python — lbb

View Markdown

littlebigbrain is the Python client for little big brain. Use it in scripts, notebooks, data pipelines and Python services. Each method calls the HTTP API and returns JSON as a dict or a typed model.

This page uses the example of the quickstart: support tickets, bugs and releases. The sections from Create a client to Pin a commit form one program. Add each block to the end of one file and run it with python3.

The reference sections after them cover the rest of the client. They include the async client and typed models.

The package needs Python 3.10 or later and depends on httpx and pydantic. Source and issues: github.com/littlebigbrains/lbb-python (Apache-2.0).

Terminal window
pip install littlebigbrain

The package imports as lbb. This page describes version 0.32.0.

import json
import os
import time
from lbb import LbbClient, LbbError
lbb = LbbClient(os.environ["LBB_URL"], api_key=os.environ["LBB_API_KEY"], graph="support")

The first argument is the complete endpoint from the environment’s Connect view in the console. api_key is an environment API key. graph names the graph that the client reads and writes. Without it, the server uses main.

One client serves one graph. lbb.graph("name") returns facts, retract, delete, publication_status and wait_for_published for another graph. For the other namespaces, create a second client with graph="name".

The client holds an HTTP connection pool. Call lbb.close() when you are done. Or use the client as a context manager: with LbbClient(...) as lbb:.

Argument Default Meaning
base_url http://127.0.0.1:7400 The environment endpoint. The default is a local server.
api_key none An environment API key (lbb_sk_test_… or lbb_sk_live_…) or a single-mode token. It is sent as a bearer token.
graph the server’s main The graph, sent as ?graph=.
environment none Environment slug for an account-scoped session token. Uses the compatible stack wire parameter.
integrations_url https://api.littlebigbrain.com The integrations API that integrations calls with the same key. See Integrations for your customers.
default_consistency the server’s eventual The read consistency when a call names none. See Read your own writes.
timeout 120.0 The timeout of one attempt, in seconds.
retry_budget_ms 60000 The total time that retries of one call may take.
max_retries 6 The most retries of one call.
retry_delay 0.1 The base delay of the backoff, in seconds.
on_retry none A callback that receives a RetryEvent for each retry.
event_hooks none httpx event hooks, for logs and metrics.
transport none An httpx transport, for tests or proxies.
api_version 2026-10-21 The value of the lbb-version header. An earlier date keeps the list shapes of that date; see Lists.

A graph accepts the record types, links and fields that you declare. A write that does not fit is refused. ontology.define declares types and links and creates the graph. ontology.evolve adds fields.

lbb.ontology.define({
"format": "spec",
"merge_default": False,
"source": json.dumps({
"entity_types": ["Ticket", "Bug", "Release"],
"relation_types": [
{"name": "REPORTED_AS", "source": "Ticket", "target": "Bug"},
{"name": "FIXED_IN", "source": "Bug", "target": "Release"},
],
}),
})
lbb.ontology.evolve({
"ops": [
{"op": "add_property", "name": "description", "value_type": "text"},
{"op": "add_property", "name": "priority", "value_type": "keyword"},
],
})

Both calls are safe to repeat. define returns changed=False for a document the graph already has. It applies an additive change, such as a new type or a wider link.

define refuses a change that narrows or removes what the graph declares. The error codes are ontology_restrictive_change, ontology_identity_breaking_change and ontology_unsupported_change. Nothing is written then.

merge_default: False leaves out the built-in default types. source also accepts JSON-LD, Turtle, RDF/XML and CSV. evolve(body, dry_run=True) previews a change. Build with JSON records covers parent types, inverse links and allowed values.

A write is a commit. facts.create takes links between records (triplets) and field values (entity_properties). A record is its type plus a key, and it has a name. A link can carry evidence, the reason it was recorded.

login = {"type": "Ticket", "key": "4821", "name": "login fails after 3.1"}
invoice = {"type": "Ticket", "key": "4822", "name": "invoice PDF is blank"}
csv_export = {"type": "Ticket", "key": "4823", "name": "CSV export times out"}
auth = {"type": "Bug", "key": "AUTH-112", "name": "session token expiry"}
pdf = {"type": "Bug", "key": "BILL-7", "name": "PDF fonts missing"}
slow_export = {"type": "Bug", "key": "DATA-58", "name": "export runs on one thread"}
r32 = {"type": "Release", "key": "3.2", "name": "Release 3.2"}
r311 = {"type": "Release", "key": "3.1.1", "name": "Release 3.1.1"}
written = lbb.graph("support").facts.create({
"triplets": [
{"source": login, "relation": "REPORTED_AS", "target": auth, "evidence": "Support triage, 2 Sep"},
{"source": invoice, "relation": "REPORTED_AS", "target": pdf, "evidence": "Support triage, 3 Sep"},
{"source": csv_export, "relation": "REPORTED_AS", "target": slow_export, "evidence": "Support triage, 4 Sep"},
{"source": auth, "relation": "FIXED_IN", "target": r32, "evidence": "Release notes 3.2"},
{"source": pdf, "relation": "FIXED_IN", "target": r311, "evidence": "Release notes 3.1.1"},
],
"entity_properties": [
{**login, "properties": {"priority": "high", "description": "Since 3.1, people are sent back to the login screen right after they type their password."}},
{**invoice, "properties": {"priority": "low", "description": "Downloaded invoices open as empty pages."}},
{**csv_export, "properties": {"priority": "low", "description": "Exports with more than 10,000 rows stop after 30 seconds."}},
],
}, idempotency_key="sdk-support-1")
lbb.wait_for_published(written["commit_seq"])
print("commit", written["commit_seq"])

The response reports commit_seq, the number of the commit. wait_for_published waits until the latest published data covers that commit. Search and default reads use published data. This usually takes a second or two.

Each write carries an idempotency key. The client creates one per call, and its own retries of that call reuse it. Pass your own idempotency_key so that a rerun of your code writes nothing new.

A second call with the same key returns the first result with replayed: True. This holds even when the payload changed, so use a new key for new data.

Field writes merge: a commit that sends one field keeps the other fields. An unknown type, link or field fails the whole commit. So does a value of the wrong type. A commit body is at most 1 MiB. For more, use NDJSON import.

Declare an embedding on a type and name the fields to embed. The platform builds one text per record, embeds it and keeps the vectors current. The first build runs in the background.

TICKET = "https://littlebigbrain.com/class/ticket"
lbb.embeddings.declare(TICKET, from_=["label", "description"])
while not lbb.embeddings.get("ticket").get("serving"):
time.sleep(2)

TICKET is the global name of the type: its name in lowercase after https://littlebigbrain.com/class/. label is the record’s name. The embedding’s name defaults to the last part of the class, here ticket.

from_ is the wire field from. declare also takes name, exclude, title, model and dim. The namespace has list, preview, set_model, refresh and delete too.

Search by meaning covers each one, with the errors and the costs.

found = lbb.embeddings.search("users cannot sign in after the upgrade", top_k=3)
for hit in found["hits"]:
print(f"{hit['score']:.3f}", hit["label"])

Each hit has an iri, a label, a score and a class. The iri is the global name of the record. served_at_seq is the commit that the search checked each hit against.

filter_ keeps hits by type and by link. include=["text"] returns the embedded text of each hit. explain=True plans the search and calls no model. embedding searches one embedding by name.

A starter workflow is a template for work that follows a type. A workflow created from it reads, after each published commit, the records that the commit added or changed, and does its work. An embedding is a workflow of the starter workflow search.embed; fit from text is ontology.fit.

lbb.use_workflow_starter("search.embed", class_=TICKET, fields=["label", "description"])
ticket = lbb.workflows.named.get("ticket")
print(ticket.status.state, ticket.status.lag_commits)
for workflow in lbb.workflows.named.all():
print(workflow.name, workflow.starter)

class_ and fields form the wire field watch; params takes the starter workflow’s settings as a dict. workflow_starters() lists the starter workflows the server offers, and use_workflow_starter() creates a workflow from one or changes it. preview_triggered_workflow() shows what creating one would do without writing anything (sample=, iris= and propose= choose what it shows). These methods return the JSON answers, and AsyncLbbClient and graph(name) have them too.

lbb.workflows.named reads and changes the workflows of the graph by name and returns the generated models: list(limit=, cursor=) returns one page in the list envelope and all() iterates every workflow, get(name) reads one, put(body) creates or changes a workflow of your worker, run(name) asks it to run now, pause(name, paused=) pauses or resumes it, and delete(name, confirm=name) deletes it. queue(name, cursor=, limit=) lists what its trigger has not sent yet, runs(name, cursor=, limit=, outcome=) and all_runs(name) its runs, get_run(name, run) one run, run_stats(name, hours=) its runs by hour, run_results(name, run=, outcome=, iri=, cursor=, limit=) and all_run_results(name) what its runs did with each input, and documents(name, q=, sort=, cursor=, limit=) and all_documents(name) the documents of a document workflow. On AsyncLbbClient each method is awaited, and all(), all_runs(), all_run_results() and all_documents() are async iterators. Starter workflows covers the fields, the status and the errors.

A developer workflow (a workflow of workflow) sends the records to a workflow of your worker. Create it with workflow=, owns= and batch=, and pause or resume it with workflows.named.pause():

lbb.use_workflow_starter(
"workflow",
name="word-count",
class_="Note",
fields=["body"],
workflow={"workflow_type": "word-count", "version": "v1"},
owns={"properties": ["word_count"]},
batch=16,
)
lbb.workflows.named.pause("word-count")
lbb.workflows.named.pause("word-count", paused=False)

Your code writes what it owns with replace_owned in the body of commit (or graph(name).facts.create); the answer reports owned. ontology.suggestion_create sends facts, and an accepted suggestion has facts_commit. Developer workflows covers the message and the rules.

A workflow that works on files keeps a derivation per file, so the same bytes under a second name are worked on once. check_derivation(sha256, workflow_type, version, settings_digest) answers exists and the upload of the live file, content_uploaded_at. record_derivation(..., run_id=, outputs=, content_uploaded_at=) keeps the first record of each key. With the upload of the check, a record of a file deleted since then writes nothing. It raises LbbError 409 derivation_upload_changed. The run of a new upload does the work:

from lbb import LbbError
key = (message["origin"]["sha256"], "pdf-parse", "v1", "none")
checked = lbb.check_derivation(*key)
if not checked["exists"]:
pages = parse(key[0])
try:
lbb.record_derivation(
*key,
run_id=message_id,
outputs={"pages": pages},
content_uploaded_at=checked.get("content_uploaded_at"),
)
except LbbError as error:
if error.code != "derivation_upload_changed":
raise

A message names instances by IRI. entity_ref(iri, type, name=None) turns one into the commit reference of that record (its key is the id sentinel and the 32-hex id), so a commit reaches a keyed record too:

from lbb import entity_ref
subjects = [entity_ref(iri, "Note") for iri in message["instances"]]

In replace_owned only the id counts. A link endpoint or a field row writes its name as the record’s display name, so pass the record’s name there. entity_id_from_iri(iri) returns the id alone.

lbb.workflows runs the turns of your workflows, with the rules of the TypeScript worker. Steps save their outputs, so a turn that runs again replays them. WorkflowWorker runs on asyncio, at most concurrency turns at once. On SIGTERM it drains its held turns and releases the rest. triggered_workflow and write_owned serve developer workflows, and lbb_activities(client) calls LBB’s models from a step. The worker records what each run did with each input from write_owned, review and requeue, and sends it with the turn’s completion (Run results). Run a worker in Python shows each part.

The document workflows read the files of a graph. files uploads them and reads what the parse made of them:

answer = lbb.files.upload("manual.pdf")
sha = answer["file"]["sha256"]
files = lbb.files.list()["files"]
pages = lbb.files.pages(sha, from_=0, to=2)
for line in pages["pages"][0]["lines"]:
print(line["id"], line["box"], line["text"])

upload takes bytes with name=, or the path of a file (its name and its content type default to the path’s), of at most 1 GiB. A path is sent from the file in chunks, never held whole. The same bytes under the same name again change nothing, so a failed call is retried. language= (the natural language of the content, a BCP 47 tag such as en or de-CH, at most 64 characters) and schema= (a name or a URL, at most 256) describe the content; the file’s Document stores each one given. list() returns the newest 1,000 files; list(limit=, after=) reads one page (limit 1 to 1,000), and the answer’s next is the after of the next page, absent at the end. get(sha256) reads one file’s record, and counts() counts the files by parse status, as of the commit the published generation holds. content(sha256) returns the bytes, and delete(sha256) repeats the SHA-256 as confirm.

content holds the whole answer. Write a large file to disk as it arrives with download, ask for one byte range with range=, and read a file’s size, type and SHA-256 without its bytes with head:

written = lbb.files.download(sha, "manual.pdf") # the bytes it wrote
last_kib = lbb.files.content(sha, range=(None, 1024))
first_kib = lbb.files.content(sha, range=(0, 1023))
info = lbb.files.head(sha) # {"bytes", "content_type", "sha256"}

A range is (start, end) with both bytes included, (start, None) to the end, or (None, n) for the last n bytes. A range with no byte of the file raises LbbError 416 range_not_satisfiable. A page range is 0-based with to excluded. Also on AsyncLbbClient and graph(name).files. From documents to a graph creates the document workflows and reads the links they write.

Search finds a starting point. A graph query follows the stored links from it to the answer. The query language is SPARQL 1.1, the W3C standard for graph queries. Core concepts explains the syntax.

ticket = found["hits"][0]["iri"]
read_at = found["served_at_seq"]
query = f"""
PREFIX dr: <https://littlebigbrain.com/r/>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
SELECT ?bug ?release WHERE {{
<{ticket}> dr:reported_as ?b .
?b rdfs:label ?bug ; dr:fixed_in ?r .
?r rdfs:label ?release .
}}"""
rows = lbb.sparql(query, as_of_commit_seq=read_at).rows()
print(rows, "read at commit", read_at)

With a real embedding model, the login ticket comes first. On a new graph, the program then prints:

[{'bug': 'session token expiry', 'release': 'Release 3.2'}] read at commit 1

as_of_commit_seq runs the query at the commit the search read. Search and query then agree on one version of the data.

Store read_at with the answer. Later writes do not change that commit. The same query with the same as_of_commit_seq returns the same rows while the commit is retained.

r321 = {"type": "Release", "key": "3.2.1", "name": "Release 3.2.1"}
hotfix = lbb.graph("support").facts.create({
"triplets": [{"source": auth, "relation": "FIXED_IN", "target": r321, "evidence": "Release notes 3.2.1"}],
}, idempotency_key="sdk-support-2")
lbb.wait_for_published(hotfix["commit_seq"])
latest = lbb.sparql(query)
replay = lbb.sparql(query, as_of_commit_seq=read_at)
print(len(latest), len(replay))

With the login ticket as the first hit, this prints 2 1. The latest data names two releases. The replay reads commit 1, which names one.

A pin past the newest commit returns 400 invalid_request. A commit that is no longer retained returns 400 historical_generation_unavailable. The server never answers from a nearby commit instead. Replay an answer covers retention.

lbb.sparql(query, …) runs a SPARQL 1.1 SELECT or ASK query. It returns SparqlResults. The examples in this and the next sections use the support graph from the program above.

results = lbb.sparql("""
SELECT ?ticket ?priority WHERE {
?t <http://www.w3.org/2000/01/rdf-schema#label> ?ticket ;
<https://littlebigbrain.com/p/priority> ?priority .
} ORDER BY ?ticket
""")
print(results.vars)
for row in results:
print(row["ticket"], row["priority"])
print(lbb.sparql("ASK { ?t <https://littlebigbrain.com/r/reported_as> ?b }").boolean)
Attribute Meaning
vars The selected variable names.
rows() Each binding as a {variable: value} dict. Iterating the results yields the same dicts, and len() counts them.
bindings The typed terms, with type, value, datatype and xml:lang.
boolean The answer of an ASK query, and None for SELECT.
row_page returned, total, has_more and the paging window.
snapshot For an eventual or pinned read, the commit it read as served_at_seq. None for a plain strong read.
next_cursor The cursor for the next page, when you asked for cursor pages.
trace_id The eval trace the server recorded, when you passed request.
search How a search inside the query ran. See Search inside a query. None for a query without a search.
profile What the server measured, when you passed profile=True.

sparql takes these keyword arguments:

Argument Meaning
as_of_commit_seq Run the query at this retained commit.
entailment "none" (default), "subclass", "rdfs" or "owl". The others add facts that follow from the schema. See Reasoning.
limit, offset Return a window of the result rows.
cursor Pages that stay on one commit. "" starts, and next_cursor continues. The query needs one indexed, ordered LIMIT of 1 to 10,000 and no OFFSET. Other queries return 400 invalid_pagination_cursor; page them with limit and offset.
consistency, min_indexed_seq See Read your own writes.
request The user’s words behind the query. The server records an eval trace, and trace_id names it.
profile True returns the server’s measurements in profile.
reason True returns a typed error. Stored rules already run when data is published.

query.sparql(...) is the same method.

CONSTRUCT and DESCRIBE return RDF documents, so sparql refuses them with 400. Run them on the native /sparql endpoint. See Native SPARQL protocol.

A query can find records by meaning. The pattern ?x search:similarTo "text" binds the records nearest to the text. The rest of the query then filters, joins and orders them like other rows. search:score binds the score. The prefix search: is https://littlebigbrain.com/search#, and the graph needs an embedding.

near = lbb.sparql("""
PREFIX search: <https://littlebigbrain.com/search#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
SELECT ?ticket ?score WHERE {
?t search:similarTo "sign-in fails" ;
search:score ?score ;
rdfs:label ?ticket .
} ORDER BY DESC(?score) LIMIT 3
""")
print(near.rows(), near.search["plan"], near.search["complete"])

search reports how the search ran: the plan (nearest, filter_first or search_first), the hits asked for (top) and bound (hits), complete, and lag_commits, how far the vectors trail the commit the query read. Search inside a SPARQL query lists the other patterns and explains the plans. A search query with cursor returns 400. Page it with search:top and LIMIT.

lbb.query.ask(question) answers a question in plain words. The server describes the graph. A decision model picks the kind of question. A reasoning model runs queries in a short loop, reads their rows, and answers.

result = lbb.query.ask("Which release fixed the login bug?")
if result.answer is None:
print("the loop stopped:", result.error)
print(result.answer, result.citations)
for row in result.rows:
print(row)
Method Returns
query.ask(...) Sends the question to POST /v1/query/ask. The keyword arguments are context, route, mode, limit, as_of_commit_seq, today, include_grounding, anchor (the IRIs of records the user picked) and timeline (the commit of each date). The result has answer (the text), citations, chart (how to draw the rows, or None), steps, rows and vars of the query that holds the answer, route, query, rationale, error, trace_id, linked (the names the server found), anchors and history. response holds the whole response. mode="route" returns the route alone.
query.ask_stream(...) Takes the arguments of query.ask and yields one event per step: grounding, route, step per tool call, answer, and last done with the response. An error event raises LbbError. Close the generator to stop the server’s work.
query.names(text, limit=...) The records a question or a list of names names: candidates with text, iri, label, class, score and by, the one to prefer first. No model call.
query.describe(question=..., classes=..., properties=...) The classes and properties a question needs, with how many sampled instances hold each property and the values of small classes, as JSON and as text. No model call.
query.commit_at(date=...) or query.commit_at(moment=...) The commit of a date or a moment: as_of_commit_seq, committed_at and resolved_by, or a note.
query.compare(query, before=..., after=..., key=..., max_rows=..., limit=..., cursor=...) Runs one SELECT at two points and returns added, removed and changed with totals, truncated and next_cursor.

The async client has the same methods: coroutines, and an async generator for ask_stream. trace_id names the eval trace of the query that holds the answer, with the question as its request. Answer questions in plain words covers the routes, the options and the limits. It also lists the data that goes to the model providers.

A judge model checks a sample of the model calls LBB makes for a graph: the search rerank, the query router and rewriter, the ontology fit and the eval labels. lbb.checks reads the checks. A person can agree with a check or correct it, and the review becomes the call’s ground truth. Check model quality explains the jobs and the scores.

month = lbb.checks.summary()
print(month["judge"], month["jobs"])
checks = lbb.checks.list(verdict="wrong", reviewed=False)["checks"]
for check in checks:
print(check["call"], check["summary"], check["judge"]["reason"])
lbb.checks.review(
checks[0]["call"],
agree=False,
verdict="right",
note="The first hit answers the question.",
)
Method Returns
checks.summary(month=) One month per job and model: the checks, the score, right, partly and wrong, and the reviews. Also the judge’s agreement with people and today’s budget.
checks.list(...) The month’s checks, newest first. job, month, verdict, reviewed, after and limit filter and page.
checks.review(call_id, agree=, ...) agree=True keeps the judge’s verdict. agree=False with verdict corrects it, with an optional score, reference and note. Returns the check.
checks.export(job=, month=) The month’s checks as a list of parsed JSON lines, each {"call": ..., "check": ...}.
checks.calls(...) The call log, newest first. job, day, checked, after and limit filter and page.
checks.call(call_id) One call with its input, its output and its check.
checks.check_call(call_id) Asks the judge to check one call now. Returns {"queued": True}.

A search tuning session tests other search settings on the graph’s own searches. The judge grades the hits, and the session proposes the settings that rank best. The settings change only when you apply the proposal.

session = lbb.embeddings.search_tuning.start()
while session["status"] in ("queued", "running"):
time.sleep(10)
session = lbb.embeddings.search_tuning.get(session["id"])
if session.get("proposal"):
lbb.embeddings.search_tuning.apply(session["id"])

search_tuning.list(limit=) lists the sessions, newest first. embeddings.set_search_settings changes the settings by hand. It takes rerank, rerank_depth, blend and probe_factor. A setting you leave at None keeps its value. lbb.RESET sets one back to its default, and the client sends it as JSON null:

from lbb import RESET
lbb.embeddings.set_search_settings(rerank_depth=60, blend=RESET)

checks.check_call and search_tuning.start spend the platform’s judge budget. The client sends them once unless you pass options={"retry": True}. The async client has every method as a coroutine.

Each use of a model on a graph (question answers ask, query routing route, search rerank rerank, ontology fit fit, eval labels label) runs one model. A trial tests another model on the use’s checked calls and scores both models against the same ground truth. When the candidate meets the bar, switch the use to it, and revert in one step. These methods return typed models, so you read trial.report.qualifies, not dictionary keys. Check model quality explains the scores and the bar.

# What each use runs now, and the models you can test.
options = lbb.models.trials.options()
# Test Claude Haiku 5.5 at low effort on question answers.
started = lbb.models.trials.create(
candidate={"provider": "anthropic", "model": "claude-haiku-5-5", "effort": "low"},
jobs=["ask"],
)
# The trial runs on the server. Wait until it compared the checked calls.
trial = lbb.models.trials.wait(
started.trials[0].id,
on_update=lambda t: print(f"{t.report.calls} calls compared"),
)
report = trial.report
print(report.current.score, report.candidate.score, report.ci_low, report.ci_high)
if report.qualifies:
lbb.models.switches.create(trial=trial.id)
# Back to LBB's model.
lbb.models.switches.revert("ask")

With AsyncLbbClient, every method, wait too, is awaitable.

Method Returns
models.trials.options() Per use: the model it runs now, LBB’s model, a switch, its checked calls, and the models you can test with their efforts and prices.
models.trials.create(candidate=, jobs=, target=, days=) trials and skipped: one trial per use. An open trial of the same use and candidate comes back as it is. candidate is a dict or a ModelTrialModel.
models.trials.wait(trial_id, until=, timeout=, poll_interval=, on_update=) The trial once it compared what it can (until="compared", the default) or ended (until="ended").
models.trials.list(job=, limit=) The trials, newest first, with their reports.
models.trials.get(trial_id) One trial with its report and its compared calls.
models.trials.call(trial_id, call_id) One compared call: both answers and the ground truth.
models.trials.stop(trial_id) Stops a trial. It keeps what it compared.
models.switches.create(trial=) Switches the trial’s use to its candidate.
models.switches.list() The switched uses.
models.switches.revert(job) Puts the use back on LBB’s model.
models.choices.list(workflow=, use=, limit=, cursor=) The model of each workflow step (extract, decide, embed): the environment’s rows, and with workflow the row in force for each step of that workflow, with the models each can run (options). all pages through it.
models.choices.put(use=, model=, workflow=, idempotency_key=) Chooses the model of a step for the environment or one workflow. The next calls run it. The client sends an idempotency key unless you pass one.
models.choices.delete(use=, workflow=, idempotency_key=) Removes a choice: the step runs the environment’s model, else LBB’s. Returns ModelChoiceDeleted (deleted is true).

A report gives both models’ right share, mean score (0 to 1), cost per call and median time; the difference with its 95% interval (delta, ci_low, ci_high); and qualifies: at least 20 calls, at most 5 points worse at the low end, no more failures, and cheaper or better. A switch refuses a trial that does not qualify (trial_not_qualified) or that compared with another model than the one the use runs now (trial_outdated): start a new trial. Catch them as LbbError and read error.code.

Reads take a consistency argument:

  • "eventual" is the default. It reads the latest published commit, and the response’s snapshot["served_at_seq"] names it.
  • "strong" also applies the newest writes. Use it to read your own write at once. Its response has no snapshot block.

min_indexed_seq sets a floor: the read must cover that commit or a later one. A floor without a consistency reads strong.

note = lbb.graph("support").facts.create({
"entity_properties": [{**invoice, "properties": {"priority": "high"}}],
})
now = lbb.sparql(
"SELECT ?p WHERE { ?t <https://littlebigbrain.com/p/priority> ?p }",
consistency="strong",
min_indexed_seq=note["commit_seq"],
)
print(len(now))

Publication may not cover the floor yet. Then an eventual read returns 429 read_your_writes_pending with a Retry-After. sparql retries it within the retry budget. You can also call wait_for_published first.

pending = lbb.sparql(
"SELECT ?p WHERE { ?t <https://littlebigbrain.com/p/priority> ?p }",
consistency="eventual",
min_indexed_seq=note["commit_seq"],
)
print(pending.snapshot["served_at_seq"])

Use "strong" when you need the answer now. Use "eventual" with a floor when you must record which commit you read.

consistency and min_indexed_seq work on sparql, sparql_select, summary, query.sparql and query.structured. ontology.conformance and entities.detail take consistency only.

entities.detail(...) reads one record: its typed attributes and its current links in rdf_relations. Name it by id, by type and name, or by type and key. as_of_commit_seq reads it at a retained commit.

record = lbb.entities.detail(type="Ticket", key="4821", consistency="strong")
print(record["attributes"], len(record["rdf_relations"]["outgoing"]))

If your data is RDF already, load it as it is. facts.import_rdf sends one document in N-Triples, Turtle, N-Quads or TriG. Load RDF data walks through a full import.

turtle = """
@prefix ex: <https://example.org/> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
ex:auth-service rdfs:label "auth-service" ; ex:writesTo ex:user-db .
ex:user-db rdfs:label "user-db" .
"""
imported = lbb.graph("catalog").facts.import_rdf(
turtle,
format="turtle",
base_iri="https://example.org/",
idempotency_key="catalog-2026-09-25",
)
print(imported["triples_read"], imported["committed_commit_seq"])

The first import creates the graph with a fixed generic schema, so you declare nothing first. Source IRIs keep their original form in queries. The response reports triples_read, committed_commit_seq, error_count and the first errors.

Argument Meaning
format "ntriples" (default), "turtle", "nquads" or "trig".
base_iri The base for relative IRIs.
blank_node_scope Scopes blank-node labels to one source document. An ASCII token of letters, digits and -_.:/.
batch Statements per internal commit. Default 100,000, maximum 1,000,000.
strict True fails the request at the first bad statement. By default the importer skips it and reports it in errors.
observed_at An RFC 3339 time to record on every statement.
build False holds back publication for this request.
idempotency_key The key for this request. The client creates one if you omit it.
graph_uri Refused with 400. Each graph serves the default RDF graph only.

Keep each request under 64 MiB. N-Triples and N-Quads have one statement per line. Split a large file on line boundaries. Turtle and TriG requests must each be a complete document.

Omit blank_node_scope to keep blank-node labels the same across chunks and retries. Reuse one scope for every chunk of one source document. Documents that reuse the same labels then stay apart.

facts.import_rdf_many([doc, …], …) imports several documents in order. Only the last request triggers publication. It returns a dict with imports, final_sequence and publication. Pass final_sequence to wait_for_published.

With an idempotency_key, document n uses the key suffixed with :n. lbb.import_rdf(...) and lbb.import_rdf_many(...) do the same for the client’s graph.

query.update(text, idempotency_key=None) runs SPARQL Update on the /update endpoint of the client’s graph. The server accepts INSERT DATA, and one request is one commit. Other update forms return 400. The client sends an idempotency key, so a retry replays the write. The answer has no body, so read the new facts with a strong read.

catalog = LbbClient(os.environ["LBB_URL"], api_key=os.environ["LBB_API_KEY"], graph="catalog")
catalog.query.update(
'INSERT DATA { <https://example.org/user-db> <https://example.org/tier> "gold" }',
idempotency_key="catalog-tier-1",
)

A graph whose first write is RDF only takes RDF writes. On it, commit, facts.create, commit_dry_run, import_ndjson, retract and reload return 400 rdf_native_graph. A durable import job fails. Write JSON records first when you need both.

For many records, send newline-delimited JSON. Each line is a triplet, or a record with type, name, an optional key and properties.

facts.import_ndjson(lines) imports in one request of at most 64 MiB. The server splits the lines into bounded commits and returns committed_commit_seq, the last one.

tickets = [
{"type": "Ticket", "key": "4824", "name": "search returns stale results", "properties": {"priority": "high"}},
{"source": {"type": "Ticket", "key": "4824", "name": "search returns stale results"},
"relation": "REPORTED_AS", "target": {"type": "Bug", "key": "SRCH-9", "name": "cache not invalidated"}},
]
sync = lbb.graph("support").facts.import_ndjson(tickets, idempotency_key="tickets-4824")
print(sync["committed_commit_seq"])

For a large dataset, use submit_import_ndjson. It streams a list, an iterable or a generator into a durable import job. It never holds the whole dataset in memory. The job runs on the server and survives a lost connection.

def records():
yield {"type": "Ticket", "key": "4825", "name": "PDF export fails", "properties": {"priority": "low"}}
accepted = lbb.submit_import_ndjson(records(), idempotency_key="tickets-2026-09-25")
status = lbb.wait_for_import_job(accepted.job_id)
if status.state.value != "succeeded":
raise RuntimeError(status.failure.message if status.failure else status.state)
print(status.committed_commit_seq.root)
  • idempotency_key is required. It binds the key to the uploaded content.
  • The client uploads once and does not retry the stream. To retry, call again with a fresh iterable and the same key.
  • An empty source raises ValueError before any request.
  • get_import_job(job_id) reads the status, and cancel_import_job(job_id) cancels the job.
  • wait_for_import_job polls every second. It takes timeout and poll_interval in seconds, and raises TimeoutError after timeout.

These methods return typed models. The durable route needs the server capability durable_import_jobs_v1. The client checks GET /version first and raises LbbCapabilityError if it is missing. A request is at most 32 GiB.

Both methods take batch, the records per internal commit, up to 10,000. The default is 1,000 for import_ndjson and 10,000 for a job. strict=True fails at the first malformed line, which is skipped by default. observed_at records an RFC 3339 time on every record.

The HTTP API documents the job states and failure fields.

sparql_select(body) is a SELECT or ASK query written as JSON. It takes link patterns, typed filters, group_by, aggregates, having and order_by. Use it when you build a query from user input and want no query text.

pairs = lbb.sparql_select({
"patterns": [{"subject": {"var": "ticket"}, "predicate": "REPORTED_AS", "object": {"var": "bug"}}],
"select": ["ticket", "bug"],
"limit": 50,
})
print(len(pairs["solutions"]))

query.structured(body) sends the same request, returns a typed model and retries a retryable 429.

entities.filter_by_attributes builds that body for you. The patterns bind variables through links. where compares declared fields on those variables. op is eq (default), ne, lt, le, gt or ge.

urgent = lbb.entities.filter_by_attributes(
patterns=[{"subject": {"var": "ticket"}, "predicate": "REPORTED_AS", "object": {"var": "bug"}}],
where=[{"var": "ticket", "field": "priority", "value": "high"}],
select=["ticket", "bug"],
)
print(len(urgent["solutions"]))

A value can also be {"dateTime": "…"} or {"entity": selector}. group_keys groups by a field value or by a calendar bucket of a date field. See Query and validate for the full request.

A body can be a dict or a model from lbb.models.

ontology.view() returns the declared types, links and fields of the graph. With counts=True, each link also reports how many current links it has.

For rules across fields and links, publish SHACL shapes, the W3C standard for validation rules. schema.publish stores the shapes and a mode. schema.view() returns the active shapes and mode.

shapes = """
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix dclass: <https://littlebigbrain.com/class/> .
@prefix dr: <https://littlebigbrain.com/r/> .
<https://example.org/shapes/Ticket> a sh:NodeShape ;
sh:targetClass dclass:ticket ;
sh:property [ sh:path dr:reported_as ; sh:maxCount 1 ;
sh:message "A ticket is reported as one bug at most." ] .
"""
lbb.schema.publish({"desired_mode": "warn", "shapes": {"source": shapes, "format": "turtle"}})
report = lbb.ontology.conformance(consistency="strong", options={"max_retries": 60})
print(report.conforms, report.result_count, report.validated_at_seq.root)
if report.conforms:
lbb.schema.publish({"desired_mode": "reject", "shapes": {"source": shapes, "format": "turtle"}})
Mode Behavior
off No check at write time.
warn Writes succeed. The conformance report lists violations. Use it while you add or repair a shape.
reject A write that breaks a shape returns 400, and nothing is stored. Use it after the report is clean.

After a publication, the platform validates the graph in the background. ontology.conformance() reads the stored report for the active shapes and takes no shapes of its own.

A strong read returns a retryable 429 strong_read_pending until the report for new shapes exists. The client retries it after each Retry-After. The first report can take several seconds, so the example raises max_retries from 6.

A change of mode publishes the same shapes as a new version. Choose an enforcement mode explains when to switch.

commit_dry_run(body) runs the same checks as a commit and writes nothing. Its schema_validation lists each violation with component, focus_node, result_path and message. An agent can read that list, fix its write and retry.

preview = lbb.commit_dry_run({
"triplets": [{"source": login, "relation": "REPORTED_AS", "target": pdf}],
})
print(preview["schema_validation"]["conforms"])

ontology.evolve takes typed operations from lbb.models, each with an op field. Examples are AddEntityTypeOp, AddRelationOp, AddPropertyOp and WidenRelationOp. The models also cover the operations that narrow, rename or remove.

AdditiveOntologyEvolveRequest accepts four operations: add_entity_type, add_relation, add_super_types and widen_relation. Use it when a model or an agent proposes changes and must not narrow the schema.

With dry_run=True, evolve returns the typed diff: applied, conflicts, publishable, no_op and the next ontology_version. Nothing is written.

from lbb.models import AddEntityTypeOp, AdditiveOntologyEvolveRequest
change = AdditiveOntologyEvolveRequest(ops=[AddEntityTypeOp(op="add_entity_type", name="Component")])
diff = lbb.ontology.evolve(change, dry_run=True)
print(diff.publishable, diff.no_op, diff.ontology_version)

Ontology drafts keep a proposed schema apart from the graph. ontology.draft_create(...) stores sample evidence, competency-question analyses, proposed operations, coverage, pitfalls and confidence at one commit.

Continue with draft_validate, then draft_promote or draft_reject. draft_promote takes an idempotency key and is safe to retry.

graph(name).retract removes current links as a new commit. edges names single links. entities removes every link of a record. purge_entity_properties: True also clears the record’s fields, so it leaves search and SPARQL. Past commits keep the retracted data.

retracted = lbb.graph("support").retract({
"edges": [{"source": pdf, "relation": "FIXED_IN", "target": r311}],
})
print(retracted["commit_seq"])
Method What it does
graph(name).retract(body) or retract(body) Removes links, and optionally fields, as a new commit.
reload(lines, confirm=, dry_run=) Makes the graph match the NDJSON lines in one commit. Records missing from the lines leave current state. confirm is the graph name. dry_run=True previews the change and writes nothing.
fork_graph(src, dst) Copies a graph into a new graph as a background job. The destination must be a new graph id. The id of a deleted graph is refused. Safe to retry.
create_graph() Creates the client’s graph with an empty schema. ontology.define also creates it.
list_graphs() Lists the graphs of the environment.
graph(name).delete(confirm=) or delete_graph(confirm=) Deletes the graph, its search feedback and its pending jobs. confirm is the graph name. Safe to retry.

A reload response carries prior_commit_seq. Read the graph at that commit with as_of_commit_seq to see the state before the reload.

planned = lbb.reload(tickets, confirm="support", dry_run=True)
print(planned.prior_commit_seq)

After a commit, the platform publishes it for search and eventual reads.

Method Returns
wait_for_published(commit_seq, timeout=30.0, poll_interval=0.25) Returns when the latest published commit covers commit_seq. Raises TimeoutError after timeout seconds, and RuntimeError when publication is blocked. Waits longer when the server sends a longer retry delay.
publication_status() state (current, queued, compacting, building, publishing or blocked), head_seq, published_seq, lag_commits and retry.
read_snapshot() head_seq, snapshot.served_at_seq, query_lag_commits, base_current and conformance_lag_commits.
summary() Record, link and type counts, with the commit they describe.
schema_summary() The types and fields found in the published data, with counts.
planner_stats(cursor=None, limit=None) The statistics the query planner uses: triple counts per link and field, and the indexes present. See See how a query ran.
metadata() The ontology version and storage size of the graph.

base_current is True when the published data reflects every commit and schema change.

published = lbb.read_snapshot_model()
print(published.snapshot.served_at_seq, published.query_lag_commits, published.base_current)

Methods return parsed JSON. On a non-2xx response, they raise LbbError.

try:
lbb.graph("support").facts.create({
"triplets": [{"source": login, "relation": "BLOCKS", "target": auth}],
})
except LbbError as err:
print(err.status_code, err.code, err)
Attribute Meaning
status_code The HTTP status.
code, type, param The error from the response body. str(err) is its message.
retryable, retry_after_seconds Whether a retry can succeed, and when.
request_id, doc_url The request id to quote in support, and a link to the docs.
endpoint_hint For stack_endpoint_required and stack_endpoint_mismatch: what to fix.
details The reasons for each item of a refusal. An example is the conflicts of 409 starter_conflict.
body, error The raw response body, and its parsed error object.

The client retries these requests:

  • GET, HEAD and OPTIONS requests.
  • Writes that carry an idempotency key: commits, imports, retracts, reloads, SPARQL updates, schema publications and draft promotions. The client adds the key when you omit it.
  • The read-only POST methods query.structured, ontology.search, ontology.resolve and ontology.draft_validate.
  • Graph deletes and forks.
  • embeddings.search_tuning.apply, which sets the same settings again.
  • A retryable 429 on sparql and query.sparql. A 5xx or a transport failure is not retried, because the query could run twice.

Other POST and PUT calls are sent once. Examples are ontology.define, embeddings.declare, embeddings.search and sparql_select.

A retry follows a 429, a 5xx or a transport failure. The delay is the Retry-After header, the body’s retry_after_seconds, or a full-jitter exponential backoff. An error that the body marks retryable: false is raised at once.

retry_budget_ms bounds the total retry time, and max_retries caps the count. on_retry receives a RetryEvent with method, path, attempt, status_code, error_code, delay_seconds and elapsed_ms.

raw_request(method, path, params=, body=, options=) returns a RawLbbResponse. It has data, status_code, request_id, version, headers, attempts, retry_count and elapsed_ms.

options overrides max_retries, retry, retry_budget_ms, timeout and headers for one call. retry is True, False or "rate_limited", which retries a retryable 429 only. The typed ontology and query methods take the same options= argument.

response = lbb.raw_request("GET", "/v1/graph/summary", options={"max_retries": 2})
print(response.status_code, response.request_id, response.attempts)

AsyncLbbClient has the same constructor and the same methods as coroutines. Use it as an async context manager, or call await lbb.aclose(). submit_import_ndjson also accepts an async iterable.

import asyncio
from lbb import AsyncLbbClient
async def main():
async with AsyncLbbClient(os.environ["LBB_URL"], api_key=os.environ["LBB_API_KEY"], graph="support") as client:
found, results = await asyncio.gather(
client.embeddings.search("export is slow", top_k=2),
client.sparql("SELECT (COUNT(*) AS ?n) WHERE { ?s ?p ?o }"),
)
print([hit["label"] for hit in found["hits"]], results.rows()[0]["n"])
asyncio.run(main())

The plain methods return dicts, which suit scripts and notebooks. For application code, the generated Pydantic models in lbb.models give checked fields and editor completion.

Some methods return a model by default. They are the ontology namespace, query.structured, the import jobs, reload, create_graph, fork_graph, delete_graph and wait_for_published.

Other methods have a *_model twin:

Returns a dict Returns a model
commit, commit_dry_run commit_model, commit_dry_run_model
graph(name).facts.create, graph(name).retract graph(name).facts.create_model, graph(name).retract_model
publication_status, graph(name).publication_status publication_status_model, graph(name).publication_status_model
summary, metadata, read_snapshot, schema_summary, planner_stats, list_graphs summary_model, metadata_model, read_snapshot_model, schema_summary_model, planner_stats_model, list_graphs_model
ontology_view, ontology_conformance ontology_view_model, ontology_conformance_model
sparql_select, entities.filter_by_attributes sparql_select_model, entities.filter_by_attributes_model
entities.detail entities.detail_model
schema.view, schema.publish schema.view_model, schema.publish_model

Each typed method is annotated with its exact model. raw.model(Model) validates any raw_request response. The async client has the same helpers as coroutines.

Some models hold a commit number as a CommitSeq. Read the integer with .root, as in status.committed_commit_seq.root.

from lbb.models import GraphSummaryResponse
summary: GraphSummaryResponse = lbb.summary_model()
print(summary.entity_count, summary.ontology_version)
raw = lbb.raw_request("GET", "/v1/graph/summary")
print(raw.model(GraphSummaryResponse).entity_count)

A model also works as a request body. The client leaves out each optional field that is None. A required field that is None is sent as JSON null.

from lbb.models import OntologyEvolveRequest, WidenRelationOp
lbb.ontology.evolve(
OntologyEvolveRequest(
ops=[
WidenRelationOp(
op="widen_relation", relation="FIXED_IN", add_domain=["Ticket"]
)
]
)
)
Area Methods
Schema ontology.define, ontology.evolve, ontology.view, ontology.conformance, ontology.search, ontology.resolve, ontology.draft_*, schema.view, schema.publish; graph(name).ontology scopes each one to a graph
Ontology starters ontology.starters.list, ontology.starters.get, ontology.starters.apply, ontology.starters.update; the module lbb.starters: crm, documents, work
Starter workflows workflow_starters, use_workflow_starter, preview_triggered_workflow; graph(name) scopes each one to a graph; entity_ref and entity_id_from_iri
Workflows by name workflows.named.list, workflows.named.all, workflows.named.get, workflows.named.put, workflows.named.run, workflows.named.pause, workflows.named.delete, workflows.named.queue, workflows.named.runs, workflows.named.all_runs, workflows.named.get_run, workflows.named.run_stats, workflows.named.run_results, workflows.named.all_run_results, workflows.named.documents, workflows.named.all_documents
Files files.upload, files.list, files.get, files.counts, files.content, files.download, files.head, files.pages, files.delete; graph(name).files scopes each one to a graph
Suggestions ontology.suggestions, ontology.suggestion_get, ontology.suggestion_create, ontology.suggestion_validate, ontology.suggestion_accept, ontology.suggestion_dismiss, ontology.suggestion_supersede, ontology.suggestion_comment
Write graph(name).facts.create, commit, commit_dry_run, graph(name).retract, retract, reload
Import graph(name).facts.import_rdf, import_rdf, import_rdf_many, graph(name).facts.import_ndjson, import_ndjson, submit_import_ndjson, get_import_job, wait_for_import_job, cancel_import_job
Search embeddings.declare, embeddings.get, embeddings.list, embeddings.preview, embeddings.search, embeddings.set_model, embeddings.refresh, embeddings.delete, embeddings.search_settings, embeddings.set_search_settings, managed_models, model_activity
Search tuning embeddings.search_tuning.start, embeddings.search_tuning.list, embeddings.search_tuning.get, embeddings.search_tuning.apply
Model checks checks.summary, checks.list, checks.review, checks.export, checks.calls, checks.call, checks.check_call
Model choice models.trials.options, models.trials.create, models.trials.wait, models.trials.list, models.trials.get, models.trials.call, models.trials.stop, models.switches.create, models.switches.list, models.switches.revert
Query sparql, query.sparql, sparql_select, query.structured, query.update, query.ask, query.ask_stream, entities.filter_by_attributes
Records entities.detail, entities.detail_model
Graphs create_graph, list_graphs, fork_graph, delete_graph, graph(name).delete
Integrations integrations.connectors, integrations.provider_apps, integrations.create_connect_session, integrations.get_connect_session, integrations.create, integrations.list, integrations.get, integrations.set_credentials, integrations.set_settings, integrations.sync, integrations.pause, integrations.resume, integrations.delete, integrations.erase, integrations.suggestions, integrations.accept, integrations.dismiss; google_drive.authorize_url, google_drive.exchange_code and google_drive.exchange_code_async (module lbb.google_drive)
Publication wait_for_published, publication_status, read_snapshot, summary, schema_summary, metadata, status
Transport raw_request, idempotency_key, close, graph

The client has further methods for managed evals and model training. This page does not cover them.

Each environment also serves the SPARQL 1.1 Protocol at /sparql. Standard tools such as YASGUI, Protégé and RDFLib’s SPARQLWrapper connect there. It takes GET ?query=, a form POST, or an application/sparql-query body.

The Accept header selects the format. SELECT and ASK return JSON, XML, CSV or TSV. CONSTRUCT and DESCRIBE return Turtle or N-Triples.

/sparql accepts consistency, min_indexed_seq, as_of_commit_seq and entailment as query parameters. To export the graph as RDF, run a CONSTRUCT query there:

exported = lbb.raw_request(
"GET", "/sparql",
params={"query": "CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }"},
options={"headers": {"accept": "application/n-triples"}},
)
print(len(exported.data.splitlines()))

The HTTP API covers the protocol in full.

Start with the client above. The optional catalog and access helpers add namespace membership, exact-epoch adoption, bounded catalog pages and delegated read tokens. They require separately enabled server support. Shared-reference queries also require explicit access to every selected source. See How to model your app for boundaries and an example.