Skip to content

Answer questions in plain words

View Markdown

Your users ask questions in plain words: “How many services does each team run?” A SPARQL query answers it. To write that query, a model must know your classes and properties. It must also know the rules of SPARQL on little big brain. It should look at the rows before it answers.

POST /v1/query/ask does these steps on the server. A model runs queries in a short loop, reads their rows, and answers in plain words. The response holds the answer, the records it cites, and the query whose rows hold the answer, with its rows. Your app shows the answer beside the rows, and can draw a chart of them: the answer says which kind of chart fits.

LBB runs the models with its own keys and pays for the calls. You choose no model and bring no key.

  1. The server describes the graph. It reads what it keeps about your graph: the classes and their counts, and the properties of the largest classes. Each property gets a short example value. The description also holds the OWL and RDFS statements and the classes you can search by meaning. The server keeps it for 10 minutes, so the next questions reuse it. A graph with many classes gets a short extra part for each question: the classes and properties whose names match the question’s words, with all their properties. Your graph’s profile comes with it.
  2. A decision model picks the route. TypeSafe’s Jev picks the kind of question. It gives a probability for each kind. At the same time, the server looks up the names in the question (“TU Dresden”, “Quelmann”) and finds the records they name. See Names in the question.
  3. A reasoning model looks at the data. Claude Sonnet 5.5 works in steps, as a person at a SPARQL console does. It runs a query and reads the row count and the first 30 rows. It looks up a name, reads the description of some classes, finds the commit of a date, or compares two points in time. It corrects a query that failed or returned too few rows. The server checks each query: only SELECT and ASK run, and a known prefix that the query uses without a PREFIX line gets one.
  4. The model answers. It answers in short plain words, names the records it read, and points to the query whose rows hold the answer. It takes at most 4 rounds of steps; then it must answer.

A route is one kind of question:

Route The question needs
lookup Records or facts that match conditions: values, links, paths.
aggregate Numbers: counts, sums, averages, rankings, groups.
search Records by meaning. The queries use search:similarTo on a class you can search by meaning.
history The graph at an earlier commit or date, or what changed.
schema The graph’s own model: classes, properties and how they connect.
unanswerable Data that the graph does not hold. The answer says what is missing.

Until 2026-10-05 the route was POST /v1/query/rewrite. Its default mode wrote one query without a look at your data, and run: true ran it. That mode is gone. The old path answers 404 and names the new one. To write your own queries in your own loop, use the tools.

The examples use a small service catalog. It has three services, the team that owns each, and two incidents. Load it as Turtle into a graph named services, as Load RDF data shows:

@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix c: <https://x.test/class/> .
@prefix e: <https://x.test/e/> .
@prefix p: <https://x.test/p/> .
c:service a rdfs:Class ; rdfs:comment "A running program that a team owns." .
c:authenticationincident rdfs:subClassOf c:incident .
e:risk a c:service ; rdfs:label "risk-service" ; p:description "scores card payments for fraud" ; p:team e:payments ; p:tier 1 .
e:ledger a c:service ; rdfs:label "ledger-service" ; p:description "books card payments into the ledger" ; p:team e:payments ; p:tier 2 .
e:auth a c:service ; rdfs:label "auth-service" ; p:description "signs users in with passwords" ; p:team e:identity ; p:tier 1 .
e:payments a c:team ; rdfs:label "payments team" .
e:identity a c:team ; rdfs:label "identity team" .
e:outage a c:incident ; rdfs:label "checkout outage" ; p:description "card payments failed at checkout" .
e:login a c:authenticationincident ; rdfs:label "login failures" ; p:description "users could not sign in" .

The search route needs an embedding. Declare one on the services, from their label and description:

