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.

source is the id you wrote the document under, and it is the address to act on: it round-trips into GET /documents/{id} and into your own links. slug is the Brain’s internal address for the same page — a content hash 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 { data } = await nicia.context("tenant:acme", {
prompt: userQuestion,
maxTokens: 2000,
});
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.

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 renders sources in a stable order keyed on each source’s id, and stops when the budget is spent. That determinism is the point: two identical calls against one snapshot return the same text, which is what makes an answer reproducible and a receipt worth holding.

That stable order is not a relevance ranking, and truncation inherits it. What gets left out is whatever fell past the budget in that order — not the least relevant material:

{
"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. 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.

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 must be at most 8192, which is the largest budget the deterministic context assembler renders to."
}
}

/context reads the whole Brain. There is no filter parameter: the prompt and the budget are the selection, and everything the Brain holds is a candidate.

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 projects every source-backed document in the Brain, orders it, 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, the answer will say so instead of quietly picking one:

{
"data": {
"text": "Acme's ARR is $145,000 (Salesforce). NetSuite reports $132,000 for the same period.",
"conflicts": [{ "id": "cfl_91ab", "subject": "crm-account-4471", "predicate": "arr" }]
}
}

That is usually the first thing Nicia tells a team that they could not already see. Correct the source record or leave the disagreement in place so every answer continues to carry the caveat.

{
"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 exactly four kinds, and each one is a different reason to distrust a short answer:

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

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

staged_pending is the one worth wiring into your UI today: it is the difference between “the Brain knows nothing” and “the Brain is waiting on a human”, which have opposite fixes. access_scoped is the same distinction for access, and no read on this surface produces one yet — a Brain key is scoped to a whole Brain, and nothing filters an answer by credential. It is enumerated here and on the wire so a client written today renders it the day a read does. warnings carries the same four kinds on /search and /query.

processing counts what is outstanding for the read you made. On /context and /query that is documents still being read for records — a page itself is in an accepted-state answer as soon as the Brain accepts it, typed or not, and whether or not your schema declares a kind that fits it. On /search it also counts documents whose text is not in the passage index yet, including ones you wrote with extract: false, because ranking cannot see them until it is. That is the warning to branch on after a write: an empty /search with a processing warning means “not yet”, and an empty /search 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/{id} 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 — selection is deterministic, which is why it is fast, cheap, and gives the same answer twice for the same snapshot.