Skip to content
Beta. This surface may change before GA; breaking changes are dated in the changelog.

Context

/context is the reason to use Nicia. You send the question your agent is about to answer; you get back text to put in the prompt, the citations behind it, and a receipt that proves what was shown.

Terminal window
curl -X POST "$API/v1/brains/tenant:acme/context" \
-H "authorization: Bearer $NICIA_KEY" \
-H "content-type: application/json" \
-d '{
"prompt": "What should I know before the renewal call?",
"maxTokens": 2000
}'
{
"data": {
"text": "Acme renewed at $120k for 24 months. Bob Smith (VP Platform) asked for SSO before Q3.\n\nCustomer reports a redirect loop after switching to Okta. Resolved same day.",
"citations": [
{
"source": "hubspot:deal:412",
"slug": "brain-evidence-0dd3e2449851d9ba41924c42bd7a61375a8b78050bfe2ca7192eb918d626236a",
"quote": "Acme renewed at $120k for 24 months. Bob Smith (VP Platform) asked for SSO before Q3.",
"recordedAt": "2026-03-14T18:02:11Z"
},
{
"source": "zendesk:ticket:8821",
"slug": "brain-evidence-3bb86259c42d0261e0f1cc2d0f7f5f6bb2ee2a9a1cfa8a5b1c5e8a5f0f2a7d31",
"quote": "Customer reports a redirect loop after switching to Okta. Resolved same day.",
"recordedAt": "2026-08-02T14:20:00Z"
}
],
"staged": 0,
"warnings": [],
"receipt": "rcp_019ffefe",
"truncated": false
}
}

Drop text into your system prompt. Render citations in your UI. Log receipt.

To read a branch, include "branch": "<branchId>" in the request body. A branch-scoped Brain key uses its branch when you omit it; a main-scoped key may name a branch explicitly. The answer is pinned to that branch’s head and its receipt names the branch. Branch documents and records are served there while main continues to serve its own accepted versions. A branch read includes an access_scoped warning to make that limited view explicit. Branches and Proposals describes how reviewed changes reach main.

source is the id you wrote the document under, and it is the address to act on: it round-trips into GET /documents/{documentId} and into your own links. slug is the Brain’s internal address for the same page — an opaque hash, derived from the Brain and your document id, for anything written through this API — and it is there to match a citation against a /search hit, never to be parsed or displayed. A document that never arrived through this API (a page authored in the console) carries slug alone.

const response = await fetch(`${API}/v1/brains/tenant:acme/context`, {
method: "POST",
headers: {
authorization: `Bearer ${NICIA_KEY}`,
"content-type": "application/json",
},
body: JSON.stringify({ prompt: userQuestion, maxTokens: 2000 }),
});
const { data } = await response.json();
const answer = await model.complete({
system: `Use only the context below.\n\n${data.text}`,
messages: [{ role: "user", content: userQuestion }],
});

One block per source, blank-line separated, in the same order as citations:

  • a document that carries frontmatter contributes a name line — the title in its own frontmatter, or the Brain’s name for the page (Evidence <your document id>) when it has none — then every frontmatter key as a key: value line, then a blank line, then its prose. A document with no frontmatter is its prose and nothing else;
  • a record contributes its label and kind (Acme (company), or its id when it has no label) and then its values, one field: value per line;
  • a page with values but no prose — a record written on a Brain’s free-form kind — contributes its label and those values.

Frontmatter is in the block on purpose: it is what the page says about itself, and a Brain never drops an authored field. It is worth knowing before you paste text into a prompt — a document carrying an internal_owner: line puts that line in front of the model.

A document’s prose is the whole document unless it was too large for the budget, in which case it is the passages your prompt matched with […] where text was left out — see Budget, not page size. Its name line and its frontmatter are unchanged either way.

