Skip to content

TypeScript

View Markdown

@littlebigbrain/client is the TypeScript client for little big brain. Use it in Node.js services, API routes and edge functions. Each method calls the HTTP API and returns typed JSON.

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

The reference sections after them cover the rest of the client.

The package has no dependencies. It calls the global fetch of Node 18+, browsers and edge workers. Source and issues: github.com/littlebigbrains/lbb-typescript (Apache-2.0).

Terminal window
npm install @littlebigbrain/client

This page describes version 0.31.0.

import { LbbClient, LbbError, parseSparqlResults } from "@littlebigbrain/client";
const lbb = new LbbClient({
baseUrl: process.env.LBB_URL!,
apiKey: process.env.LBB_API_KEY!,
graph: "support",
});

baseUrl is the complete endpoint from the environment’s Connect view in the console. apiKey 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 the same namespaces for another graph, plus facts for writes. lbb.withScope({ graph: "name" }) returns a new client with the same options.

Option Default Meaning
baseUrl required The environment endpoint.
apiKey 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. An environment key already fixes its environment.
stack none Deprecated alias for environment; conflicting values fail before a request.
integrationsUrl https://api.littlebigbrain.com The integrations API that integrations calls with the same key. See Integrations for your customers.
defaultConsistency the server’s eventual The read consistency when a call names none. See Read your own writes.
timeoutMs 120000 The timeout of one attempt, including the response body. 0 disables it.
retryBudgetMs 60000 The total time that retries of one call may take.
maxRetries 6 The most retries of one call.
retryDelayMs 100 The base delay of the backoff.
fetch the global fetch Your own fetch implementation.
onRequest, onResponse, onRetry none Callbacks for request events. They never receive bodies or credentials.
apiVersion 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.

await lbb.ontology.define({
format: "spec",
merge_default: false,
source: JSON.stringify({
entity_types: ["Ticket", "Bug", "Release"],
relation_types: [
{ name: "REPORTED_AS", source: "Ticket", target: "Bug" },
{ name: "FIXED_IN", source: "Bug", target: "Release" },
],
}),
});
await 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, { dryRun: 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.

const login = { type: "Ticket", key: "4821", name: "login fails after 3.1" };
const invoice = { type: "Ticket", key: "4822", name: "invoice PDF is blank" };
const csvExport = { type: "Ticket", key: "4823", name: "CSV export times out" };
const auth = { type: "Bug", key: "AUTH-112", name: "session token expiry" };
const pdf = { type: "Bug", key: "BILL-7", name: "PDF fonts missing" };
const slowExport = { type: "Bug", key: "DATA-58", name: "export runs on one thread" };
const r32 = { type: "Release", key: "3.2", name: "Release 3.2" };
const r311 = { type: "Release", key: "3.1.1", name: "Release 3.1.1" };
const written = await 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: csvExport, relation: "REPORTED_AS", target: slowExport, 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." } },
{ ...csvExport, properties: { priority: "low", description: "Exports with more than 10,000 rows stop after 30 seconds." } },
],
},
{ idempotencyKey: "sdk-support-1" },
);
await lbb.waitForPublished(written.commit_seq);
console.log("commit", written.commit_seq);

The response reports commit_seq, the number of the commit. waitForPublished 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 idempotencyKey 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.

const TICKET = "https://littlebigbrain.com/class/ticket";
await lbb.embeddings.declare({ class: TICKET, from: ["label", "description"] });
while (!(await lbb.embeddings.get("ticket")).serving) {
await new Promise((resolve) => setTimeout(resolve, 2_000));
}

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

The namespace also has list, preview, setModel, refresh and delete. Search by meaning covers each one, with the errors and the costs.