Terminal window
curl -X PUT "$LBB_URL/v1/embeddings?graph=services" \
-H "Authorization: Bearer $LBB_API_KEY" -H 'Content-Type: application/json' \
--data '{"class": "https://x.test/class/service", "from": ["label", "description"]}'
Terminal window
curl -X POST "$LBB_URL/v1/query/ask?graph=services" \
-H "Authorization: Bearer $LBB_API_KEY" -H 'Content-Type: application/json' \
--data '{"question": "How many services does each team run?",
"context": "Teams own services."}'

This excerpt shows the answer, the route, the step and the model calls of the question above:

{
"answer": {
"text": "The payments team runs 2 services. The identity team runs 1 service.",
"citations": ["https://x.test/e/payments", "https://x.test/e/identity"],
"chart": { "kind": "bar", "x": "l", "y": "n" }
},
"route": {
"kind": "aggregate",
"by": "router",
"confidence": 0.99,
"probabilities": { "aggregate": 0.99, "unanswerable": 0.01 }
},
"query": {
"sparql": "PREFIX p: <https://x.test/p/>\nPREFIX class: <https://x.test/class/>\nPREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>\nSELECT ?t ?l (COUNT(?s) AS ?n) WHERE { ?s a class:service ; p:team ?t . OPTIONAL {?t rdfs:label ?l} } GROUP BY ?t ?l",
"entailment": "none"
},
"rationale": "I grouped the services by their p:team link and counted each group.",
"attempts": 1,
"steps": [
{ "n": 1, "tool": "sparql", "input": { "query": "PREFIX p: …", "entailment": "none", "as_of_commit_seq": null }, "ok": true, "rows": 2, "ms": 4 }
],
"models": [
{ "role": "router", "provider": "typesafe", "model": "jev-latest", "ok": true, "ms": 300, "tokens": 757, "cost_micro_usd": 0 },
{ "role": "rewriter", "provider": "anthropic", "model": "claude-sonnet-5-5", "ok": true, "ms": 2987, "tokens": 4941, "cost_micro_usd": 5064 },
{ "role": "rewriter", "provider": "anthropic", "model": "claude-sonnet-5-5", "ok": true, "ms": 2382, "tokens": 5274, "cost_micro_usd": 4074 }
],
"timings": { "ground_ms": 17, "route_ms": 301, "rewrite_ms": 5369, "run_ms": 4, "total_ms": 5717, "link_ms": 7, "anchor_ms": 0 }
}

The model ran one query, read its two rows, and answered. result holds those rows. Each turn of the model is one entry of models.

The rows of query hold what the answer states. Some answers narrow the rows: a cutoff, a condition, or the first few by a measure. The model then runs a query that does the narrowing, and names that query. The server checks the records the answer cites against the rows of its query. An answer that cites other records, or skips rows, goes back to the model once. An answer that names records of its rows by their labels but cites none goes back once too. So the rows that your app shows beside the answer match the text, and the records it names come with their IRIs.

Field Meaning
answer text, the answer in plain words, and citations, the IRIs of the records it names. Each citation was in what the model read. The server removes any other. chart says how to draw the rows of result, when they make a chart. See Draw the chart.
route kind, confidence, by and probabilities. by is router (Jev), rewriter (the model chose another route), or caller (your request fixed it).
query The query whose rows hold the answer: sparql, with its PREFIX lines, and entailment. For a comparison, the compared query.
result The rows of query: the response of POST /v1/query/sparql-text. It holds the rows in results, row_page, snapshot, the search report of a search query, and trace_id. Absent for a comparison.
history For a question about the past: the commit the model read, and for a comparison the rows that differ. See Questions about the past.
steps The steps in order: n, tool (sparql, find_entities, describe, commit_for_date or compare), input, ok, rows, error and ms.
rationale One sentence: which rows answer the question.
error Why the model stopped before it answered. answer is then absent, and result holds the best rows it read.
attempts The number of queries the model ran.
grounding The description the models read: its commit_seq, and its counts of classes, properties and embeddings. focus_classes and focus_properties count the extra part for this question. profile_version is the version of your graph’s profile the model read. names is the number of names the server looked up the question’s names in.
models One entry per model call, with its tokens and its cost in millionths of a dollar.
timings Milliseconds per step: ground_ms, route_ms, rewrite_ms (the model’s turns), run_ms (its steps) and total_ms. link_ms and anchor_ms run at the same time as route_ms.
linked The names in the question that the server found in your graph. See Names in the question.
anchors What the server read about each record of anchor.