Block n of text is citations[n].quote, character for character — text is those quotes joined by a blank line and nothing else. That is the property to lean on: whatever you show a user as a citation is exactly what the model was shown, so a quote can never be a paraphrase of the answer or a subset of it. Nothing internal to the Brain’s storage — its opaque ids, its file paths — appears in either. Read the blocks from citations rather than by splitting text: prose can contain blank lines of its own.

maxTokens is a budget, not a row count. Nicia orders sources by relevance to your prompt and fills until the budget is spent, so what a small budget buys you is the material your question actually named.

Two things decide that order, fused. A record ranks because a declared field holds something your prompt named — which is how a record with no prose at all is retrievable — and because the passage index ranked its text. Neither is a penalty for the other: a typed row your prompt names by customer is found with no prose in the Brain, and a document your prompt matches only in wording is found with no field in common. Sources nothing matched keep a stable order keyed on each source’s id, so the tail never varies.

A third stage may then re-judge the top of that list. Where a Brain is served by a deployment with a reranker, a cross-encoder reads your prompt and each leading candidate together and scores how well that candidate answers it — the one signal the two legs above cannot give you, because neither ever looks at the prompt and the record at the same time. It re-orders the head; everything past it keeps its fused order. The unit it judges is still the source, so a typed record with no prose is reranked on its declared values like anything else.

That means a model can judge relevance, and you can tell whether one did. The snapshot on every answer carries the retrieval models it was served under — the embedding model, and the reranker or none — so “which models produced this text” is recorded rather than assumed. A Brain on a deployment with no reranker is ordered by the two legs alone and says none, which is what the whole surface did before reranking existed.

What does not depend on any of that is the receipt. It records the exact sources and versions an answer rested on, so any answer stays reproducible from its receipt whatever changes afterwards — including a change of ranking model. That is the guarantee to build on: not that the ranker is a fixed computation, but that what you were shown is written down.

Truncation follows that order, so what gets left out is the material your prompt matched least — but “least” is not “not at all”:

{
"data": {
"truncated": true,
"omitted": 12,
"warnings": [{ "kind": "truncated", "count": 12 }]
}
}

truncated says the budget ran out; omitted counts the sources that never made it in — counted against everything the Brain holds, so a source the read never selected as a candidate counts exactly like one that did not fit. Read a true as “you have not seen everything”, never as “the rest did not matter”. Raise the budget, or go to /query for the specific rows you need.

A document too large to serve entire contributes the passages your prompt matched — the ones the passage index has already read, which is also what earned it its place in the order — rather than contributing nothing. […] marks every place the render left text out: before the first passage when it is not the start of the document, between two passages that were not adjacent, and at the end. truncated is true for that answer even when omitted is 0, because you have the parts of that document your question named and not the document.

A record is never excerpted. It is its declared values, and there is nothing to leave out of them, so a record renders whole or does not render — which is also why a Brain of typed records with no prose in it behaves identically whether or not anything has ever been indexed.

A source that fits neither route — too large for the budget, with no matched passage small enough to stand for it — is skipped rather than treated as a stop signal, so one oversized source cannot empty an answer it happens to lead. Skipped sources count toward omitted.

The default budget is 2048 tokens and the ceiling is 8192 — both the current deterministic assembler’s own numbers, and both will rise. Asking for more than the ceiling is a 400 naming it, never a quiet clamp: a request that was answered at a budget you did not choose, with truncated: true on it, is indistinguishable from one whose budget genuinely ran out.

{
"error": {
"kind": "validation",
"message": "maxTokens: maxTokens must be at most 8192, which is the largest budget the deterministic context assembler renders to."
}
}

/context answers from the whole Brain. There is no filter parameter: the prompt and the budget are the selection. The read selects its candidates in a bounded pass rather than loading the Brain — records whose field values hold the prompt’s terms, best-matched first; the documents the passage index ranks for the prompt; and the remaining records in a stable order, page by page, until the budget is spent — so a large Brain is served at the same cost as a small one and is never refused for its size.

