TypeScript
@littlebigbrain/client is a thin, dependency-free wrapper over the platform
fetch. Request and response types are generated from little big brain’s API
contract, so every method is fully typed.
It runs anywhere there is a global fetch: Node 18+, browsers, and edge workers.
You can also pass your own fetch through the fetch option.
Source and issues: github.com/littlebigbrains/lbb-typescript (Apache-2.0).
Install
Section titled “Install”npm install @littlebigbrain/clientCreate a client
Section titled “Create a client”import { LbbClient, LbbError } from "@littlebigbrain/client";
const lbb = new LbbClient({ baseUrl: "https://0abc1def--production.db.eu.littlebigbrain.com", apiKey: process.env.LBB_API_KEY, // lbb_sk_live_… / lbb_sk_test_…});
const graph = lbb.graph("main");Load RDF
Section titled “Load RDF”facts.importRdf posts a document in N-Triples, Turtle, N-Quads, or TriG. The
graph is created on first write if it does not exist, using a fixed generic
schema, so no ontology design is required to start.
const imported = await graph.facts.importRdf(turtleDocument, { format: "turtle", baseIri: "https://example.org/data/", graphUri: "https://example.org/graphs/current", blankNodeScope: "catalog-2026-07",});console.log(imported.triples_read, imported.committed_commit_seq);Keep each request under the 64 MiB body cap. N-Triples and N-Quads are line-oriented, so a large file can be split on line boundaries; Turtle and TriG are document grammars, so each request must be a complete parseable document.
Omit blankNodeScope to preserve blank-node labels across chunks and retries, or
reuse one scope for every chunk of a source document to isolate it from documents
that reuse the same labels.
Write statements
Section titled “Write statements”Scope once with client.graph("name"). Its facts, entities, schema,
ontology, and query namespaces carry the same graph and branch. Pass an
idempotencyKey so retries are safe:
await graph.facts.create( { triplets: [ { source: { type: "SERVICE", name: "auth-service" }, relation: "WRITES_TO", target: { type: "DATABASE", name: "user-db" }, confidence: 0.93, evidence: "auth-service writes identity records to user-db", }, ], }, { idempotencyKey: "import-2026-06-13" },);Durable bulk import
Section titled “Durable bulk import”For a large NDJSON dataset of entity and edge records, submitImport streams an
iterable or async iterable without assembling one large string. The explicit key
binds retries to the uploaded content:
async function* records() { for await (const row of sourceRows()) { yield { type: "DOCUMENT", name: row.title, key: row.id, properties: row }; }}
const accepted = await lbb.submitImport(records(), { idempotencyKey: "sharepoint:run-2026-07-29",});const status = await lbb.waitForImportJob(accepted.job_id);if (status.state !== "succeeded") throw new Error(status.failure?.message);console.log(status.committed_commit_seq, status.publication_job);The SDK requires durable_import_jobs_v1 from GET /version and never silently
falls back to the synchronous import endpoint. getImportJob and
cancelImportJob support reconnect and cancellation. Every acknowledged commit
is strongly queryable; base reconciliation remains asynchronous and optional.
Empty iterables fail locally before a POST.
Automatic publication
Section titled “Automatic publication”const published = await lbb.readSnapshot();console.log({ servedAt: published.snapshot.served_at_seq, commitLag: published.query_lag_commits, generationLag: published.generation_lag,});Writes make one exact RDF delta visible in the branch head and advance one
server-managed reconciliation fence. Strong SPARQL is immediately queryable
from the immutable base plus that bounded suffix; clients do not coordinate
indexing. waitForPublished(commitSeq) is optional when a workflow wants the
base itself to cover a commit, publicationStatus() exposes maintenance
progress, and readSnapshot() diagnoses the head-owned RDF lineage.
For several RDF documents, graph.facts.importRdfMany(...) sends intermediate
imports with reconciliation deferred and triggers it once after the final
write. Its finalSequence may be passed to graph.waitForPublished(...) when
base compaction, rather than strong queryability, is the desired gate.
SPARQL
Section titled “SPARQL”sparqlRows(...) runs a SPARQL 1.1 text query (SELECT or ASK) and returns parsed
results, so you avoid a manual JSON.parse of a results string and the zipping
of head.vars with binding values:
const { vars, rows } = await lbb.sparqlRows({ query: `SELECT ?service ?db WHERE { ?service <https://littlebigbrain.com/r/writes_to> ?db } LIMIT 10`, entailment: "rdfs", // optional: apply the RDFS rules at query time});for (const row of rows) console.log(row.service, "->", row.db);
const exists = (await lbb.sparqlRows({ query: "ASK { ?s ?p ?o }" })).boolean;sparqlRows returns { vars, boolean, bindings, rows }. rows flattens the
bindings to { variable: lexicalValue }, bindings keeps the raw typed term
objects, and boolean is the ASK answer (null for SELECT). sparqlText(...)
returns the unparsed envelope, and the standalone parseSparqlResults(response)
helper parses it.
Structured query
Section titled “Structured query”sparql(body) is the JSON form of a SELECT/ASK: a basic graph pattern with typed
filters, GROUP BY (entity keys, scalar property keys, or calendar date
buckets), aggregates, HAVING, and ORDER BY. Use it when building a query from
user input is safer than assembling query text.
await lbb.sparql({ patterns: [{ subject: { var: "service" }, predicate: "WRITES_TO", object: { var: "db" } }], select: ["service", "db"], limit: 50,});entities.filterByAttributes(...) builds that structured body for you when you
already have relation patterns and need typed attribute predicates:
await lbb.entities.filterByAttributes({ patterns: [{ subject: { var: "service" }, predicate: "WRITES_TO", object: { var: "db" } }], where: [ { field: "slo", op: "ge", value: 0.99 }, { var: "db", field: "tier", value: "prod" }, ], select: ["service"],});Ontology, shapes & validation
Section titled “Ontology, shapes & validation”const ontology = await graph.ontology.view({ counts: true });const report = await graph.ontology.conformance(); // published SHACL reportconsole.log(report.conforms, report.result_count, report.validated_at_seq);
const bundle = await graph.schema.view(); // active shapes + modeawait graph.schema.publish({ desired_mode: "warn", shapes: { source: shapesTurtle, format: "turtle" },});There is no request-time runShacl() or ShaclQueryRequest. Publishing changes
the active schema; conformance reads the immutable report produced for that
schema and never accepts inline shapes.
commitDryRun(...) runs the same validation a real commit would and reports the
would-be effect without writing anything, which is the cheapest way to catch a
bad statement before it is committed.
Branches
Section titled “Branches”A branch is a copy-on-write snapshot. Fork one, load into it, validate it, and merge it back as a single commit:
// Scope a client to the new branch, then fork it from `main`.const review = lbb.withScope({ branch: "review" });await review.createBranch({ from_branch: "main" });
// …load and validate on `review`…
// Merge back: the client scoped to the fork parent replays the child onto it.await lbb.mergeBranch({ from_branch: "review" });await review.deleteBranch({ confirm: "review" });mergeBranch replays the child’s post-fork commits onto the scoped branch, its
fork parent, as one commit. Event ids are preserved, SHACL validation runs first,
and conflicts come back as supersedure_race. Graph deletion is whole-graph:
lbb.graph("main").delete({ confirm: "main" }).
Time travel & lineage
Section titled “Time travel & lineage”await lbb.currentState({ entity: { entity_type: "DATABASE", name: "user-db" } });await lbb.history({ entity: { entity_type: "SERVICE", name: "auth-service" } });await lbb.why({ /* which observations support this statement */ });To pin a SPARQL query to a past commit, use the native /sparql endpoint’s
?as_of_commit_seq= and ?as_of_valid_time= parameters. See the
HTTP API.
Read consistency & read-your-writes
Section titled “Read consistency & read-your-writes”Reads default to eventual: one immutable F3 base supplies the explicit
watermark in snapshot.served_at_seq. Pass consistency: "strong" to apply the
head’s exact bounded delta suffix, or set a client-level default with
new LbbClient({ …, defaultConsistency: "strong" }). No request reads mutable
truth or builds an index.
To read back exactly what you wrote, use the minIndexedSeq floor. Take the
committed sequence a write returned (surfaced as commitSeq) and read strongly
with minIndexedSeq set to it. The accepted commit already carries its F3
delta, so no publication polling loop is needed:
const { commitSeq } = await lbb.commit({ triplets });const rows = await lbb.sparqlRows( { query: "SELECT ?s ?p ?o WHERE { ?s ?p ?o }" }, { consistency: "strong", minIndexedSeq: commitSeq },);minIndexedSeq and consistency are accepted on query.sparql,
query.structured, sparqlRows, sparqlText, and summary.
Response metadata & errors
Section titled “Response metadata & errors”Regular methods return parsed JSON. Use rawRequest(...) when you need
requestId, version, or headers. Every method throws LbbError on a non-2xx
response:
try { await lbb.graph("main").facts.create({ triplets: [/* … */] });} catch (err) { if (err instanceof LbbError) { console.error(err.status, err.code, err.message, err.param, err.requestId, err.docUrl); }}Requests have a 120-second per-attempt timeout. Safe reads and
idempotency-keyed writes retry 429, 5xx, and network failures with
full-jitter exponential backoff, bounded by a retry budget (retryBudgetMs,
default 60s) rather than a fixed attempt count. maxRetries (default 6) is a
secondary cap. Retries honor Retry-After, and an error the server marks
non-retryable is raised immediately. Namespaced methods accept the same final
request options: timeoutMs, maxRetries, retryBudgetMs, signal,
headers, and idempotencyKey for mutations.
Configure onRequest, onResponse, and onRetry (an absorbed-retry callback)
for body-free lifecycle events. rawRequest(...) reports attempts,
retryCount, and elapsedMs alongside the request id and API version.
Method map
Section titled “Method map”| Area | Methods |
|---|---|
| Load RDF | graph("main").facts.importRdf |
| Write | graph("main").facts.create, commit, commitDryRun, graph("main").retract |
| Bulk import | submitImport, getImportJob, waitForImportJob, cancelImportJob, graph("main").facts.import |
| Graph lifecycle | createGraph, forkGraph (durable create-only copy), reload (declarative full-state replace, dryRun previews the delta), deleteGraph, deleteBranch |
| Branches | withScope({ branch }), createBranch, mergeBranch, deleteBranch |
| Query | sparqlRows, sparqlText, sparql (structured), query.structured, query.sparql, query.analytics, entities.filterByAttributes |
| Ontology | ontology.view, ontology.conformance, ontology.define, ontology.evolve |
| Schema & shapes | schema.view, schema.publish |
| Temporal / lineage | currentState, history, why |
| Inspection | entities.sample, entities.get, entities.detail, readSnapshot, schemaSummary, status, metadata, summary |
Common shapes are exported directly as Entity, GraphSummary, and
CommitRequest. Every generated shape remains available as
Schemas["TypeName"], and the raw components, paths, and operations are
exported too.
Native SPARQL Protocol
Section titled “Native SPARQL Protocol”A stack also serves the native SPARQL 1.1 Protocol at /sparql
(GET ?query=, POST form or application/sparql-query body,
Accept-negotiated JSON, XML, CSV, and TSV for SELECT/ASK and Turtle or
N-Triples for CONSTRUCT/DESCRIBE) for standard clients like YASGUI and
Protégé. sparqlRows is the in-process SELECT/ASK equivalent that returns parsed
JSON. To export a snapshot as an RDF document, call
GET /v1/graph/export/rdf over HTTP.