const found = await lbb.embeddings.search({
text: "users cannot sign in after the upgrade",
top_k: 3,
});
for (const hit of found.hits) console.log(hit.score.toFixed(3), 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.

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.

await lbb.workflows.starters.use({
starter: "search.embed",
watch: { class: TICKET, fields: ["label", "description"] },
});
const ticket = await lbb.workflows.named.get("ticket");
console.log(ticket.status.state, ticket.status.lag_commits);
for await (const workflow of lbb.workflows.named.all()) {
console.log(workflow.name, workflow.starter);
}

workflows.starters.list() lists the starter workflows the server offers, and workflows.starters.use(request) creates a workflow from one or changes it. workflows.triggered.preview shows what creating one would do without writing anything. workflows.named reads and changes the workflows of the graph by name: list({ limit, cursor }) returns one page in the list envelope and all() iterates every workflow, get reads one, run asks it to run now, and delete repeats the name as confirm. use, pause and a preview without propose retry a failed call. 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. use takes its workflow, owns and batch, and workflows.named.pause(name, paused) pauses or resumes it. triggeredWorkflow() defines the workflow that gets the trigger messages, and writeOwned() commits what your code owns with replace_owned inside a step, under the step’s effect id, so a turn that runs again does not write twice. workflows.named.put creates or changes a workflow of your worker, with shards, route_by and window, and a watch or a content trigger. queue(name, { cursor, limit }) lists what the trigger has not sent yet, runs(name, { cursor, limit, outcome }) and allRuns(name) its runs, runResults(name, { run, outcome, iri, cursor, limit }) and allRunResults(name) what its runs did with each input, and documents(name, { q, sort, cursor, limit }) and allDocuments(name) the documents of a document workflow. A WorkflowWorker records those results for you: writeOwned, review of an item with a subject, and requeue report each input with the turn’s completion (Run results). workflows.derivations checks and records what one version of a workflow made of one file (check, record); derive(key, work) does the check, the work and the record. Developer workflows covers the message and the rules, with a worked example; the repository’s database/examples/starter-workflows/word-count.mjs is a runnable one.

commit and graph(name).facts.create send replace_owned and return owned; ontology.suggestions.create sends facts, and an accepted suggestion has facts_commit.

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

import { entityRef } from "@littlebigbrain/client";
const subjects = message.instances.map((iri) => entityRef(iri, "Note"));

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 as the third argument there. entityIdFromIri(iri) returns the id alone.

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

import { readFile } from "node:fs/promises";
const { file } = await lbb.files.upload(await readFile("manual.pdf"), {
name: "manual.pdf",
contentType: "application/pdf",
});
const { files } = await lbb.files.list();
const pages = await lbb.files.pages(file.sha256, { from: 0, to: 2 });
for (const line of pages.pages[0]?.lines ?? []) console.log(line.id, line.box, line.text);

upload takes a Blob, an ArrayBuffer or a Uint8Array of at most 1 GiB and sends it as the raw body; 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 as a Uint8Array, and delete(sha256) repeats the SHA-256 as confirm.

content holds the whole answer. Read a large file in chunks as they arrive with contentStream, ask for one byte range with range, and read a file’s size, type and SHA-256 without its bytes with head:

import { createWriteStream } from "node:fs";
const out = createWriteStream("manual.pdf");
for await (const chunk of lbb.files.contentStream(file.sha256)) out.write(chunk);
out.end();
const lastKiB = await lbb.files.content(file.sha256, { range: { suffix: 1024 } });
const firstKiB = await lbb.files.content(file.sha256, { range: { start: 0, end: 1023 } });
const { bytes, contentType, sha256 } = await lbb.files.head(file.sha256);

A range with no byte of the file throws LbbError 416 range_not_satisfiable. A page range is 0-based with to excluded. Also on graph(name).files. putPages(sha256, parser, { uploaded_at, status: "parsed", pages }) stores the pages a parser of your own made (PUT /v1/files/pages). uploaded_at is the record’s (get(sha256)): a file deleted and uploaded again since answers 409 file_upload_changed. The same pages again store nothing, so a retry is safe. 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.

const ticket = found.hits[0].iri;
const readAt = found.served_at_seq!;
const query = `
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 .
}`;
const { rows } = await lbb.sparqlRows({ query, as_of_commit_seq: readAt });
console.log(rows, "read at commit", readAt);

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

const r321 = { type: "Release", key: "3.2.1", name: "Release 3.2.1" };
const hotfix = await lbb.graph("support").facts.create(
{ triplets: [{ source: auth, relation: "FIXED_IN", target: r321, evidence: "Release notes 3.2.1" }] },
{ idempotencyKey: "sdk-support-2" },
);
await lbb.waitForPublished(hotfix.commit_seq);
const latest = await lbb.sparqlRows({ query });
const replay = await lbb.sparqlRows({ query, as_of_commit_seq: readAt });
console.log(latest.rows.length, replay.rows.length);

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.

The client runs SPARQL 1.1 SELECT and ASK queries. The examples in this and the next sections use the support graph from the program above.

Method Returns
sparqlRows(body, opts?) Parsed results: { vars, boolean, bindings, rows, snapshot }, with rowPage, nextCursor, traceId, search and profile when the server sends them. Retries a retryable 429.
sparqlText(body, opts?) The unparsed response. results holds SPARQL Results JSON as a string, next to row_page, snapshot and next_cursor. Retries a retryable 429.
parseSparqlResults(response) The parsed results of a sparqlText response.
query.sparql(body, opts?) Parsed results, like sparqlRows. Retries a retryable 429.
query.sparqlRaw(body, opts?) The unparsed response, like sparqlText. Retries a retryable 429.

rows flattens each binding to { variable: value }. bindings keeps the typed terms, with type, value, datatype and xml:lang. boolean is the answer of an ASK query, and null for SELECT. snapshot.served_at_seq is the commit that an eventual or pinned read used. A plain strong read returns snapshot: null.

const ask = await lbb.sparqlRows({
query: "ASK { ?t <https://littlebigbrain.com/r/reported_as> ?b }",
});
const raw = await lbb.sparqlText({ query: "SELECT (COUNT(*) AS ?n) WHERE { ?s ?p ?o }" });
console.log(ask.boolean, parseSparqlResults(raw).rows[0].n, raw.row_page.total);

The body takes these fields:

Field Meaning
query The SPARQL text, SELECT or ASK.
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. The response’s row_page reports returned, total and has_more.
cursor Pages that stay on one commit. "" starts, and next_cursor from the response continues. The query needs one indexed, ordered LIMIT of 1 to 10,000, no OFFSET, and no limit or offset field. Other queries return 400 invalid_pagination_cursor; page them with limit and offset.
request The user’s words behind the query. The server then records an eval trace, and the parsed results name it in traceId.
reason true returns a typed error. Stored rules already run when data is published.

CONSTRUCT and DESCRIBE return RDF documents, so these methods refuse 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.

const near = await lbb.sparqlRows({
query: `
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`,
});
console.log(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.

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

const result = await lbb.query.ask("Which release fixed the login bug?");
if (!result.answer) console.log("the loop stopped:", result.error);
console.log(result.answer, result.citations, result.rows);
Method Returns
query.ask(question, opts?) Sends the question to POST /v1/query/ask. Returns answer (the text), citations, chart (how to draw the rows, or null), steps, route, query, rationale, the parsed rows and vars of the query that holds the answer, error, traceId, linked (the names the server found), anchors and history. response holds the whole response. opts takes context, route, limit, asOfCommitSeq, today, includeGrounding, anchor (the IRIs of records the user picked) and timeline (the commit of each date). opts.mode: "route" returns the route alone.
query.askStream(question, opts?) 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 throws LbbError. Abort opts.signal 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.commitAt({ date } | { 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.

traceId names the eval trace of the query that holds the answer. The question is 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. 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.

const month = await lbb.checks.summary();
console.log(month.judge?.agreement, month.jobs);
const { checks } = await lbb.checks.list({ verdict: "wrong", reviewed: false });
for (const check of checks) console.log(check.call, check.summary, check.judge.reason);
await 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(opts?) The month’s checks, newest first. job, month, verdict, reviewed, after and limit filter and page.
checks.review(callId, body) { agree: true } keeps the judge’s verdict. { agree: false, verdict, score?, reference?, note? } corrects it. Returns the check.
checks.export({ job?, month? }) The month’s checks as parsed JSON lines, each { call, check }.
checks.calls(opts?) The call log, newest first. job, day, checked, after and limit filter and page.
checks.call(callId) One call with its input, its output and its check.
checks.checkCall(callId) 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.

let session = await lbb.embeddings.searchTuning.start();
while (session.status === "queued" || session.status === "running") {
await new Promise((resolve) => setTimeout(resolve, 10_000));
session = await lbb.embeddings.searchTuning.get(session.id);
}
if (session.proposal) await lbb.embeddings.searchTuning.apply(session.id);

searchTuning.list({ limit? }) lists the sessions, newest first. embeddings.setSearchSettings changes the settings by hand. It takes rerank, rerank_depth, blend and probe_factor. A field you leave out keeps its value, and null sets it back to its default:

await lbb.embeddings.setSearchSettings({ rerank_depth: 60, blend: null });

checks.checkCall and searchTuning.start spend the platform’s judge budget. The client sends them once unless you pass { retry: true }.

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. Check model quality explains the scores and the bar.

// What each use runs now, and the models you can test.
const { jobs } = await lbb.models.trials.options();
// Test Claude Haiku 5.5 at low effort on question answers.
const { trials: [trial] } = await 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.
const result = await lbb.models.trials.wait(trial.id, {
onUpdate: (t) => console.log(`${t.report.calls} calls compared`),
});
const { report } = result;
console.log(report.current.score, report.candidate.score, report.ci_low, report.ci_high);
if (report.qualifies) {
await lbb.models.switches.create({ trial: result.id });
}
// Back to LBB's model.
await lbb.models.switches.revert("ask");
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, skipped }: one trial per use. An open trial of the same use and candidate comes back as it is.
models.trials.wait(id, opts?) The trial once it compared what it can (until: "compared", the default) or ended (until: "ended"). onUpdate, pollIntervalMs, timeoutMs and signal control the wait.
models.trials.list({ job?, limit? }) The trials, newest first, with their reports.
models.trials.get(id) One trial with its report and its compared calls.
models.trials.call(id, callId) One compared call: both answers and the ground truth.
models.trials.stop(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? }) Chooses the model of a step for the environment or one workflow. The next calls run it. The client sends an idempotency key.
models.choices.delete({ use, workflow? }) Removes a choice: the step runs the environment’s model, else LBB’s. Returns { id, object, deleted: 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 (409 trial_not_qualified) or that compared with another model than the one the use runs now (409 trial_outdated): start a new trial. Catch them as LbbError and read error.code.

Reads take a consistency option:

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

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

const note = await lbb.graph("support").facts.create({
entity_properties: [{ ...invoice, properties: { priority: "high" } }],
});
const now = await lbb.sparqlRows(
{ query: "SELECT ?p WHERE { ?t <https://littlebigbrain.com/p/priority> ?p }" },
{ consistency: "strong", minIndexedSeq: note.commit_seq },
);
console.log(now.rows.length);

Publication may not cover the floor yet. Then an eventual read returns 429 read_your_writes_pending with a Retry-After. sparqlRows, sparqlText, query.sparql and query.sparqlRaw retry it within the retry budget. You can also call waitForPublished first.

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

consistency and minIndexedSeq work on sparqlRows, sparqlText, sparql, summary, query.sparql, query.sparqlRaw and query.structured. ontology.conformance and entities.detail take consistency only.

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

const 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" .
`;
const imported = await lbb.graph("catalog").facts.importRdf(turtle, {
format: "turtle",
baseIri: "https://example.org/",
idempotencyKey: "catalog-2026-09-25",
});
console.log(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.

Option Meaning
format ntriples (default), turtle, nquads or trig.
baseIri The base for relative IRIs.
blankNodeScope 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.
observedAt An RFC 3339 time to record on every statement.
build false holds back publication for this request.
idempotencyKey The key for this request. The client creates one if you omit it.
graphUri 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 blankNodeScope 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.importRdfMany([{ rdf }, …], opts) imports several documents in order. Only the last request triggers publication. Pass its finalSequence to waitForPublished. With an idempotencyKey, document n uses the key suffixed with :n.

query.update(text, opts?) runs SPARQL Update on the /update endpoint. 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.

await lbb.graph("catalog").query.update(
'INSERT DATA { <https://example.org/user-db> <https://example.org/tier> "gold" }',
{ idempotencyKey: "catalog-tier-1" },
);

A graph whose first write is RDF only takes RDF writes. On it, facts.create, commitDryRun, facts.import, 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(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.

const 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" } },
];
const sync = await lbb.graph("support").facts.import(tickets, { idempotencyKey: "tickets-4824" });
console.log(sync.committed_commit_seq);

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

async function* records() {
yield { type: "Ticket", key: "4825", name: "PDF export fails", properties: { priority: "low" } };
}
const accepted = await lbb.submitImport(records(), { idempotencyKey: "tickets-2026-09-25" });
const status = await lbb.waitForImportJob(accepted.job_id);
if (status.state !== "succeeded") throw new Error(status.failure?.message);
console.log(status.committed_commit_seq);
  • idempotencyKey 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 fails in the client before any request.
  • getImportJob(id) reads the status, and cancelImportJob(id) cancels the job.
  • waitForImportJob polls every second and takes pollIntervalMs, timeoutMs and signal.

The durable route needs the server capability durable_import_jobs_v1. The client checks GET /version first and throws 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 facts.import and 10,000 for a job. strict: true fails at the first malformed line, which is skipped by default. observedAt records an RFC 3339 time on every record.

The HTTP API documents the job states and failure fields.

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

const pairs = await lbb.sparql({
patterns: [{ subject: { var: "ticket" }, predicate: "REPORTED_AS", object: { var: "bug" } }],
select: ["ticket", "bug"],
limit: 50,
});
console.log(pairs.solutions.length);

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

const urgent = await lbb.entities.filterByAttributes({
patterns: [{ subject: { var: "ticket" }, predicate: "REPORTED_AS", object: { var: "bug" } }],
where: [{ var: "ticket", field: "priority", value: "high" }],
select: ["ticket", "bug"],
});
console.log(urgent.solutions.length);

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.

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.

const 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." ] .
`;
await lbb.schema.publish({ desired_mode: "warn", shapes: { source: shapes, format: "turtle" } });
const report = await lbb.ontology.conformance({ consistency: "strong", maxRetries: 60 });
console.log(report.conforms, report.result_count, report.validated_at_seq);
if (report.conforms) {
await 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 maxRetries from 6.

A change of mode publishes the same shapes as a new version. schema.publish(body, { dryRun: true }) previews a publication. Choose an enforcement mode explains when to switch.

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

const preview = await lbb.commitDryRun({
triplets: [{ source: login, relation: "REPORTED_AS", target: pdf }],
});
console.log(preview.schema_validation?.conforms);

Ontology drafts keep a proposed schema apart from the graph. ontology.drafts.create(body) takes 1 to 100 sample records and stores the proposed operations, an analysis per competency question, coverage, pitfalls and confidence at one commit. It writes no facts. Continue with ontology.drafts.validate(id), then promote(id) or reject(id, reason). promote sends 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.

const retracted = await lbb.graph("support").retract({
edges: [{ source: pdf, relation: "FIXED_IN", target: r311 }],
});
console.log(retracted.commit_seq);
Method What it does
graph(name).retract(body) Removes links, and optionally fields, as a new commit.
reload(lines, { confirm, dryRun? }) Makes the graph match the NDJSON lines in one commit. Records missing from the lines leave current state. confirm is the graph name. dryRun: true previews the change and writes nothing.
forkGraph({ 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.
createGraph() Creates the client’s graph with an empty schema. ontology.define also creates it.
listGraphs() Lists the graphs of the environment.
graph(name).delete({ confirm }) or deleteGraph({ 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.

const planned = await lbb.reload(tickets, { confirm: "support", dryRun: true });
console.log(planned.prior_commit_seq);

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

Method Returns
waitForPublished(commitSeq, opts?) Resolves when the latest published commit covers commitSeq. Throws after timeoutMs (default 30,000) or when publication is blocked. Polls every pollIntervalMs (default 250), or later when the server sends a longer retry delay.
publicationStatus() state (current, queued, compacting, building, publishing or blocked), head_seq, published_seq, lag_commits and retry.
readSnapshot() head_seq, snapshot.served_at_seq, query_lag_commits, base_current and conformance_lag_commits.
summary(opts?) Record, link and type counts, with the commit they describe.
schemaSummary() The types and fields found in the published data, with counts.
plannerStats({ cursor?, limit? }) 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.

const published = await lbb.readSnapshot();
console.log(published.snapshot.served_at_seq, published.query_lag_commits, published.base_current);

Each method returns parsed JSON. On a non-2xx response, it throws LbbError.

try {
await lbb.graph("support").facts.create({
triplets: [{ source: login, relation: "BLOCKS", target: auth }],
});
} catch (err) {
if (!(err instanceof LbbError)) throw err;
console.error(err.status, err.code, err.message);
}
Field Meaning
status The HTTP status.
code, type, message, param The error from the response body.
retryable, retryAfterSeconds Whether a retry can succeed, and when.
requestId, docUrl The request id to quote in support, and a link to the docs.
endpointHint 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 The raw response body.

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.sparql, query.sparqlRaw, query.structured, embeddings.search, ontology.search, ontology.resolve and ontology.drafts.validate.
  • embeddings.searchTuning.apply, which sets the same settings again.
  • Graph deletes and forks.
  • A retryable 429 on sparqlRows and sparqlText. A 5xx or a network failure is not retried, because the query could run twice.

Other POST and PUT calls are sent once. Examples are ontology.define, embeddings.declare and sparql. Pass { retry: true } to a namespace method to override this. retry is true, false or "rate_limited", which retries a retryable 429 only.

A retry follows a 429, a 5xx or a network 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 thrown at once.

retryBudgetMs bounds the total retry time, and maxRetries caps the count. Namespace methods take these options as their last argument: timeoutMs, maxRetries, retryBudgetMs, retry, signal and headers. Writes also take idempotencyKey.

rawRequest(method, path, opts) returns the response with metadata: requestId, version, headers, attempts, retryCount and elapsedMs. onRetry receives each retry with its status, error code and delay.

const response = await lbb.rawRequest("GET", "/v1/graph/summary");
console.log(response.status, response.requestId, response.attempts);

The package exports the types of every request and response.

Export What it is
Schemas["Name"] Any shape of the API contract, such as Schemas["SearchResponse"].
Entity, EntitySelector, CommitRequest, CommitResponse, GraphSummary, GraphMetadata, SchemaView, Snapshot Short names for common shapes.
SparqlResults, SparqlTerm, SparqlResultsJson Parsed SPARQL results, one typed term, and the raw results document.
LbbClientOptions, CallOptions, RequestOptions, ReadConsistencyOptions The option types of the client and its methods.
components, paths, operations The raw OpenAPI types of the contract.
import type { Schemas } from "@littlebigbrain/client";
type Hit = Schemas["SearchHit"];
const best: Hit | undefined = found.hits[0];
Area Methods
Schema ontology.define, ontology.evolve, ontology.view, ontology.conformance, ontology.search, ontology.resolve, schema.view, schema.publish
Drafts ontology.drafts.create, ontology.drafts.get, ontology.drafts.validate, ontology.drafts.promote, ontology.drafts.reject
Ontology starters ontology.starters.list, ontology.starters.get, ontology.starters.apply, ontology.starters.update; the starters export: starters.crm, starters.documents, starters.work
Starter workflows workflows.starters.list, workflows.starters.use, workflows.triggered.preview, workflows.named.list, workflows.named.all, workflows.named.get, workflows.named.put, workflows.named.run, workflows.named.pause, workflows.named.queue, workflows.named.runs, workflows.named.allRuns, workflows.named.getRun, workflows.named.runStats, workflows.named.runResults, workflows.named.allRunResults, workflows.named.documents, workflows.named.allDocuments, workflows.named.delete, workflows.derivations.check, workflows.derivations.record, workflows.derivations.derive; graph(name).workflows scopes each one to a graph; triggeredWorkflow, writeOwned, parseTriggerMessage, entityRef and entityIdFromIri for developer workflows
Files files.upload, files.list, files.get, files.counts, files.content, files.contentStream, files.head, files.pages, files.delete
Suggestions ontology.suggestions.list, ontology.suggestions.get, ontology.suggestions.create, ontology.suggestions.validate, ontology.suggestions.accept, ontology.suggestions.dismiss, ontology.suggestions.supersede, ontology.suggestions.comment
Write graph(name).facts.create, commit, commitDryRun, graph(name).retract, reload
Import graph(name).facts.importRdf, importRdfMany, graph(name).facts.import, submitImport, getImportJob, waitForImportJob, cancelImportJob
Search embeddings.declare, embeddings.get, embeddings.list, embeddings.preview, embeddings.search, embeddings.setModel, embeddings.refresh, embeddings.delete, embeddings.searchSettings, embeddings.setSearchSettings, managedModels, modelActivity
Search tuning embeddings.searchTuning.start, embeddings.searchTuning.list, embeddings.searchTuning.get, embeddings.searchTuning.apply
Model checks checks.summary, checks.list, checks.review, checks.export, checks.calls, checks.call, checks.checkCall
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, models.choices.list, models.choices.all, models.choices.put, models.choices.delete
Query sparqlRows, sparqlText, sparql, query.sparql, query.sparqlRaw, query.structured, query.update, query.ask, query.askStream, entities.filterByAttributes
Records entities.detail
Graphs createGraph, listGraphs, forkGraph, deleteGraph, graph(name).delete
Integrations integrations.connectors, integrations.providerApps, integrations.createConnectSession, integrations.getConnectSession, integrations.create, integrations.list, integrations.get, integrations.setCredentials, integrations.setSettings, integrations.sync, integrations.pause, integrations.resume, integrations.delete, integrations.erase, integrations.suggestions, integrations.accept, integrations.dismiss; googleDrive.authorizeUrl and googleDrive.exchangeCode (imported from the package)
Publication waitForPublished, publicationStatus, readSnapshot, summary, schemaSummary, metadata, status
Transport rawRequest, request, integrationsRequest, idempotencyKey, withScope, listAll

commit(body) is the client-level form of facts.create. It also returns the number as commitSeq. 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 and Protégé 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:

const exported = await lbb.rawRequest<string>("GET", "/sparql", {
query: { query: "CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }" },
headers: { accept: "application/n-triples" },
});
console.log(exported.data.split("\n").length);

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.