Skip to content

Load your data

View Markdown

You load your data as JSON, from your own code. Rows from a database and sections from your documents become records and links in one graph.

Call What it does
define Declares your types and links
evolve Declares your fields
commit Writes your records and links as JSON
embeddings.declare Turns on search by meaning

Then you search in plain words and follow links with a query. You run no embedding model and no vector store.

Every example on this page follows the use case you pick here. So does the guide Add links with an LLM.

Your sources. A ticket database and your release notes.

The question. Which release fixed the login bug?

The graph.

Ticket → REPORTED_AS → Bug ← COVERS ← Section → PART_OF → Document

Each use case loads two sources. The database gives the records and their links. The documents give the text that explains them. The COVERS link joins the two.

Your data In the graph See
A database or a spreadsheet Rows become records. Foreign keys become links. Load a database
Documents: a wiki, Notion pages, PDFs Each section becomes a record with its text. Load documents
Facts that only the text states An LLM reads the text and writes links. Add links with an LLM
RDF files: Turtle, N-Triples Each statement is stored as written. Load RDF files

The examples read the files of your use case from the current folder. They write to a graph named kb. To try a second use case, change the graph name.

import { readFileSync } from "node:fs";
import { LbbClient } from "@littlebigbrain/client";
const lbb = new LbbClient({
baseUrl: process.env.LBB_URL!,
apiKey: process.env.LBB_API_KEY!,
graph: "kb",
});
const read = (file: string) => JSON.parse(readFileSync(file, "utf8"));

The quickstart shows where to find the endpoint and the API key.

A graph accepts only the types, links and fields you declare. So the schema comes first. schema.json names the types and the links between them. fields.json names the fields and their value types.

schema.json
{
"entity_types": ["Ticket", "Bug", "Document", "Section"],
"relation_types": [
{ "name": "REPORTED_AS", "source": "Ticket", "target": "Bug" },
{ "name": "PART_OF", "source": "Section", "target": "Document" },
{ "name": "COVERS", "source": "Section", "target": "Bug" }
]
}
fields.json
{
"ops": [
{ "op": "add_property", "name": "priority", "value_type": "keyword" },
{ "op": "add_property", "name": "opened_at", "value_type": "date_time" },
{ "op": "add_property", "name": "text", "value_type": "text" }
]
}

define creates the graph with your types and links. evolve then adds the fields. merge_default: false leaves out the built-in starter types.

await lbb.ontology.define({
format: "spec",
merge_default: false,
source: readFileSync("schema.json", "utf8"),
});
await lbb.ontology.evolve(read("fields.json"));

Both calls are safe to repeat. If you write before you declare a schema, the graph gets a small starter schema. Your own types are then refused.

One rule maps each table to the graph:

In your database In the graph
A table A type
A row A record. The row’s id is its key. A readable column is its name.
A foreign key A link
A join table One link per row
A column A field

A short script reads the rows and writes this JSON. Here are the tables and the JSON they become:

Table tickets:

id title bug_id priority opened_at
4821 login fails after 3.1 AUTH-112 high 2026-09-02T09:15:00Z
4822 invoice PDF is blank PDF-31 medium 2026-09-03T14:40:00Z

Table bugs:

id title
AUTH-112 session token expiry
PDF-31 PDF fonts missing
records.json
{
"triplets": [
{
"source": { "type": "Ticket", "key": "4821", "name": "login fails after 3.1" },
"relation": "REPORTED_AS",
"target": { "type": "Bug", "key": "AUTH-112", "name": "session token expiry" }
},
{
"source": { "type": "Ticket", "key": "4822", "name": "invoice PDF is blank" },
"relation": "REPORTED_AS",
"target": { "type": "Bug", "key": "PDF-31", "name": "PDF fonts missing" }
}
],
"entity_properties": [
{
"type": "Ticket",
"key": "4821",
"name": "login fails after 3.1",
"properties": { "priority": "high", "opened_at": "2026-09-02T09:15:00Z" }
},
{
"type": "Ticket",
"key": "4822",
"name": "invoice PDF is blank",
"properties": { "priority": "medium", "opened_at": "2026-09-03T14:40:00Z" }
}
]
}

triplets holds the links. entity_properties holds the field values. Send the file as one commit:

const records = await lbb.graph("kb").facts.create(read("records.json"), {
idempotencyKey: "kb-records-1",
});
console.log("commit", records.commit_seq);

The idempotency key makes a retry safe. Use a new key for new data. For a table with many rows, stream the rows through an import job.

You split each document at its headings. Each section becomes a Section record. Its text field holds the words, and a PART_OF link names its document.

Export PDFs and wiki pages to text first. Keep each section to a few paragraphs. Give each section a stable key, such as the document id plus the section number. A later load with the same key updates the section.

Release notes 3.2
## Sign-in fixes
Sessions no longer expire right after login. People who were sent back to the login screen after the 3.1 upgrade can sign in again. This fixes AUTH-112.
## Faster exports
CSV exports now run on several threads. An export of 50,000 rows finishes in under ten seconds.
sections.json
{
"triplets": [
{
"source": { "type": "Section", "key": "release-notes-3.2#1", "name": "Sign-in fixes" },
"relation": "PART_OF",
"target": { "type": "Document", "key": "release-notes-3.2", "name": "Release notes 3.2" }
},
{
"source": { "type": "Section", "key": "release-notes-3.2#2", "name": "Faster exports" },
"relation": "PART_OF",
"target": { "type": "Document", "key": "release-notes-3.2", "name": "Release notes 3.2" }
}
],
"entity_properties": [
{
"type": "Section",
"key": "release-notes-3.2#1",
"name": "Sign-in fixes",
"properties": {
"text": "Sessions no longer expire right after login. People who were sent back to the login screen after the 3.1 upgrade can sign in again. This fixes AUTH-112."
}
},
{
"type": "Section",
"key": "release-notes-3.2#2",
"name": "Faster exports",
"properties": {
"text": "CSV exports now run on several threads. An export of 50,000 rows finishes in under ten seconds."
}
}
]
}