Narrowing lives on /query, where a predicate is checked against a declared field and an unsupported one comes back as a 400 naming it. Filtering prose retrieval would mean filtering on document metadata that no read indexes — so rather than accept a filter and quietly ignore it, /context refuses one:

{
"error": {
"kind": "validation",
"message": "Unrecognized key: \"filter\""
}
}

A silently ignored filter is worse than none: you would get a confident, well-formed, whole-Brain answer to a question that asked for a slice, and nothing in the response would tell you.

about is refused for the same reason, and it is the one most worth explaining. It read as “you already know what this is about, so walk the relationships from there” — and there is no traversal behind it. Context assembly selects sources by the prompt, orders them, and renders until the budget is spent; there is no subject coordinate anywhere in that. Accepted and ignored, it would have answered 200 with a whole-Brain answer to a question that named one account.

{
"error": {
"kind": "validation",
"message": "Unrecognized key: \"about\""
}
}

To narrow to one record, read it with /query.

When two sources make incompatible claims over the same window, Nicia retains both facts. The read names the disagreement: warnings[] kind contested plus per-item status. There is no conflicts[] field on /context.

{
"data": {
"text": "acme\ncontract_value_usd: 120000",
"citations": [
{
"slug": "stmt-salesforce-arr",
"quote": "acme\ncontract_value_usd: 120000",
"recordedAt": "2026-08-14T09:20:00.000Z"
}
],
"items": [
{
"kind": "record",
"id": "stmt-salesforce-arr",
"fields": { "contract_value_usd": 120000 },
"status": "contested"
}
],
"staged": 0,
"warnings": [
{
"kind": "contested",
"subject": "acme",
"predicate": "contract_value_usd",
"count": 2
}
],
"receipt": "rcp_contested",
"truncated": false
}
}

GET /findings is the operator inbox — list, then decide with POST /v1/brains/{handle}/findings/{findingId}/decide and an Idempotency-Key. The integrator already has the signal from the read. See Statements.

A standing answer is a reviewed answer to a question people keep asking. It rests on claims, and the claims were read from documents. Every read that serves a standing answer serves its validity beside it:

  • current: nothing Nicia monitors for this answer has moved since it was accepted.
  • needs_review: something it rests on moved. reasons names each claim that changed or was removed, each claim whose quoted source text changed, and each new claim that arrived from a document the answer already uses.
  • unknown: Nicia cannot tell. For example, the answer declares no claim it rests on, part of what it rests on carries no evidence to check, or the check could not read everything it monitors.

Nicia does not interpret a document’s prose when it arrives: the text is served and indexed as it is. A standing answer rests on the sections it cites. The answer’s composer cites one statement per document section the answer depends on, each pinned server-side to that section, and the answer depends_on exactly those citations. A section is cited by its heading, as § <heading>, and pinned as everything under that heading, subsections included. An edit anywhere inside a cited section marks the answer needs_review; an edit to another section, or lines added above it, does not.

  • Only markdown headings count. A document with no headings, such as text converted from a PDF, can be cited only by line range, and a line range cannot see a sentence added just after the cited lines. Convert documents so that their headings are kept as markdown headings.
  • Only the composer’s citations are monitored. A standing answer your own agent writes through the public API, even one that cites a statement with verified evidence, reads unknown with reason support_unmonitored, and /context withholds its text. Answers that validity monitors come from the maintained-knowledge setup in the console.
  • Rebuilds are not automatic on a document edit. Validity flags the answer and /context withholds it at once, but a replacement is proposed only when someone recomposes it from the Governed answers panel.

Grounds are measured from affirmed: the last time someone decided the answer still holds. That is its creation, a person’s write to it, or a Review decision on it. An agent’s rewrite that the Brain’s policy lands without review changes the answer (accepted moves) but does not clear grounds that moved, so the answer stays needs_review until a person or a reviewer affirms it.