The query that holds the answer records an eval trace with the question as its request. Label its rows with evals, and the good answers become test cases.

A question takes about 6 to 12 seconds and costs about 1 to 2 cents.

Your app can show each step while it happens: “Searching by meaning…”, the query, the rows. Send the header Accept: text/event-stream. The server then sends server-sent events instead of one JSON body. Each event has a name and JSON data:

Event Data When
grounding cached, age_ms, classes The graph description is ready.
route kind, confidence, by, probabilities The route is known. A second route comes before answer when the model chose another route.
step n, tool, input, ok, rows A step of the model ended. input is one line: the query without its PREFIX lines, the name, the words or the date.
answer text, citations, chart The answer, just before done.
done The whole response The last event. It is the same JSON as the response without the header.
error status, code, message The last event when the request failed. The same status and code as without the header.

Ignore event names that you do not know. Later versions add events. A bad request, the daily limit and an authentication error still answer with their HTTP status and JSON error, before any event. When your app closes the connection, the server stops the work and calls no more models.

EventSource cannot send a POST, so read the stream with fetch. This loop shows each step in the browser:

const response = await fetch(`${LBB_URL}/v1/query/ask?graph=services`, {
method: "POST",
headers: {
Authorization: `Bearer ${LBB_API_KEY}`,
"Content-Type": "application/json",
Accept: "text/event-stream",
},
body: JSON.stringify({ question: "Which services score card payments?" }),
signal: controller.signal, // controller.abort() stops the work on the server
});
if (!response.ok) throw new Error((await response.json()).error.message);
const show = {
grounding: () => "Reading the graph…",
route: (d) => (d.kind === "search" ? "Searching by meaning…" : `Route: ${d.kind}`),
step: (d) => (d.tool === "sparql" ? `${d.input} (${d.rows ?? 0} rows)` : `${d.tool}: ${d.input}`),
answer: (d) => d.text,
};
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
let buffer = "";
for (;;) {
const { value, done } = await reader.read();
if (done) break;
buffer += value;
let end;
while ((end = buffer.indexOf("\n\n")) >= 0) {
const block = buffer.slice(0, end);
buffer = buffer.slice(end + 2);
const name = block.match(/^event: (.*)$/m)?.[1];
const data = block.match(/^data: (.*)$/m)?.[1];
if (!name || data === undefined) continue; // a ": keep-alive" comment
const event = JSON.parse(data);
if (name === "done") render(event); // the whole response
else if (name === "error") showError(event.message);
else if (show[name]) setStatus(show[name](event)); // skip unknown events
}
}

The SDKs read the stream for you. In TypeScript, lbb.query.askStream(question, { signal }) returns the events as an async iterable. In Python, client.query.ask_stream(...) is a generator of events. Both raise the usual error on an error event. In MCP, lbb_query with mode: "question" sends each step as a progress notification when your client asks for progress.

answer.chart says how to draw the rows of result. kind is bar, line, scatter or table. x, y and series name columns of the rows. series, when present, splits the rows into groups.

The server checks each hint against the rows. Each named column exists. For bar and line, y holds numbers. For scatter, x and y hold numbers. A hint that does not fit is dropped, and the answer stays. There is no chart when the rows make no chart, for example one number or a yes or no. A bar whose x holds dates, years or year-months, in order, comes as a line: a time series reads better as a line.

This example draws the answer above with Chart.js:

const { answer, chart, rows } = await lbb.query.ask(
"How many services does each team run?",
);
showText(answer);
if (chart?.kind === "bar" || chart?.kind === "line") {
new Chart(canvas, {
type: chart.kind,
data: {
labels: rows.map((row) => row[chart.x!]),
datasets: [{ label: chart.y!, data: rows.map((row) => Number(row[chart.y!])) }],
},
});
} else if (chart?.kind === "table") {
showTable(rows);
}

In Python the hint is result.chart, a dict. In MCP it is answer.chart.

Field Meaning
question The question, 1 to 4,000 characters.
context Notes from your app for the model, at most 8,000 characters. See Notes for the model.
route Fixes the route. The decision model is not called.
mode answer (default) runs the loop and answers. route returns the route only. See Route only.
limit The rows each query returns: 1 to 1,000, default 100. The model reads at most 30 of them.
as_of_commit_seq Read the graph at this commit.
today Today’s date as YYYY-MM-DD, for questions such as “the last 14 days”. The default is the server’s UTC date.
include_grounding true returns the description the models read in grounding.text.
anchor The IRIs of records the user picked in your app, at most 10. See Records the user picked.
timeline Dates that stand for your commits, at most 200: [{date, as_of_commit_seq, label?}]. See Questions about the past.

Any other field answers 400. run and previous belonged to the removed one-shot mode: the loop always runs its queries, and corrects them itself. Put what your app knows in context.

?consistency=strong on the URL makes the queries read the newest writes. The default eventual reads the latest published commit. See Read consistency.

context tells the model what your data means: units, names to prefer, how records connect. Send the same text with every question. The model provider then reads it from its cache, which is faster.

{
"question": "Which tier 1 services does the payments team run?",
"context": "Teams own services. Tier 1 services are the critical ones."
}

Notes that hold for every question of a graph belong in its profile. The server keeps the profile with the graph and gives it to the model with each question, so your app does not send the notes every time. A profile holds notes and up to 20 worked examples: a question and the SPARQL query that answers it. Write what the shape of the data does not say: which property holds the current state, who “me” is, which records to leave out.

Terminal window
curl -X PUT "$LBB_URL/v1/query/rewrite/profile?graph=main" \
-H "Authorization: Bearer $LBB_API_KEY" \
-H 'Content-Type: application/json' \
-d '{
"notes": "Teams own services. Tier 1 services are the critical ones.",
"examples": [{
"question": "Which services does the payments team run?",
"sparql": "PREFIX p: <https://x.test/p/>\nSELECT ?s WHERE { ?s p:team <https://x.test/e/payments> }"
}],
"expected_version": 0
}'

GET /v1/query/rewrite/profile?graph=main reads it back with its version. The profile route keeps its old name. Each write adds 1 to the version. Send the version you read as expected_version: when another write came first, the server answers 409 conflict and stores nothing. ?dry_run=true checks a profile and stores nothing. The server parses each example query, and accepts only SELECT and ASK. Empty notes and no examples clear the profile. context still adds notes for one call.

The SDKs have query.rewriteProfile() and query.setRewriteProfile() (TypeScript), and query.rewrite_profile() and query.set_rewrite_profile() (Python). The MCP server has lbb_configure with get_rewrite_profile and set_rewrite_profile.

mode: "route" asks Jev alone, in about 0.3 seconds, and calls no other model. Use it to show “searching by meaning…” at once, or to pick a template of your own. When every route is below 0.5, ask the user to make the question clearer.

{ "question": "How many services are there?", "mode": "route" }

The response has route, and no answer or query. attempts is 0.

route sets the kind of question. Jev is not called. Use it when your app knows the kind, for example from a search box.

{ "question": "Show me the services", "route": "aggregate" }

Users name records in their own words: “TU Dresden” for “Technische Universität Dresden”, “Quelmann” for “Quellmann Fenstertechnik GmbH”, “Jurgen Weiss” for “Jürgen Weiß”. Before the model starts, the server looks up these names in your graph and gives the model the records they name. The queries then use each record’s IRI and do not search the names with text filters. That is faster, and it does not match the wrong record. The model can also look a name up itself, with its find_entities step.

