Search and queries
/context is the right call for “what should my agent know.”
These two are for when you want the raw material instead.
Both endpoints can read a branch. Add branch=<branchId> to /search, or
"branch": "<branchId>" to the /query body. A branch-scoped key selects its
branch implicitly. The record and declared-field indexes are scoped to the
selected branch, so a field predicate on /query cannot return a record that
exists only on main. Both answers include access_scoped when a branch limits
the read, and their receipts pin the selected branch.
Search — passages
Section titled “Search — passages”curl "$API/v1/brains/tenant:acme/search?q=sso+redirect+loop&limit=5" \ -H "authorization: Bearer $NICIA_KEY"{ "data": { "results": [ { "source": "zendesk:ticket:8821", "slug": "brain-evidence-3bb86259c42d0261e0f1cc2d0f7f5f6bb2ee2a9a1cfa8a5b1c5e8a5f0f2a7d31", "title": "SSO login loop after IdP change", "excerpt": "Customer reports a redirect loop after switching to Okta…", "rank": 1, "version": 3 } ], "mode": "fulltext", "staged": 0, "warnings": [], "receipt": "rcp_019fff21" }}Search ranks the passages of everything the Brain holds, best match first. Use
it to build your own retrieval flow, to show a “sources” panel, or to debug why
/context answered the way it did. Document passages come first, then the
typed records that match, each in its own ranked order.
A typed record’s hit carries its kind; a document passage has none. A
standing answer
is a typed hit that also carries validity and verification. One whose
grounds moved, or cannot be checked, is shown by its question alone: its text
is withheld from excerpt, exactly as /context withholds it. An answer’s
text never arrives as an untyped document passage.
A typed hit on a record that cites a document which has since moved or been
withdrawn carries support, and a statement in that state has an empty
excerpt, as /context
serves it.
How a query matches
Section titled “How a query matches”q is read as a web-search query, with English stemming, the same way for
documents and for records:
- Every word must match.
sso redirectfinds what mentions both, not either one. - Words match by stem, not by substring.
redirectsmatchesredirect, butdirectdoes not match insideredirect— there is no mid-word match. "quoted phrase"matches those words in that order.orbetween two words matches either:okta or saml.-wordexcludes what contains it:sso -okta.
A record is matched on its name and slug, its field values and its prose, and a match in its name outranks one in a field, which outranks one in its prose. Search reads a bounded, index-ranked selection however large the Brain is; it never refuses a Brain for its size.
rank is a position, not a score: 1 is the best match. Ranking fuses a
keyword leg and a semantic leg into an ORDER, and there is no comparable
relevance number underneath to hand you. version is the exact document
version the passage was cut from, and it is what the receipt records.
source is the id you wrote the document under — the address to act on, and the
one that round-trips into GET /documents/{documentId}. slug is the Brain’s internal
address for the page (an opaque hash, derived from the Brain and your document
id, for anything written through this API), and it is what lets you line a hit
up against a /context citation.
mode is the whole narrowing vocabulary:
mode |
Ranks by |
|---|---|
fulltext (default) |
Keywords. No embedding round trip, so it is the fast path |
hybrid |
Keywords and meaning, fused — matches phrasings you did not use |
The response echoes the mode it actually ranked by. hybrid needs an
embedding provider, and a deployment without one answers your hybrid request
with keyword ranking rather than failing a read it can still serve — so
data.mode comes back fulltext and you can tell a fused answer from a
keyword one. Check it before you conclude that semantic matching did not help.
Search takes no filter, exactly like /context.
Ranking reads a passage index, and that index knows nothing about your document
metadata. When you want exact resolution rather than a ranking — this kind, this
slug, every gold-tier site whose contract ends this quarter — that is /query
below. Ranked retrieval and typed filtering are different tools, and Nicia will
not pretend one is the other by accepting a filter it cannot apply.
Query — rows
Section titled “Query — rows”When you want records by their declared fields rather than ranked passages:
curl -X POST "$API/v1/brains/tenant:acme/query" \ -H "authorization: Bearer $NICIA_KEY" \ -H "content-type: application/json" \ -d '{ "kind": "person", "where": { "slug": "bob-smith" }, "limit": 20 }'{ "data": { "records": [ { "id": "bob-smith", "kind": "person", "fields": { "name": "Bob Smith", "role": "VP Platform" }, "sources": ["zendesk:ticket:8821"], "token": "brain-record-token-v1:9f2c4b6a1d0e8f735a4c2b9e1d6f0a3c8b5e7d2a4f6c9b1e3d5a7f0c2b4e6d8a", "updatedAt": "2026-08-02T14:20:00Z" } ], "staged": 0, "warnings": [], "receipt": "rcp_019fff44" }}Every declared field is filterable, and every predicate is answered from a typed index — never a scan. A bare value is equality; an operator object names one or more operators on the same field:
| Operator | Applies to | Matches a record that… |
|---|---|---|
"gold" or { "eq": … } |
string, enum, number, boolean, date, datetime, and slug |
holds exactly that value |
{ "ne": … } |
the same scalar types | holds the field, with any other value |
{ "in": [ … ] } |
the same scalar types, and slug; up to 50 values |
holds one of the values |
{ "has": … } |
string_list |
holds that exact element |
{ "lt" / "lte" / "gt" / "gte": … } |
number, date, datetime |
holds a value on that side of the bound |
A query ANDs at most eight predicates; each operator counts as one, so
{ "gte": "2026-10-01", "lt": "2027-01-01" } spends two. A record that does not
hold a field matches no operator on it, ne included. The question “gold sites
whose contract ends before December” is one request:
{ "kind": "site", "where": { "tier": "gold", "contract_end": { "lt": "2026-12-01" } }, "limit": 50}Nothing normalizes text: "Gold" does not match "gold", and nothing is
trimmed. A date operand is a calendar day (YYYY-MM-DD); a datetime operand
is an instant with its offset, compared as the instant it names — see
Schemas.
What is refused, always with a 400 naming the field and never with an empty
page:
- a field the Brain does not declare (for that
kind, when you name one). Undeclared values are stored and returned; declaring the field is what indexes it; - an operator the declared type does not take (
lton anenum,hason astring), or an operand it cannot hold ("12/01/2026"for adate); 400 index_not_ready, only for a declared field whose index is being rebuilt because its declaration just changed — a field declared after records carrying it were written, for example. Retry shortly; every other field stays queryable meanwhile.
linked narrows to the records with one declared link to one exact
kind:identity target, then applies where inside that set. There is no or,
no ordering by a field, and no multi-hop traversal.
Paging
Section titled “Paging”/query is keyset-paged with an opaque cursor. Its limit may ask for at most
100 records — that is the reader’s page, and a larger one is a 400 rather
than a page quietly cut down to size:
curl -X POST "$API/v1/brains/tenant:acme/query" \ -H "authorization: Bearer $NICIA_KEY" \ -H "content-type: application/json" \ -d '{ "kind": "person", "cursor": "eyJrIjoi…" }'{ "data": { "records": [], "staged": 0, "warnings": [], "receipt": "rcp_019fff44", "nextCursor": "eyJrIjoi…" }}Pages walk records in codepoint order of their slug — byte order, so a-c
comes before ab — with or without where. Cursors are stable under concurrent
writes: a record added mid-walk will not shift the page boundary and cause you
to skip one, and a record that matched on every page you read appears exactly
once. A record that starts or stops matching while you walk appears if it
matches when its page is read.
/search is not paged. It is a ranked top-N read with no stable cursor to
hand out, so it takes a limit of at most 20 and returns the best matches for
your query. Ask a narrower question rather than walking a ranking — a cursor
here would have served you the same first page over and over.
Receipts
Section titled “Receipts”Search and query both issue receipts, exactly like /context. Anything that
shows a customer information can be reconstructed later, whichever endpoint
produced it. See Receipts.
Which one do I want?
Section titled “Which one do I want?”- Putting knowledge in a prompt →
/context. It selects, budgets, and renders for you. - Showing sources, or building your own selection →
/search. - Filtering records by declared fields for a table, dashboard, or rule →
/query.
If you are reaching for /search to then paste the results into a prompt, use
/context with format: "items" instead — you get the same pieces plus the
budgeting.