Beside validity, every standing answer carries verification: whether the version you are served passed its regression check before it was published. verified needs a passing check for that exact revision and no override; overridden keeps the check’s outcome and the reviewer’s reason; an answer an agent wrote directly, without Review, is not_evaluated, and never inherits an earlier version’s verdict. The two are independent: an answer can be current and not evaluated, or verified and since overtaken by its grounds. Only validity decides whether /context serves the text.

current is not a claim about the world. It means the monitored scope did not move through observedThrough, and monitored lists that scope: the claims the answer rests on and the documents they were read from. It is not a regression check either; validity says nothing about whether the answer was evaluated.

On /context, an answer that is not current is served without its text, as its question and validity only, with a warning of kind answer_not_current. An agent assembling context cannot mistake an answer whose grounds moved for the current one. The last accepted text stays available to direct reads, a view read, or an export, each with the same validity. Once the grounds move, rebuild reports what the Brain’s rebuild automation has done: not_attempted, running, failed, proposed with the waiting change, or completed_without_proposal. A failed rebuild never makes the old answer look current.

{
"data": {
"text": "Refund answer (standing_answer)\nquestion: How long do customers have to ask for a refund?\nvalidity: needs_review — NOT a current answer; its text is withheld (input_changed refund-window; monitored 1 claim: refund-window; sources: refund-policy through log position 14)\nverification: verified — this version passed its regression check",
"citations": [
{
"slug": "refund-answer",
"quote": "Refund answer (standing_answer)\nquestion: How long do customers have to ask for a refund?\nvalidity: needs_review — NOT a current answer; its text is withheld (input_changed refund-window; monitored 1 claim: refund-window; sources: refund-policy through log position 14)\nverification: verified — this version passed its regression check",
"recordedAt": "2026-09-28T09:00:00.000Z"
}
],
"items": [
{
"kind": "record",
"id": "refund-answer",
"fields": {
"question": "How long do customers have to ask for a refund?"
},
"validity": {
"state": "needs_review",
"reasons": [
{
"kind": "input_changed",
"recordId": "refund-window",
"changed": { "position": 14, "at": "2026-09-28T10:30:00.000Z" }
}
],
"accepted": { "position": 9, "at": "2026-09-28T09:00:00.000Z" },
"affirmed": { "position": 9, "at": "2026-09-28T09:00:00.000Z" },
"monitored": {
"claims": ["refund-window"],
"sources": ["refund-policy"]
},
"observedThrough": { "position": 14, "at": "2026-09-28T10:30:00.000Z" },
"rebuild": {
"state": "failed",
"at": "2026-09-28T10:30:05.000Z",
"reason": "Model provider unavailable"
}
},
"verification": {
"state": "verified",
"changeId": "chg_7Qm2",
"evaluation": { "state": "passed", "score": 91 }
}
}
],
"staged": 0,
"warnings": [
{
"kind": "answer_not_current",
"count": 1,
"detail": "1 standing answer's grounds moved or cannot be checked since it was accepted, so its text is withheld; each item's `validity` says why."
}
],
"receipt": "rcp_answer_not_current",
"truncated": false
}
}

GET /records/{id} and POST /query carry the same validity on a standing answer. /search returns a standing answer as a typed hit with the same kind, validity and verification, and shows one that is not current by its question alone.

A citation pins the version of the document it quoted, which keeps the quote true after the document is revised. It also means a record can keep showing a sentence the document no longer says, or cites a document that was withdrawn. Every read that serves a record checks its citations against the version the Brain serves now, and says so when one no longer holds.

A record carries support only when a cited document moved. It is absent when every cited document still serves the cited words, and when the record cites nothing. state is source_withdrawn when a cited document is no longer served, and otherwise support_at_risk, when the cited words are no longer in it. reasons names each document.