The server reads the names of your records: rdfs:label, and the properties whose name is a name, a label or a title, such as name, legalName or title. It ignores case, accents, punctuation and legal forms such as “GmbH”. It also matches a name with one or two letters spelled differently, and an acronym inside a longer name (“TU Dresden”). No model is called for this step. It takes a few milliseconds.

Some words are not names, so the server does not link them:

  • A word that asks, such as “Summarize”, “Compare” or “Describe” at the start of a question.
  • A capital that only starts a sentence (“Call notes of last week?”).
  • A lone acronym (“ICP”), a short word in lower case (“crm”) or a short code (“Q3”), unless a record has exactly that name.

When a person and a record about the person have the same name (a CRM contact and the Notion page with the contact’s name), the person comes first. People and organizations come before other records, and both come before documents, notes, messages and mentions. The server reads this from the class name (“Contact”, “SourceDocument”) and its superclasses (foaf:Person, schema:CreativeWork).

The response lists the matches in linked:

"linked": [
{
"text": "Quelmann",
"iri": "https://demo.test/org/quellmann",
"label": "Quellmann Fenstertechnik GmbH",
"class": "https://demo.test/class/Organization",
"score": 0.82,
"by": "fuzzy"
}
]

text is the words of the question. by is exact, partial (the words are part of the name), fuzzy (spelled differently) or acronym. A name with close matches has up to three entries with the same text, the best first. Show “Did you mean Quellmann Fenstertechnik GmbH?” when by is fuzzy.

The server looks up names after it has read them once for your graph. After a commit, it reads them again in the background. Until it is done, a question links with the names it read before. A word that many records hold in their names, such as a city in the names of its universities, is not linked.

When the user has a record open in your app, send its IRI in anchor. The server reads the record’s types, its label and its links. The model uses the IRI and does not search for the record’s name.

{
"question": "Who works on the open issues of this project?",
"anchor": ["https://demo.test/project/offline-sync"]
}

anchors in the response tells what the server read about each IRI. An IRI that is not in your graph gets a note, and the question still gets an answer.

In the SDKs, pass anchor to query.ask: { anchor: [iri] } in TypeScript, anchor=[iri] in Python. lbb_query mode=question takes anchor too. The answer of query.ask holds linked and anchors.

A question about a date reads the graph as it was then. The model calls its commit_for_date step with the date, and runs its query at that commit. Each commit records when it was written. For “Which findings were open on 18 June?” the server finds the last commit written by the end of 18 June (UTC). history.as_of_commit_seq names that commit, and history.resolved_by says how the server found it: commit_time, timeline, or request (the model or your request named the commit).

Sometimes your commits stand for other dates. A demo loads its milestones in one minute, or an import loads records of last year today. Then pass timeline: the date each commit stands for.

{
"question": "Which findings were open on 18 June?",
"timeline": [
{ "date": "2026-06-10", "as_of_commit_seq": 4, "label": "Scope check" },
{ "date": "2026-06-18", "as_of_commit_seq": 5, "label": "LV addendum" }
]
}

The server reads the latest point on or before the date: here commit 5. history.label repeats its label.

The server never answers a question about a past date with the graph of another commit. When it cannot place the date, the step fails and the model reads why; it then says what the graph cannot tell:

  • The date is before the first commit of the graph, or before the first point of timeline.
  • An older server version wrote the commits, so they record no time, and the date falls between the creation of the graph and its last write. Pass a timeline.

A question that asks what changed (“Which deals moved stage since 1 June?”) uses the compare step. It runs one query at the commit of the date and at the later point (your as_of_commit_seq, else the latest commit), and pairs the rows by the record. The response’s history then has compare: true, the earlier point, history.key, history.added (the rows of records only the later point has), history.removed (the rows of records only the earlier point has), and history.changed (the records whose values differ, with their rows at both points). history.totals counts each list. Each list shows at most 500 entries. history.truncated is true when a point was not read whole, or a list holds more than 500 entries. For more, use POST /v1/query/compare. See Replay answers & history.

