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.
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 }],});What text is
Section titled “What text is”One block per source, blank-line separated, in the same order as citations:
- a document that carries frontmatter contributes a name line — the
titlein its own frontmatter, or the Brain’s name for the page (Evidence <your document id>) when it has none — then every frontmatter key as akey: valueline, 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 nolabel) and then its values, onefield: valueper 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.
Budget, not page size
Section titled “Budget, not page size”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." }}No filter, on purpose
Section titled “No filter, on purpose”/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.
No subject scoping either
Section titled “No subject scoping either”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.
Disagreement is reported, not hidden
Section titled “Disagreement is reported, not hidden”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.
An empty answer explains itself
Section titled “An empty answer explains itself”{ "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.
Rendering it yourself
Section titled “Rendering it yourself”If you want the pieces rather than the prose, ask for items and render your own prompt block:
-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.
Cost and latency
Section titled “Cost and latency”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.
- Search and queries — when you want rows, not prose.
- Receipts — what that
receiptstring is worth.