Send the file with the same code as step 2 and a new idempotency key:

const sections = await lbb.graph("kb").facts.create(read("sections.json"), {
idempotencyKey: "kb-sections-1",
});

Declare an embedding on the Section type, from its name and its text. The platform embeds each section and keeps the vectors current. label is the record’s name.

An embedding reads published data. So the code first waits for the sections to publish. The first build then runs in the background, until the embedding reports serving.

await lbb.waitForPublished(sections.commit_seq);
await lbb.embeddings.declare({
class: "https://littlebigbrain.com/class/section",
from: ["label", "text"],
});
while (!(await lbb.embeddings.get("section")).serving) {
await new Promise((resolve) => setTimeout(resolve, 2_000));
}
const found = await lbb.embeddings.search(read("search.json"));
for (const hit of found.hits) console.log(hit.label);

search.json asks the question in your own words:

search.json
{ "text": "users cannot sign in after the upgrade", "top_k": 3 }

The first hit is Sign-in fixes.

A declaration spends model tokens. To preview the cost, see Search by meaning. It also shows how to filter hits by type or by link.

The records and the sections are in one graph now. No link joins them yet. The COVERS link says which record a section is about.

An LLM can read each section and write those links for you. The guide Add links with an LLM continues with the same graph and files.

Part Example Required
Types Customer, Order At least one
Links, with the types they connect PLACED, from Customer to Order At least one
Fields, with a value type total (number), placed_at (date and time) No, but a field must be declared before a write uses it
Parent types VipCustomer is a kind of Customer No
Allowed values status is open or closed No
A reverse name for a link PLACED_BY for PLACED No

Each field has a value type:

Value value_type
Free text text
A code or label, such as a status keyword
A whole number i64
A decimal number f64
True or false bool
A date and time date_time

The API calls the schema the ontology. The HTTP API lists the other value types.

Your data will grow. The schema grows with it through evolve:

To add op
A type add_entity_type
A link add_relation
A field add_property
A parent type add_super_types
More types to an existing link widen_relation

Additions are always safe. A change that removes or narrows something is blocked while stored data uses it. Add ?dry_run=true to preview a change and the records it affects. The HTTP API lists every operation.

You can also call define again with a larger document. It adds what is new. It refuses a document that leaves out a type or link the graph has. That error is 400 ontology_identity_breaking_change.

Skip this section if you have no schema file yet. Some teams already have their schema as an OWL file. OWL is a standard format for schemas.

define reads such a file in Turtle, JSON-LD, RDF/XML or CSV. Set format to turtle, json_ld, rdf_xml or csv, and pass the file as source.

@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix ex: <https://example.org/shop#> .
ex:Customer a owl:Class .
ex:Order a owl:Class .
ex:VipCustomer a owl:Class ; rdfs:subClassOf ex:Customer .
ex:placed a owl:ObjectProperty ; rdfs:domain ex:Customer ; rdfs:range ex:Order .
ex:total a owl:DatatypeProperty ; rdfs:domain ex:Order ; rdfs:range xsd:decimal .
In the OWL file Becomes
owl:Class A type: Customer, Order, VipCustomer
rdfs:subClassOf A parent type: a VipCustomer is a Customer
owl:ObjectProperty A link: PLACED
owl:DatatypeProperty A field: total, a decimal number

Link names become upper case. Writes match type and link names in any case. So placed and PLACED name the same link.

RDF files need no schema. An RDF import stores each statement as written, and your queries use your own predicates. You can add rules and reasoning later.

The first write to a graph decides its write model:

  • RDF first. The graph becomes RDF-native. It takes RDF import and SPARQL INSERT DATA from then on. A JSON write is refused with 400 rdf_native_graph.
  • JSON records first. The graph keeps JSON writes, and it also takes RDF import.

So to mix both formats in one graph, write the JSON records first. Load RDF data covers formats, large files and limits.

  • JSON records. Stream newline-delimited JSON through an import job. The same schema rules apply, so declare the schema first. A job that meets an unknown type fails with invalid_import.
  • RDF. Send the file in chunks. See Load a large dataset.
Error What to do
unknown entity type 'Customer' Declare the type, with define or with the evolve op add_entity_type.
unknown relation 'RETURNED' Declare the link with the evolve op add_relation.
unknown property field 'priority'; declare it first Declare the field with the evolve op add_property.
property 'total' is declared F64 but its value is not a number Send a value of the declared type.
graph_not_found from evolve Call define first. It creates the graph.
ontology spec requires at least one named relation type Add at least one link to the define document.
400 ontology_identity_breaking_change Keep the existing types and links in the document, or use evolve.
400 with no predicate named text when you declare an embedding The sections are not published yet. Wait for them, then declare again.
409 embedding_not_ready from a search The first build is still running. Wait until the embedding reports serving.
400 rdf_native_graph The graph was first written with RDF. Use RDF import, or load the JSON records into a new graph.
An import job fails with invalid_import Read the job’s error.message, fix the schema or the data, then submit the job again with a new idempotency key.