{
"data": {
"record": {
"id": "embedded-payments-consolidation",
"kind": "thesis",
"fields": { "thesis": "Falcon will consolidate the segment" },
"links": {},
"token": "brain-record-token-v1:9d2f4b6a8c0e1f3a5b7c9d1e3f5a7b9c1d3e5f7a9b1c3d5e7f9a1b3c5d7e9f1a",
"updatedAt": "2026-09-30T09:20:11Z",
"support": {
"state": "support_at_risk",
"reasons": [{ "kind": "support_at_risk", "document": "falcon-memo" }]
}
},
"staged": 0,
"warnings": [],
"receipt": "rcp_01a0c3f2"
}
}

What /context does about it depends on the kind. A statement is exactly its cited words, so one whose document moved is served without its text, as its support only, as a standing answer that is not current is. Any other record is served whole and marked: its values are as last accepted, and the citation behind them no longer holds. Either way the read warns with evidence_moved, and its receipt records what was served, marker included. GET /records/{id}, POST /query, /search, a view read and the MCP brain_context and brain_query tools carry the same support; a direct read keeps the last accepted values beside it, and a search hit shows a withheld statement with an empty excerpt.

The check is bounded: one lookup of what the cited documents serve now, and document text is read only for a document whose served version is not the one the citation pinned. A revision that leaves the cited words in place, an edit elsewhere on the page, is not a change to the support. A citation that named no words pinned nothing finer than the document, so only its withdrawal is detected. A branch read is not checked.

GET /receipts/{receiptId}/now reports the same thing after the fact in evidence[]: each record the receipt served whose cited documents have since moved or been withdrawn, with the support it was served with (servedSupport, absent when it was served intact) and its support now, even though the record’s own version did not change.

Your agent does not need Nicia’s composer to keep a monitored answer. Over the API or MCP it reads a document, cites the part the answer rests on, and rests the answer on that citation:

  1. List the documents with their outline (MCP brain_documents) and read the section. Every heading is listed as the exact anchor to cite.
  2. Write a statement whose evidence names the document and the anchor (and, if you like, the quote). See Cite your sources. The Brain checks the anchor against the version it serves and computes the pins itself; you never send one.
  3. Write the standing_answer with its question, its prose as body, and depends_on linking the statements it rests on.

The answer then reads current, lists the statements and documents under monitored, and reads needs_review when a cited section changes. An edit to another section of the same document leaves it current. A statement cited by its quote alone, with no anchor, is stored and shown but not monitored, so an answer resting only on such statements reads unknown.

{
"data": {
"text": "",
"citations": [],
"staged": 3,
"warnings": [
{
"kind": "staged_pending",
"count": 3,
"detail": "3 changes are waiting for review, so the Brain may be waiting on a human rather than knowing nothing."
}
],
"receipt": "rcp_31af09",
"truncated": false
}
}

staged is the number of changes awaiting review — proposals, not the records inside them, so a batch of twenty records staged as one proposal counts 1. It is the count the review queue itself is measured in, which is where a non-zero value sends you.

A non-zero staged means the answer may be waiting on a human rather than missing, and that is the distinction to surface to your users: they are very different problems. It is usually 0 on an open Brain, whose writes are admitted as they arrive — but not always, because anything that proposes rather than writes (Nicia’s own Brain agent stages every change it suggests) puts work in the queue whatever the mode.

staged can also be null, which means the count could not be taken on this read. The answer and its receipt are unaffected; what you cannot conclude from a null is that nothing is waiting. It is nullable for exactly that reason — reporting a failed count as 0 would say “the Brain knows nothing relevant” on a Brain that is holding your material for a reviewer.

Why an answer is less than the whole truth

Section titled “Why an answer is less than the whole truth”

warnings is present on every read and empty when the answer is complete. There are seven kinds. Four are operational; contested means sources disagree, answer_not_current means a standing answer was withheld, and evidence_moved means a record cites a document that moved or was withdrawn:

Kind Means
processing Evidence is accepted but not indexed yet, so a just-written document isn’t in this answer
access_scoped Your credential’s scope excluded matching content — published, not yet emitted by any read
truncated The token budget cut the answer short
staged_pending Matching content exists but is awaiting review
contested Two sources disagree about a subject and predicate in this answer
answer_not_current A standing answer matched, but its grounds moved or cannot be checked, so its text is withheld
evidence_moved A record matched, but a document it cites moved or was withdrawn; a statement’s text is withheld, any other record is marked with support