In the SDKs, pass timeline to query.ask. The answer of query.ask holds history. lbb_query mode=question takes timeline too.

Your app can run its own agent: your model writes queries, reads the rows and writes again, in a loop of a few steps. Four routes give that agent what only the server knows. None calls a model, and each is a read with the usual API key. The loop of POST /v1/query/ask uses two of them as its describe and compare steps.

Route Use it when
POST /v1/query/names The question names a record: a person, a company, a project. It returns the IRI of each name with its other candidates, the one to prefer first. Put the IRI in the query; do not match the name with CONTAINS. commit_seq is the newest commit the name index holds. Right after a write, send min_commit_seq with the commit_seq of your write: the call waits a few seconds for the index to hold it, then answers 429 read_your_writes_pending. Labels (rdfs:label, skos:prefLabel, schema:name) link in every class, however large. When index_complete is false, index_gaps lists the classes the name index did not read in full: another name property of such a class may not link.
POST /v1/query/describe The agent needs classes or properties it has not seen. Send the question, or classes and properties as IRIs. Each class lists how many of its sampled instances hold each property, so the agent sees that a filter on a rare property returns few rows. A small class (30 instances at most, such as stages or statuses) lists its instances with their values. text holds the same description for a prompt.
GET /v1/graph/commit-at?date=YYYY-MM-DD The question names a date. It returns the last commit written by the end of that day (or moment= an RFC 3339 time). Run the query with that as_of_commit_seq. Commits written before 6 October 2026 record no time: for an earlier date, note names the first commit that does, and you pass a timeline to query.ask instead.
POST /v1/query/compare The question asks what changed. Send one SELECT, before and after (each a commit, a date or a moment; after defaults to the latest), and key: the variable of the record. It returns added, removed and changed with their totals, and pages each list with next_cursor.

A loop in TypeScript, with your model in decide:

import { LbbClient } from "@littlebigbrain/client";
const lbb = new LbbClient({
baseUrl: process.env.LBB_URL!,
apiKey: process.env.LBB_API_KEY!,
graph: "crm",
});
const question = "Which of Mara Lindqvist's deals moved stage since 1 June?";
// 1. The records the question names, and the parts of the graph it needs.
const names = await lbb.query.names({ text: question });
const lindqvist = names.candidates.find((c) => c.text === "Mara Lindqvist")?.iri;
const schema = await lbb.query.describe({ question });
// 2. Your model writes the query from schema.text and the IRIs.
const sparql = await decide(question, schema.text, { lindqvist });
// 3. What changed: the query at the date's commit and now, paired by deal.
const diff = await lbb.query.compare({
query: sparql,
before: { date: "2026-06-01" },
key: ["deal"],
});
for (const change of diff.changed) {
console.log(change.key.deal.value, change.before, "->", change.after);
}
console.log(diff.totals, diff.truncated);

compare reads up to max_rows rows at each point (default 20,000, at most 50,000), page by page, and stops early when the read deadline is near. truncated: true and a note say when a point was not read whole; each point’s rows and total say how much. Pass next_cursor back as cursor, with the same request, for the next entries of each list; the cursor keeps both commits. Leave LIMIT out of the query.

In the MCP server these are lbb_query modes: names, describe, commit_at and compare.

