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

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.

Terminal window
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.

q is read as a web-search query, with English stemming, the same way for documents and for records:

  • Every word must match. sso redirect finds what mentions both, not either one.
  • Words match by stem, not by substring. redirects matches redirect, but direct does not match inside redirect — there is no mid-word match.
  • "quoted phrase" matches those words in that order.
  • or between two words matches either: okta or saml.
  • -word excludes 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.

When you want records by their declared fields rather than ranked passages:

Terminal window
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 (lt on an enum, has on a string), or an operand it cannot hold ("12/01/2026" for a date);
  • 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.

/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:

Terminal window
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.

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.

  • 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.

  • Context — the prompt-shaped read.
  • Schemas — declaring the fields /query filters on.