count and detail are optional; detail is one sentence written to be safe to show an end user.

contested is the sources-disagree signal: the answer is not settled. Wire it when an agent is about to act on a number two sources still fight over. staged_pending is the other one worth wiring today: it is the difference between “the Brain knows nothing” and “the Brain is waiting on a human”. access_scoped is enumerated and emitted by nothing — a Brain key is scoped to a whole Brain, and nothing filters an answer by credential. It stays published so a client written today renders it the day a read does. warnings carries the same kinds on /search and /query; only /context withholds an answer, so only /context emits answer_not_current and evidence_moved.

processing counts what is outstanding for the read you made, and the same word means two different things depending on the read.

Nothing is ever withheld from /context or /query for it. A page is in an accepted-state answer as soon as the Brain accepts it — typed or not, indexed or not, and whether or not your schema declares a kind that fits it. On /search, by contrast, a document whose text is not in the passage index yet is genuinely absent from the results, because ranking passages is all that endpoint does.

What processing tells you on /context is that part of what you were shown was ordered without its own text being read. Relevance ordering uses the passage index, so a document still being chunked can only rank on its declared fields; it is in the answer, but under a tight budget it may not have made the cut. That is why the sentence in detail says “may not include” rather than “is not in this answer”. A record with no prose is never counted: it has no passages, its declared fields are the whole of what it says, and it is fully rankable the moment it is written.

It is the warning to branch on after a write: an empty /search with a processing warning means “not yet”, and an empty one with no warnings means “not here”. Branch on the warning, not on its count: the index check looks at your most recent writes, which is where an unindexed document is, so a very large backlog reports what it looked at rather than the total. The same honesty rule staged: null follows applies here — a count that could not be taken emits no warning rather than a processing: 0 claiming nothing is outstanding, so warnings is what the read knows, never a promise that it looked.

If you want the pieces rather than the prose, ask for items and render your own prompt block:

Terminal window
-d '{ "prompt": "…", "format": "items" }'
{
"data": {
"text": "",
"citations": [
{
"source": "hubspot:deal:412",
"slug": "brain-evidence-0dd3e2449851d9ba41924c42bd7a61375a8b78050bfe2ca7192eb918d626236a",
"quote": "Acme renewed at $120k for 24 months.",
"recordedAt": "2026-03-14T18:02:11Z"
}
],
"items": [
{
"kind": "document",
"id": "hubspot:deal:412",
"text": "Acme renewed at $120k for 24 months."
},
{
"kind": "record",
"id": "acme",
"fields": { "name": "Acme", "arr": 120000 }
}
],
"staged": 0,
"warnings": [],
"receipt": "rcp_019ffefe",
"truncated": false
}
}

text is empty because you asked for the pieces instead. citations and the receipt come back either way, so a custom renderer does not cost you provenance.

Switch on kind. A document is a page nothing has typed and carries text, rendered the same way /context renders it. A record is a typed row and carries fields — the same values /query returns, as numbers and booleans rather than as YAML you would have to pull back out of a string — addressed by the id PATCH /records/{recordId} takes, so an item round-trips into a write.

The split is your schema’s, not a guess at one. A piece is a document when it is filed under the kind your Brain uses for things nobody has typed — your own free-form kind, the one you declared without fields, or document when you declared none — and a record otherwise, as long as it carries a value beyond its prose. So a kind you declare called document (or note) and give fields to is your kind: its records arrive as record, with your fields.

One /context call is one round trip and typically 200–500 ms. It does not call a language model to write anything: nothing is summarised, paraphrased or generated, so every word you get back is a word your Brain already held. On a deployment with a reranker, the ranking stage does score the leading candidates with a small cross-encoder inside that same round trip; on one without, no model runs at all beyond embedding your prompt.