Your agent’s answers can become test cases, and the router gets a score on your questions.

  1. Route the question with mode: "route". Give the route to your model as a hint.
  2. Run the query that holds the answer with request, the user’s words, and route, the route you got. The eval trace keeps both. Send request with that query only, so the other queries of your loop record no trace.
  3. Label the answer. valid: true makes the query the golden query and the route the golden’s route. route in the label names another route.
  4. To test your agent again, read the goldens (GET /v1/evals/goldens), ask each question with your agent, and send the answers to POST /v1/evals/run/answers:
{
"runner": "my-agent",
"answers": [
{
"golden": "4f0c2a9e1b7d3c5a8e6f1d2b",
"sparql": "SELECT ?s ?label WHERE { … }",
"entailment": "subclass",
"route": { "kind": "lookup", "confidence": 0.91, "by": "router" },
"reply": { "text": "Two services: risk and ledger." }
},
{ "golden": "9a1e7c3b5d2f4a6c8e0b1d3f", "error": "the model timed out" }
]
}

The server runs each query and checks it as it checks an answer of query.ask: the type of the query, its rows against the golden query, and the judged rows. Leave out sparql when your agent found the question unanswerable. route_check compares the router’s pick with the golden’s route. It scores the router and does not change the verdict. The runs of your agent are a series of their own: GET /v1/evals/results?runner=my-agent. In the SDKs this is evals.runAnswers (TypeScript) and evals.run_answers (Python); in the MCP server, lbb_evals action=run_answers.

  • Each server answers at most 500 questions per environment per UTC day. Past that, the route returns 429 rewrite_limit until the next UTC day. A request with a bad body does not count.
  • A question has at most 4,000 characters, and context at most 8,000.
  • A profile has at most 8,000 characters of notes and 20 examples. An example query has at most 4,000 characters, and all examples together at most 32,000.
  • A request stops at its 60-second read deadline. Other reads stop at 25 seconds.
  • The model takes at most 4 rounds of steps and 12 steps. A query runs at most 8 seconds, a comparison 12. The answer cites at most 20 records.
  • A query returns at most 1,000 rows; the model reads at most 30.
  • A query reads one commit, like every other query. A comparison reads two, one after the other.
  • timeline has at most 200 points. One lookup of the commit of a date reads at most 512 published generations in 8 seconds. The server keeps what it read, so the next lookup goes on from there.
HTTP code Meaning
400 invalid_ask_request A field is outside its limits, or it belongs to the removed one-shot mode (run, previous, mode: "rewrite"). param names it.
400 invalid_request The body is not valid JSON, or it has another unknown field. The message lists the fields.
404 not_found POST /v1/query/rewrite, removed on 2026-10-05. did_you_mean names /v1/query/ask.
429 rewrite_limit The environment asked its questions of the day on this server. Retry the next UTC day.
503 rewrite_model_unavailable The model did not answer, or the server has no model. The server already tried a failed model call once more when time allowed. Retry later.
400 invalid_rewrite_profile A profile is outside its limits, or an example query does not parse or is not a SELECT or an ASK. param names notes or examples.
409 conflict A profile write named an expected_version that is no longer stored. Read the profile again.

A step that fails is no error of the route. The model reads the error and tries again. A model call that fails on the way (a network error, a timeout, a busy or failed provider) is sent once more when enough time is left; models lists both calls. When the model stops before it answers, the status is still 200, answer is absent, and error says why.

To answer, the models read a description of your graph and the rows of their queries. These texts go to the model providers: TypeSafe for Jev, and Anthropic for Claude. The texts that leave little big brain are:

  • your question and context;
  • the records the server found for the names in the question, and the records of anchor: their IRIs, labels and types, and for anchor their links with up to three linked records each;
  • your graph’s profile (to Anthropic only);
  • class and property names, their labels and comments, and their counts;
  • short example values from up to 100 records of each large class, at most 48 characters each;
  • the OWL and RDFS statements of your schema;
  • the names and fields of your embeddings;
  • what the model’s steps return (to Anthropic only): the row count and up to 30 rows of each query (values cut at 200 characters), the records find_entities finds, the descriptions describe returns (with the instances and values of small classes), the commits of dates, and the first 30 entries of each list of a comparison.

The texts of an embedding already go to the embedding model in the same way.