Changelog
The Brain API is in beta. That label buys exactly one thing — the freedom to change a shape before anyone is depending on it — and it costs one thing in return: every change gets written down here, including the ones that remove something these pages used to describe.
2026-10-21 — brain_query is POST /query
Section titled “2026-10-21 — brain_query is POST /query”Changed: the MCP tool brain_query takes the body of POST /v1/brains/{handle}/query less branch (kind, where, linked, cursor, limit, and an optional consumerId, which defaults to the key’s id) and answers with that route’s output: records, staged, warnings, receipt (the receipt id) and nextCursor. It is one service, so an agent and a developer read the same record, token and support included, and a branch-pinned key gets the same access_scoped warning. Previously it took {query: {kind: "source_backed_entities", recordKind, predicates, semanticIds, includeBody}, consumerId} and answered {snapshot, result: {items}, receipt}. A record is now looked up by where: {slug} rather than semanticIds, the full receipt is read with brain_receipts, and the documents query kind is gone: use brain_documents or brain_context.
2026-09-30 — Cloning a Brain can copy its documents
Section titled “2026-09-30 — Cloning a Brain can copy its documents”Changed: a console clone now offers two choices. empty is structure only: the declared schema and governance, with no records and no documents. full is everything: the schema, every record, and every document the source Brain serves. Copied documents are new submissions in the clone (fresh identities, the same bytes and metadata, served at once), and a record’s cited documents are re-pointed at the clone’s copies. Only the served version of a document is copied, so a citation of a superseded version is dropped rather than left pointing at the source Brain. Previously a full clone left documents behind.
2026-09-30 — A cited document that moved is never served as current
Section titled “2026-09-30 — A cited document that moved is never served as current”Added: support on a record a read serves, on GET /records/{id}, POST /query, /context, /search, a view read and the MCP tools, when a document the record cites has moved or been withdrawn since the citation pinned it. state is source_withdrawn or support_at_risk (the cited words are no longer in the served version), and reasons names each document. A record whose citations still hold carries none.
Changed: /context serves a statement whose cited document moved or was withdrawn without its text, as it serves a standing answer that is not current. This closes the gap where a withheld answer’s citations were served beside it with their old quotes, so an agent read “Free tier | 10 Brains” after the row said 3. Any other record is served whole and marked. A /search hit on such a statement has an empty excerpt.
Added: a warning of kind evidence_moved on /context, and evidence[] on GET /receipts/{receiptId}/now: each record the receipt served whose cited documents have since moved or been withdrawn, with the support it was served with and its support now, even when the record’s own version is unchanged.
2026-09-30 — Your own agent can keep a monitored answer
Section titled “2026-09-30 — Your own agent can keep a monitored answer”Added: anchor on an evidence citation of a record, batch or statement write: § <heading> or L12-L14. The Brain resolves it over the document version it serves, refuses one that names nothing, is ambiguous, or does not contain your quote, and computes the evidence pins itself. A statement that cites an anchor is monitored, so a standing answer that depends_on it reads current, and needs_review when that part of the document changes. Reads show the resolved anchor on the citation. sourceAnchor and anchorHash join startByte and endByte as locator keys only the Brain writes. A statement cites one anchored part.
Added (preview): the MCP tool brain_documents: list the documents a Brain serves with each one’s outline, or read one document or one section of it. Reading text is receipted.
Added: staged and warnings[] on MCP brain_context, the same ones POST /context reports: a source that did not fit, a change waiting in review, an answer that is not current. Changed: a statement that cites exactly one document and states its own sourceAnchor rests on that anchor, as if it had been sent as the citation’s anchor; a sourceAnchor the document does not support is now refused. A record write’s body field is prose on every kind and is no longer reported as undeclared.
2026-09-30 — A spreadsheet becomes records through a snapshot
Section titled “2026-09-30 — A spreadsheet becomes records through a snapshot”Added (preview): POST /v1/brains/{handle}/sources/{name}/snapshots/{snapshotId}/tables adds a CSV or TSV table to a snapshot, one document per row, with a mapping from columns to a declared kind’s fields. The row’s identity column, made into a slug, is the record’s id. Sealing reads every mapped cell into its field without a model, so the snapshot is planned when the seal returns; a cell its field cannot hold is kept as a statement. A table with an empty, unaddressable, or repeated identity is refused whole with 400, naming the rows. A complete refresh retires the rows a later export no longer holds; a partial one withdraws nothing. See Add a table.
Changed: an apply that writes more than 400 records commits them in batches ahead of the write that serves their documents, so a 2,000-row table loads as one complete snapshot in one call. A retry after an interruption does not rewrite what already committed.
This brings the registered v2 Brain surface to 84 operations.
2026-09-28 — Search finds standing answers again, with their state
Section titled “2026-09-28 — Search finds standing answers again, with their state”Added: kind on a /search hit for a typed record, and validity and verification on a standing answer’s hit. An answer whose grounds moved, or cannot be checked, is shown by its question alone. Its page is still never returned as an untyped passage. A search receipt now records what it served, so GET /receipts/{id}/now compares a searched answer’s grounds too.
2026-09-28 — A receipt says when an answer it served lost its grounds
Section titled “2026-09-28 — A receipt says when an answer it served lost its grounds”Added: answers[] on GET /receipts/{receiptId}/now. For every standing answer the receipt served, it gives the validity it was served with and its validity now, and since: grounds_moved (even when the version is unchanged), replaced, unchanged, or unknown (for a receipt that predates validity). movedSince names each reason that arose after the read. It reports what changed after the read, never that the answer was wrong when served.
Fixed: a retired source document now puts the answers that rest on its claims in needs_review (source_withdrawn). Previously a retired document could still read as supporting them.
2026-09-28 — A standing answer says whether it was checked
Section titled “2026-09-28 — A standing answer says whether it was checked”Added: verification beside validity on every standing answer a read serves: verified (the served version passed its regression check for that exact revision, with no override), overridden (published on a reviewer’s recorded reason, with the check’s outcome kept), not_evaluated (written directly, without Review), or unknown. A direct update never inherits an earlier version’s verdict. Exports carry it in answers.json.
Changed: approving a change that carries a standing answer now requires a passing, current regression check, or an evaluationOverrideReason on POST /changes/{changeId}/lifecycle/approve. A check still running answers 409. The gate governs Review approval only; whether an agent’s write enters Review is still the Brain’s admission level.
2026-09-28 — A standing answer says when its grounds moved
Section titled “2026-09-28 — A standing answer says when its grounds moved”Added: validity on every standing answer a read serves: /context, /query, GET /records/{id}, a view read, and the MCP brain_context and brain_query tools. It is current, needs_review (with reasons naming each claim that changed or was removed, each claim whose quoted source text changed, and each new claim from a document the answer already uses), or unknown (when Nicia cannot tell, including an answer that rests on no claim). Grounds are measured from affirmed, the answer’s creation or the last time a person or a reviewer decided it still holds, so an agent’s unreviewed rewrite cannot clear them. It also names the monitored claims and documents, and the log position the check read through (observedThrough). Once the grounds move, rebuild reports what the rebuild automation did, including a failed rebuild.
Changed: /context withholds the text of a standing answer that is not current. It serves the question and validity only, and adds a warning of kind answer_not_current.
Changed: /search no longer returns a standing answer’s text, from either its record or its page, because a search hit carries no validity.
Added: a Brain export carries answers.json, each standing answer’s validity as of the export.
2026-09-27 — Every value knows who said it
Section titled “2026-09-27 — Every value knows who said it”Added: GET /v1/brains/{handle}/records/{recordId}/fields/{field} returns a field’s look-behind. It gives the value held, the producers whose latest word is that value (support), the other values producers still assert (dissent), and every assertion and retraction in recorded order (history). asOf answers as the Brain had recorded the field at an instant. Every accepted write now records per-field attribution: a person, an agent, an API key, or a source. Records accepted before this release are attributed to internal:system:legacy.
Added: views. A schema may declare named, read-only views over its records, each with bindings (the declared field an everyday term means), prefer (whose word wins when producers disagree), and optional fields and kinds. GET /v1/brains/{handle}/views/{view}/records/{recordId} reads a record through one, receipted with the record version and the view’s schema version. ?view= on a field’s look-behind answers through a view. A write key may add a view; changing or removing one needs admin. The registered v2 Brain surface now has 83 operations.
Changed: an agent’s write that would change a value a person set now waits for review below L4, instead of overwriting it. Agents keep updating values that other agents or keys set.
2026-09-27 — A person’s snapshot no longer overwrites a teammate’s value
Section titled “2026-09-27 — A person’s snapshot no longer overwrites a teammate’s value”Fixed: a source snapshot applied with a management key or a console session counts as a person’s write, and a person’s writes land at every level. So when such a snapshot contradicted a value someone had set by hand, its replacement value landed, and the hand-set value was overwritten. That broke the rule that a snapshot never overwrites a value a person or another source supports. The change now waits in Review at every level below L4, exactly as a Brain key’s does. Existing Brains pick up the corrected rule when their seeded policy is re-materialized.
2026-09-27 — Limits you could only find by hitting them
Section titled “2026-09-27 — Limits you could only find by hitting them”Documented: a source snapshot extracts at most 200 typed values and 100
statements from one document. A document over either cap fails as a whole:
its plan entries are unknown, nothing is truncated. The snapshot limits table
now lists both.
Changed: the brain_write_records tool description states its 20-record
cap and tells an agent to send a larger set, such as spreadsheet rows, as
several calls with their own idempotencyKey. It used to say only “one atomic
batch”, so an agent with a long list learned the cap from a validation error.
Changed: the Brain Agent and the build.md and import.md procedures no
longer turn away operational data. Leads, accounts, invoices, and activity logs
can live in a Brain; whether they should is your call.
2026-09-25 — Brain MCP tools are named brain_*
Section titled “2026-09-25 — Brain MCP tools are named brain_*”Breaking: every tool on the Brain MCP server is renamed from knowledge_*
to brain_*, with no aliases: brain_schema, brain_query, brain_context,
brain_events, brain_write_document, brain_write_records,
brain_evolve_schema, brain_receipts, brain_reads, and brain_proposals.
Inputs, outputs, and capabilities are unchanged. A client that cached the old
tool list must list tools again; an agent prompt or skill that names a
knowledge_* tool must use the new name.
2026-09-25 — Agents can propose the schema they need
Section titled “2026-09-25 — Agents can propose the schema they need”Changed: PATCH /v1/brains/{handle}/schema and POST /schema/promote
accept a Brain key with write as well as admin. Submitting a schema change
is not deciding it: the Brain’s mode decides, as it does for record writes, so
an agent’s additive change lands on an open Brain and waits in review on a
reviewed one. Removing or retyping declared structure is still refused. A key
without admin may only add new kinds, fields, and links; changing an existing
declaration, including an enum’s values or a safeguard, is a 403 for it
(SCHEMA_EDIT_NEEDS_ADMIN over MCP).
Added: the brain_evolve_schema MCP tool, offered to keys that can
read and write. A brain_write_records refusal for an undeclared kind
now lists undeclaredKinds in its action and points to
brain_evolve_schema, or tells a write-only key to ask its operator.
Changed: schema candidates suggest date, datetime, and — for a small
set of recurring labels — enum fields, not only string, number,
boolean, and string_list. Dates mixed with free text are suggested as a
string instead of being dropped.
2026-09-25 — Query records across the Brains on a schema
Section titled “2026-09-25 — Query records across the Brains on a schema”Added: POST /v1/schemas/{slug}/query runs one typed record query across
every Brain built on one of your organization’s schemas, with the kind and
where of POST /v1/brains/{handle}/query checked against the organization
schema. Records carry their Brain’s handle; each Brain reports an outcome
and, when served, the receipt its read committed. See querying across the
fleet.
2026-09-25 — Published changes leave the review queue
Section titled “2026-09-25 — Published changes leave the review queue”Fixed: a staged change published through
POST /changes/{changeId}/lifecycle/publish is no longer counted as awaiting
review. /context’s staged kept counting it, and the console’s Review queue
kept listing it with its rows pending.
2026-09-25 — Snapshot changes in the review queue; withdrawn statements
Section titled “2026-09-25 — Snapshot changes in the review queue; withdrawn statements”Fixed: GET /changes?status=staged no longer fails with 500 once a
source snapshot’s change is waiting for review. A change now lists the source
documents it serves or retires under a new optional documents field
({ source, id, action }, action being serve or retire); records holds
records only.
Fixed (preview): when a snapshot withdraws a document — a complete
snapshot leaves it out, or its new version no longer says it — the statements
only that document made are retracted with it, in the same outcome: a retired
document’s statements wait in its review. A statement a document the source
still serves makes is kept. Before, they stayed active and /context kept
serving them.
2026-09-23 — Three-way compare for branches
Section titled “2026-09-23 — Three-way compare for branches”Changed (preview): GET /branches/{branchId}/compare and branch
Proposals are three-way. They report the branch’s changes since its fork,
applied onto main as main is now. Compare no longer fails with 409 once
main has moved past the fork. Changes main made after the fork are no longer
reported as branch changes. Edits to different fields of the same record
merge. A field, record, or document that both sides changed differently is
reported as a conflict, with main’s value at the fork, main’s value now, and
the branch’s value. diff is now typed as records and documents, and
summary gains conflicts. A Proposal with conflicts still opens. Its
conflicting operations wait in review, and only an explicit approval
publishes the branch’s value.
Fixed: listing a branch’s documents no longer stops paging early when a page contains a document pin without a stored version.
2026-09-23 — Read-only branches; reads never wait on a writer
Section titled “2026-09-23 — Read-only branches; reads never wait on a writer”Added (preview): POST /branches accepts mode: "read_only", a branch
pinned at its fork point. It refuses every write with 409 — records,
documents, and opening a Proposal — and a key scoped to it can hold only
read. Branches report their mode. Omitting the body still creates a
writable branch.
Changed: reads of a branch no longer take its write lease, so concurrent
reads, and a read beside a write, no longer fail with 409. Each read is
served from one committed branch head.
Changed: destroying a branch now removes everything the fork and its
writes created — indexes, date crossings, document pins, unreferenced staged
documents, watermarks — and closes its open Proposals, so a destroyed branch
no longer emits date.reached. Archiving a branch parks its date crossings
and index rebuilds until it is restored.
2026-09-23 — Read a branch as its own Brain view
Section titled “2026-09-23 — Read a branch as its own Brain view”Added (preview): /context, /search, /query, and document reads accept
a branch coordinate. A branch-scoped key uses its branch implicitly. Branch
records and staged document versions are indexed and served there without
appearing on main. /branches/{branchId}/compare and Proposals include the
document delta; approved ChangeSets publish it through main’s existing policy.
Changed: read receipts pin branchId; receipt and who-was-served lookups
respect it. Events carry branchId, and a branch-scoped key sees only its
branch’s events. Branch answers include access_scoped. Branch documents
currently require extract: false; direct branch extraction is not yet
available. The v2 operation count remains 81.
2026-09-23 — Branch Proposals and scoped Brain keys
Section titled “2026-09-23 — Branch Proposals and scoped Brain keys”Added (preview): GET /branches/{branchId}/compare reads a branch’s
logical delta against main without staging a change. POST /proposals pins a
branch’s current delta for review; GET /proposals, GET /proposals/{id},
POST /proposals/{id}/revise, and POST /proposals/{id}/close manage its
revisions. Snapshot review changes and staged direct agent writes also appear
as Proposals. Applying a Proposal uses the existing ChangeSet lifecycle and
main’s policy. Proposal events join the Brain event stream and webhook payloads.
The MCP endpoint can list and inspect Proposals.
Added (preview): POST /keys accepts an optional branch to mint a key
restricted to one branch. Such a key can write that branch and open its
Proposal, but cannot address main’s records or mint keys.
Removed: the former merge planning endpoint and its automatic publication behavior. Branches are no longer called experiments on the wire. The registered v2 Brain surface now has 81 operations.
2026-09-23 — Typed field queries, date fields, and date.reached
Section titled “2026-09-23 — Typed field queries, date fields, and date.reached”Added: date and datetime field types. A date is a calendar day
(YYYY-MM-DD); a datetime is an ISO 8601 instant with its offset, stored and
returned in UTC. Every write path holds a value to its declared type — record
writes and batches (an update to an existing record included), statements whose
predicate names the field, and source snapshot extraction. See Dates and
instants.
Changed: POST /v1/brains/{handle}/query filters on every declared field.
where takes a bare value (equality) or an operator object: eq, ne, and
in on scalar fields, has on a string_list, and lt, lte, gt, gte on
number, date, and datetime — at most eight predicates, AND-combined, each
answered from a typed index. Paging and order are unchanged. See Search and
queries.
Changed: a where predicate on an undeclared field is now a 400
saying the field is not declared. It used to answer 400 index_not_ready,
which now means only that a declared field’s index is being rebuilt after its
declaration changed — retry shortly. A declared non-slug field no longer
returns index_not_ready at all. The Brain MCP server’s knowledge_query
takes the same predicates.
Added (preview): a date or datetime field may declare notifyBefore
ISO 8601 offsets (P30D, P0D for the date itself). When a record’s value
minus an offset is reached, the event stream carries one date.reached —
exactly once per record, field, value, and offset — and Brain-key webhooks
deliver it like any other event. See Date
reached.
The registered v2 Brain surface has 81 operations after the Proposal additions.
2026-09-23 — Each organization’s schema registry is its own
Section titled “2026-09-23 — Each organization’s schema registry is its own”Fixed: POST /v1/schemas checked a registered kind against every
organization’s registrations: registering product refused with
409 conflict when any other organization had registered a product with
different fields. A registration is now the organization’s own — two
organizations may register the same slug and the same kind names with
different shapes, and each organization’s Brains, provisioned by slug or by
extends, carry that organization’s shape.
Fixed: GET /v1/schemas listed schemas other organizations had
registered, and schemas the platform bundles for its own demos. It lists only
your organization’s registrations, ordered by slug; GET /v1/schemas/{slug}
answers 404 for a slug only another organization registered.
Changed: contract is always present on GET /v1/schemas/{slug} and the
commit response; schema is derived from it, as a Brain provisioned from the
slug commits it. A schema-editor proposal is merged onto the registered
contract and refused 409 only for dropping or retyping a declared kind or
field. An organization holds at most 100 registered schemas; registering a new
slug past that is 409 conflict.
2026-09-23 — Preview: the event stream and Brain-key webhooks
Section titled “2026-09-23 — Preview: the event stream and Brain-key webhooks”Added (preview): GET /v1/brains/{handle}/events (read) is one ordered,
gap-free stream of what the Brain accepted and how its changes moved:
record.accepted and record.deleted (with the names of the fields a change
touched, never their values), statement.asserted and statement.retracted,
document.served and document.retired, schema.evolved, change.staged,
change.decided and change.published, availability.changed, and
snapshot.planned and snapshot.applied. Events carry ids, field names and
version numbers only. Page it with an opaque cursor passed back as after;
filter it with types.
Added (preview): POST /v1/brains/{handle}/webhooks,
GET /v1/brains/{handle}/webhooks and
DELETE /v1/brains/{handle}/webhooks/{webhookId}
(admin; creating one also needs read) push chosen event types to an HTTPS URL on a public host, in order
and at least once, signed with a secret shown once. Each body is one event as
GET /events lists it plus the previousCursor, so a receiver that missed a
delivery reads the gap from the stream. The Brain MCP server gains
knowledge_events; @nicia/brain-client gains listEvents, createWebhook,
listWebhooks and deleteWebhook. See Events and
webhooks.
This brings the registered v2 Brain surface to 76 operations.
2026-09-23 — Preview: queryable receipts
Section titled “2026-09-23 — Preview: queryable receipts”Added (preview): GET /v1/brains/{handle}/receipts lists receipts newest
first, without their items, filtered by consumerId, kind, key, and a
committed-time window (since inclusive, until exclusive). A key lists the
receipts it committed; a key with admin lists every key’s. See
Listing receipts.
Added (preview): GET /v1/brains/{handle}/records/{recordId}/reads,
GET /v1/brains/{handle}/documents/{documentId}/reads, and
GET /v1/brains/{handle}/sources/{name}/documents/{documentId}/reads list every
receipted read that served the address, from any key, with the version it
served, the version served now, and status (current, superseded,
retracted, deleted, held). stale=true keeps only the reads a
correction made stale. They need review and return no content. See
Who was served.
Changed: a key with admin — the management key included — can read any
receipt on its Brain through GET /receipts/{id}, /now, and /documents,
where before only the key that committed it could. A receipt still names the
key that committed it. Any other key still gets 404.
Changed: the API export carries receipts.json: the receipts the exporting
key can list, with their snapshots, hashes and items, never the text a read
was handed. These pages said the export omits receipts.
Added (preview): the Brain MCP server gains knowledge_receipts (list
receipts, or get one) for a read key and knowledge_reads (who was served a
record or document) for a review key. @nicia/brain-client gains
listReceipts, listRecordReads, listDocumentReads, and
listSourceDocumentReads.
This brings the registered v2 Brain surface to 72 operations.
2026-09-23 — Review declarations travel with the schema; statements on every Brain
Section titled “2026-09-23 — Review declarations travel with the schema; statements on every Brain”Changed: POST /v1/schemas takes { "contract": … }, a complete
DeclaredSchemaContract, and registers it exactly as sent — each field’s
multiplicity and consequence included — so a Brain provisioned from the
slug, or extends it, carries them. GET /v1/schemas/{slug} and the commit
response return contract. The schema-editor proposal body is still accepted
and still cannot set either declaration. GET /v1/schemas/{slug}/brains lists
changedKinds and changedFields, and GET /schema-lineage’s drift lists
changedKinds and changedFieldsByKind, where the baseline declares a kind or
field differently. A rollout applies a changed declaration to tracking Brains,
and reports failed for a Brain whose review declarations it would lower. See
your organization’s own schema.
Changed: PATCH /v1/brains/{handle}/schema can set, tighten, or loosen a
field’s multiplicity and consequence (it refused both). The Brain stores
exactly the contract admitted, and declaredVersion in the answer is that
contract’s version. Loosening a review declaration is admitted like a
destructive change: from a Brain key it waits in review. See review
declarations.
Changed: the statement kind and its links are on every Brain’s schema.
POST /v1/brains/{handle}/statements works on any Brain (it answered
rejected, Unknown entity kind "statement", on a Brain that had not
declared the kind), and applying a snapshot’s plan records a disagreement as
a statement on any Brain. A schema that declares its own statement gets the
platform’s. GET /schema lists the kind.
Changed: an admin key may read GET /v1/brains/{handle}/schema, the
schema it evolves; it still reads none of the Brain’s contents. The reference
publishes the alternative as x-nicia-brain-capability-alternatives, and
@nicia/brain-client’s operation metadata as alternativeCapabilities.
Fixed: a record of a kind whose identityField is a declared field — the
first way to declare one is the registry’s contract above — could be written
but not read: GET /records/{id} answered 404 and /query nothing. Records
are read at their id, as they are written.
2026-09-23 — Preview: the text a receipt pinned; applying a plan writes its records
Section titled “2026-09-23 — Preview: the text a receipt pinned; applying a plan writes its records”Added (preview): GET /v1/brains/{handle}/receipts/{receiptId}/documents
returns the exact text of every document version a receipt pinned, in the
receipt’s order, even after the document was rewritten or retired, with a
contentHash of each. Only the key that committed the receipt can read it;
any other key gets 404. Records the receipt pinned are not included. Each
page is a read of its own: it commits a receipt of the new kind document and
returns its id as receipt. Up to 25 documents per page. @nicia/brain-client
gains getReceiptDocuments. See Receipts.
Changed (preview): applying a snapshot’s plan now writes what its
documents state into your records, under the same decision as the documents
they came from: new records and fields, changed values this source alone
supported, and withdrawals of values nothing else supports — a field unset,
or a record deleted when every value it held is withdrawn. A value a person or another source supports is never
overwritten: a contradiction is staged for review as a statement of what the
source said and a separate proposed value, and a reviewer selects one or both.
Statements are written only in a Brain whose schema has the Standing Answers
kinds. GET /v1/brains/{handle}/changes/{changeId}/lifecycle names, on each
operation, the record ({ kind, id }) it writes or the document
({ source, id }) it serves or retires. See Snapshots
and Sources, both new.
Corrected: these pages said a receipt “still resolves to the text that was
actually shown” and that GET /documents/{id} serves a document’s text. The
document read returns status and version, not text; the text a receipt pinned
is the read above.
This brings the registered v2 Brain surface to 68 operations.
2026-09-23 — Preview: apply a snapshot’s plan; receipts name the documents they served
Section titled “2026-09-23 — Preview: apply a snapshot’s plan; receipts name the documents they served”Added (preview):
POST /v1/brains/{handle}/sources/{name}/snapshots/{snapshotId}/apply with the
planRevision you read decides the plan document by document under the
Brain’s admission policy. Admitted documents are served at once; the rest are
staged in one review change (review.changeId, the id /changes/{changeId}
reads) or returned in rejected. A document missing from a complete
snapshot is retired only through that decision: at the default rung an agent
source’s retirement waits for review, and until a reviewer publishes it the
document keeps being served. A retired document’s earlier versions stay
readable by the receipts that pinned them. Repeating the apply of the same
revision returns the same body. 409 whose message starts
snapshot_plan_stale means what the source serves, or the Brain’s schema,
changed since the plan: seal again and re-read it. Applying a later snapshot
recomputes an earlier snapshot’s still-pending review for the documents the
later one covers. A snapshot now reports applied, or awaiting_review with
the reviewChangeId it waits on. @nicia/brain-client gains
applySourceSnapshot.
Changed: a receipt’s snapshot now carries evidenceSetDigest, naming
which documents, at which versions, the Brain served for that read. Writing a
document — through a snapshot or POST /documents — changes it without
changing stateVersion, so two receipts at one stateVersion can now differ.
A receipt item for a document of a named source carries
document: { source, id } and uses your document id as source.
This brings the registered v2 Brain surface to 67 operations.
2026-09-23 — Preview: snapshot plans read your schema
Section titled “2026-09-23 — Preview: snapshot plans read your schema”Changed (preview): sealing a snapshot of a Brain whose schema declares
fields now reads every added or changed document into those fields. The
snapshot is extracting until the last document is read, then planned; a
Brain whose kinds declare no fields plans at once, as before.
GET /v1/brains/{handle}/sources/{name}/snapshots/{snapshotId}?plan=true now
returns each document’s content hashes and the record fields it states, with
the value, the quote it was read from, and how it compares with what the Brain
has accepted: new, restated, changed, contradicts, disagrees,
withdrawn or unknown, and who supports the accepted value.
?findings=true pages what the snapshot’s own documents say about each other:
duplicate, disagrees, and — on a field declared single — contradicts.
A plan or findings page commits a receipt of the new kind plan and returns
its id as receipt; polling a snapshot’s status commits none. Resealing a
planned snapshot reuses every document already read. Applying a plan still
decides only its documents: the record fields a page shows are not yet written.
2026-09-22 — Preview: source snapshots
Section titled “2026-09-22 — Preview: source snapshots”Added (preview): a source is your key plus a name you choose, such as one
customer’s content.
POST /v1/brains/{handle}/sources/{name}/snapshots opens a snapshot of it.
POST /v1/brains/{handle}/sources/{name}/snapshots/{snapshotId}/documents adds
up to 100 Markdown documents at a time, each acknowledged new, changed or
unchanged against what the source serves.
POST /v1/brains/{handle}/sources/{name}/snapshots/{snapshotId}/seal with
completeness: "complete" or "partial" compares the snapshot with what the
source serves and answers 202.
GET /v1/brains/{handle}/sources/{name}/snapshots/{snapshotId}?plan=true pages
the plan: each document added, changed, unchanged or, for a complete
snapshot, missing. Entries name the document and its class only; versions and
content hashes are omitted until plan reads are receipted.
POST /v1/brains/{handle}/sources/{name}/snapshots/{snapshotId}/cancel ends
one, and GET /v1/brains/{handle}/sources lists every source with its owner.
Nothing in a snapshot is served yet. Its documents are stored but do not
appear in /context, /search, /query or the document listing; applying a
plan is not available yet. Only the key that opened a snapshot can add to,
seal, read or cancel it; any other key gets 404. An open snapshot expires
after 24 hours and a planned one after 7 days. @nicia/brain-client gains the
matching preview methods.
This brings the registered v2 Brain surface to 66 operations.
2026-09-22 — Record search is full-text, large Brains are served, and publish refuses a moved base
Section titled “2026-09-22 — Record search is full-text, large Brains are served, and publish refuses a moved base”Publish refuses a change whose records moved (breaking). POST /v1/brains/{handle}/changes/{changeId}/lifecycle/publish re-checks every record the change was
staged against. If any was edited, created or deleted since staging, nothing is
applied: the answer is 409 conflict naming each record that moved, and the
lifecycle’s application becomes blocked with blockedReason: "candidate_base_moved" and blockedRefreshAction: "restage_against_current_state". Approving again does not help; stage the
change again against the current records. Any client that publishes must
handle the 409. See Review.
Changes staged before this release were discarded. A change that was pending review when this release deployed no longer exists; propose it again.
/search matches records with full-text search. A record now matches when
it holds every word of q, compared by English stem, with web-search syntax:
"quoted phrases", or between alternatives, and -word to exclude. A word no
longer matches inside another word (direct does not find redirect). A match
in a record’s name outranks one in its fields, which outranks one in its prose.
See Search and queries.
Large Brains are served, not refused. /context, /search, /query and
the record listings select what they read in the database, so a Brain of any
size is answered rather than refused with 400. /context picks its
candidates by the prompt and fills the budget from them; omitted counts every
source not rendered, including ones never selected. See
Context.
/query pages in codepoint slug order — byte order, so a-c sorts before
ab. A cursor from a walk started before this release was taken in a
different order: restart that walk from the first page.
A staged deletion is its own lifecycle operation. GET /v1/brains/{handle}/changes/{changeId}/lifecycle lists it beside the record
writes, and select can keep or set it aside like any other operation.
Revoking a key takes effect on the next request. A request authenticates once, at its start; every request after the revocation is refused.
2026-09-21 — Authorization: Bearer is the only scheme
Section titled “2026-09-21 — Authorization: Bearer is the only scheme”x-api-key is refused outright, on every /v1/* request — 401 unauthorized before any credential is even looked up, whether the value is an
organization key, a Brain key, or a personal key. Send Authorization: Bearer nsk_… (an organization key) or Bearer nbk_… (a Brain key) instead.
Personal API keys are gone. The console no longer mints them, and one
minted before this change no longer authenticates anything on /v1/* — there
is no header left for it to reach. An organization key, minted from
Settings → Organization keys or POST /v1/keys, is the credential for a
backend, a script, or an agent working across a whole organization.
x-organization-id still exists, for one case only. A management key
minted from /v1/keys is bound to its organization and never needs it. A
management key minted before that binding existed carries none, and still
requires the header on every call — see Authentication.
2026-09-21 — One vocabulary: change, statement, finding
Section titled “2026-09-21 — One vocabulary: change, statement, finding”Renamed (breaking), with no alias. A request that uses an old name is refused as an unknown field or path, and a response no longer carries the old name.
| Was | Is |
|---|---|
the …/observations and …/records/{id}/observations paths |
…/statements and …/records/{recordId}/statements |
observationIds, observations on a statement write or a finding |
statementIds, statements |
conflictId in /findings/{conflictId} |
findingId |
changeSetId, successorChangeSetId, remainderChangeSetId |
changeId, successorChangeId, remainderChangeId |
adoptedChangeSetId, candidateRevision on a lifecycle |
adoptedChangeId, revision |
proposalId, proposalItemId in a review_required outcome |
changeId, changeItemId |
{id} in record, document, receipt and change paths |
{recordId}, {documentId}, {receiptId}, {changeId} |
Idempotency-Key is the only place a retry key goes. The key mint,
schema promote and evolve, and the two definition writes used to take an idempotencyKey in the
JSON body; that field is gone, and a key minted with one in the body is refused
because the mint body is strict. Send the header, as on every other write.
A batch can carry a precondition per record. Each record in POST /batch takes
an optional ifMatch: the token you last read for it. Every precondition is
checked before anything is written, so one stale token refuses the whole batch
with 409 conflict naming that record, and nothing is applied. A malformed
ifMatch is a 400, and a record that does not exist yet cannot carry one. The
response returns every written record’s new token, so the next batch chains
without a re-read. knowledge_write_records takes the same field.
A receipt describes the read, not the credential’s plane. connectionId and
purpose: "ask" are gone; key already names who read. A receipt item’s version
is the record or document revision it was served at.
Migrating: rename the fields and paths above, move idempotencyKey to the
header, and drop any read of receipt.connectionId or receipt.purpose.
@nicia/brain-client renames to match: reportObservations, listObservations
and withdrawObservation are now writeStatements, listStatements and
retractStatement.
2026-09-22 — Brain MCP follows the current protocol surface
Section titled “2026-09-22 — Brain MCP follows the current protocol surface”The Brain MCP endpoint now supports Client ID Metadata Documents for modern MCP clients while retaining dynamic client registration for clients that still use it. Every MCP tool publishes an output schema, and successful structured results are validated against that schema before they are returned.
Tool calls now have a per-credential safety budget of 120 invocations per minute. When that budget is exhausted, the endpoint returns 429 with Retry-After; discovery and tool-list requests do not consume the budget.
The same five tools are now easier for smaller models to use without hidden
prompt knowledge. Every input schema includes a valid example; context and
query explicitly distinguish semantic retrieval from exact filtering; and the
two write tools return a stable action describing whether knowledge changed,
review is required, or indexing is still in progress. Record writes name the
declaredVersion they validated against. Safe failures carry stable recovery
codes and the next tool when one is required.
A compact mcp-surface-usability harness matrix now compares five model rows
on eight deterministic cases covering tool selection, schema-first writes,
idempotency, concurrency tokens, staged outcomes, and error recovery.
2026-09-21 — The Brain MCP endpoint is addressed by handle
Section titled “2026-09-21 — The Brain MCP endpoint is addressed by handle”Changed (breaking): the Brain MCP endpoint is now
https://api.nicia.ai/mcp/brains/{handle}. The old
…/mcp/brains/{brainId}/connections/{connectionId} is gone, with no alias: a
client still pointed at it gets a 404. The endpoint takes the same nbk_ Brain
key as every other Brain route, sent as Authorization: Bearer, and the key
decides which Brain it may reach — a handle that names another Brain’s answers
the same 404 as one that does not exist. The opaque Brain id works in place of
the handle.
Any Brain key works, whatever client it was minted for. The endpoint no
longer requires the key to have been minted for MCP. Its tools follow the key’s
capabilities: read offers knowledge_schema, knowledge_context and
knowledge_query, and write adds knowledge_write_document and
knowledge_write_records.
A key writes as an agent. A key you mint over the API, or with the Connect
dialog, is a program’s credential, not you: its writes are governed by the
Brain’s mode, so on a reviewed Brain they stage for review even when a person
minted the key. Writes a person makes in the app still land.
OAuth signs in to the same URL. A client that speaks MCP OAuth can be given
that URL with no credential and opens a browser. The token it receives is recorded as a key on that Brain, named
OAuth: <client>, with exactly the read and/or write scopes approved on the
consent screen. A handle that names a Brain in more than one of your
organizations is refused; use the Brain’s id.
Migrating: press Connect in the Brain’s page for the new URL and a key, or
change the URL in your client’s config to /mcp/brains/{handle}. The key you
already have keeps working if it holds the capabilities you need.
2026-09-21 — The id-addressed plane is gone; the handle-addressed one is the whole API
Section titled “2026-09-21 — The id-addressed plane is gone; the handle-addressed one is the whole API”Removed (breaking), with no alias. Every route addressed by a Brain’s
internal id — credential create/list/rotate/revoke, the handshake, the
mutation-batch and evidence writes, and the policy reads — is gone, along with
the eight-name capability vocabulary those routes used and the webhook
registration that was never wired to any of them. A request to any of the old
…/{brainId}/… paths now gets a 404. @nicia/brain-client drops the class
that spoke them and the capability-preset constant derived from that
vocabulary.
The four-name capability vocabulary (read/write/review/admin) —
already the only one PUT /v1/brains/{handle} and everything under it ever
used — is now the only one there is.
Migrating: provision on a handle (PUT /v1/brains/{handle}), mint a
nbk_ Brain key (POST /v1/brains/{handle}/keys) in place of the old
credential, and call documents, records/batch, statements, findings,
changes, context/query/search, receipts, schema, and exports —
every one addressed by the handle, every one taking Authorization: Bearer
with either that key or the organization’s management key. There is no
automated migration: the old credential does not carry over, because the plane
it authenticated is gone.
2026-09-21 — Export the whole Brain over the API
Section titled “2026-09-21 — Export the whole Brain over the API”Added: POST /v1/brains/{handle}/exports starts an export and answers 202
with a queued export; GET /v1/brains/{handle}/exports/{exportId} reports its
state; GET /v1/brains/{handle}/exports/{exportId}/download streams the .zip
once it is completed. A repeated Idempotency-Key on the start returns the
export the first request created. @nicia/brain-client gains startExport,
getExport, waitForExport and downloadExport.
The archive is the whole accepted Brain, not only its documents: every
document at the address you gave it, every typed record as Markdown under
records/<kind>/<slug>.md beside records.json, evidence.json, the complete
revision history, and a manifest whose root hash covers the documents and their
history. Changes still waiting in Review, read receipts and the operation ledger
are not included.
Export needs read, not admin. It carries the whole Brain out, so it sits
on the capability that already reads it, and an admin-only key is refused.
Webhooks still have no Brain-key endpoint. Only the key that started an export
can read or download it; another key’s export id, or one from another Brain, is
a 404.
This brings the registered v2 Brain surface to 59 operations.
2026-09-21 — The Brain MCP server is five tools
Section titled “2026-09-21 — The Brain MCP server is five tools”Removed: the question-answering tool, the custom-context receipt tool, the evidence and mutation submission tools, the mutation-outcome read, and the two correction tools. A call to any of them is now an unknown tool.
The server offers knowledge_schema, knowledge_context, knowledge_query,
knowledge_write_document and a new knowledge_write_records.
knowledge_write_records takes the same body as POST /v1/brains/{handle}/batch
— records of id, kind, fields and links, plus an optional intent —
and an optional idempotencyKey in place of the Idempotency-Key header. It
answers with the same change: applied, staged with the change id, or
rejected with the reason. It goes through the same service as the HTTP route,
so a write means the same thing from either door.
Why: the write tools spoke the substrate’s vocabulary (mutation kinds, base versions, snapshot ids, provenance blocks), which is more than an agent should have to get exactly right, and two of them existed only to read back what a write should have returned. Correcting knowledge is writing new knowledge, so there is no correction tool. Nothing on MCP approves a change: an agent proposes, a person decides.
think is gone everywhere. The console’s Ask box now shows the context the
Brain would serve for the question, with its citations, from the same read
knowledge_context uses. No model is called, so it never writes an answer the
Brain did not vouch for.
2026-09-21 — Preview operations are marked, everywhere
Section titled “2026-09-21 — Preview operations are marked, everywhere”An operation can now be Preview: shipped and working, but not a stability
commitment. It may change shape or be removed without notice. The API reference
lists every preview operation under its own Preview (unstable) tag, marks it
x-nicia-stability: preview and prefixes its summary and description; the
TypeScript client carries the same marker in its manifest and its hover
documentation.
Preview today: every durable-branch operation, the three definition operations,
and on the change lifecycle select, schedule, cancel and the journal read.
Documents, records, statements, context, query and search, changes with
approve, reject and publish, receipts, the schema, and keys are stable.
2026-09-21 — Markdown import ingests documents and types nothing
Section titled “2026-09-21 — Markdown import ingests documents and types nothing”The markdown import lands every file as a document.
The import provisions the Brain under its handle the way PUT /v1/brains/{handle}
does, then submits each file as a document, addressed by its source path, with
its path and frontmatter kept as document metadata. It no longer wires a graph,
so it writes no records: a source type: is metadata, and typing a document is a
later, reviewed step by the Brain Agent.
The confirmation contract shrank to match. The commit body no longer takes
typeMappings, and the proposal no longer proposes typeMappings or
edgeFields. The preview and the report drop pagesTyped, pagesUntyped,
unclassified, foldersDetected, foldersImported, nodeCount and
edgeCount; the report drops sourceCorpusId, and a Karpathy raw/ tree is no
longer split into a separate Sources collection. Each receipt entry now names its
document with evidenceId and no longer carries folderId. A re-import versions
a changed file in place, keeps a moved file’s address, and reports a file removed
from the source without deleting its document.
2026-09-21 — Console page operations address a page by its slug
Section titled “2026-09-21 — Console page operations address a page by its slug”Rename, delete and the deletion plan take a slug, not an artifactId.
The console’s rename, deletion-plan and delete operations now name the page by
its stable address.
A page is a native record, and a record has no artifact to point at.
Rename changes the title only. The address is the page’s identity — receipts
cite it and [[wikilinks]] resolve to it — so a rename never moves it and no
inbound link needs patching. The response reports the slug, the
previousTitle, the new title, and whether the change adopted or is waiting
on review.
Delete is one governed write. The record leaves accepted state and the
chosen link edits (keep, remove or repoint) land in the same batch; the
Brain’s admission policy decides whether it lands now or waits for review. The
plan now also lists backingDocuments, the evidence documents the page cites,
which a deletion keeps. The result carries a changeId (the batch that landed,
or the proposal waiting on review) in place of changeSetId.
Cloning copies native records. A clone claims a new Brain and replays the source’s records into it under the same slugs. Evidence documents stay with the Brain that received them.
Build-from-corpus is removed. The corpus “build a brain” flow and its job polling are gone, along with the per-Brain option to re-structure on source sync. Documents are evidence: an agent promotes them into records through review.
2026-09-21 — Approve and reject move onto the ChangeSet lifecycle
Section titled “2026-09-21 — Approve and reject move onto the ChangeSet lifecycle”The approve and reject endpoints on a change are removed.
Deciding a change is now the lifecycle: POST /changes/{id}/lifecycle/approve
binds the exact candidate and takes no body, POST /changes/{id}/lifecycle/publish
applies it (send an Idempotency-Key; a retry returns the committed outcome),
and POST /changes/{id}/lifecycle/reject requires a reason, which
GET /changes?status=rejected reports back.
Partial approval is select, and it is per record. GET /changes/{id}/lifecycle
lists a change’s operations; POST /changes/{id}/lifecycle/select splits them into a
successor you approve and publish and a pending remainder, and supersedes the
original. The records, fields, and edits body on approve is gone: a field
cannot be selected on its own, and a value cannot be edited on the way through.
partially_accepted is no longer produced.
2026-09-19 — Infer governed definitions from documents and reported counts
Section titled “2026-09-19 — Infer governed definitions from documents and reported counts”POST /v1/brains/{handle}/definitions infers competing definition candidates
from Brain documents and counts reported by the authenticated producer. The
server pins the accepted snapshot, preserves unknown membership, grounds counts
only to matching concepts, and reports facts that would distinguish competing
rules. GET /definitions inspects immutable candidates and POST /definitions/{candidateId}/review selects, approves, or rejects one through
the authoritative ChangeSet lifecycle. Accepted definitions are versioned,
machine-readable records. Both writes require idempotency keys.
This brings the registered v2 Brain surface to 57 operations.
2026-09-19 — Durable branches and selective publication
Section titled “2026-09-19 — Durable branches and selective publication”A Brain can now fork a durable experiment branch from its exact current default origin, author records on it, and ask the server to derive a merge plan from the branch’s committed state. Planning stages that plan as an authoritative ChangeSet candidate; approval, optional scheduling, and publication use the same lifecycle as every other governed change.
The new operations list, create, inspect, write, plan, archive, restore, and
destroy branches under /v1/brains/{handle}/branches. This brings the
registered v2 Brain surface to 53 operations.
2026-09-18 — Record concurrency moves from an integer version to an opaque token
Section titled “2026-09-18 — Record concurrency moves from an integer version to an opaque token”A record’s optimistic-concurrency authority is now an opaque, branch-scoped
token, not the integer version. Reads return it as record.token and mirror
it as a strong ETag; writes take it in If-Match (the same quoted value).
record.version and written record.version are gone.
The token identifies the Brain generation, the active branch and its
incarnation, the record and its incarnation, the editable revision, and the
relevant schema identity. A token from one branch never authorizes a write on
another, and A → B → A edits or delete/recreate do not revive an old token.
A stale or cross-branch token is 409 conflict; a malformed header — an
integer, an unquoted value, a weak tag, the * wildcard, or a list — is 400 validation naming the header. DELETE now honours If-Match too. Receipt
items[].version remains an integer served-version pin and is not a write
precondition.
2026-09-15 — Corrections address a permanent position, and “who was told” is retired
Section titled “2026-09-15 — Corrections address a permanent position, and “who was told” is retired”change.id, correction.correctsChangeId, and every outcome.changeIds entry
now name a permanent Brain-log position ({brainId}:{position}:{entryIndex}:{effectIndex})
instead of a row id from the retired internal change-event table. Anything
that stored an old-style id stops resolving; re-read the change from
/v1/brains/{handle}/changes or a fresh write’s own response.
The affected-reads correction endpoint is removed — formerly a GET on
the correction’s affected-reads sub-path — along with
observedConsumers[]/candidateRuns[] on the knowledge.corrected
webhook payload. Answering “who was told” needed a scan this surface has no
substitute for yet. GET /receipts/{id}/now still tells you, per receipt,
whether what it cited has since changed — write the correction, then poll the
receipts you already hold rather than asking the Brain to enumerate every
consumer.
This brings the registered v2 Brain surface to 45 operations.
2026-09-15 — Brain keys can be renamed and granted new access
Section titled “2026-09-15 — Brain keys can be renamed and granted new access”PATCH /v1/brains/{handle}/keys/{keyId} updates a key’s name and capabilities.
Capability changes take effect for its live credentials on the next request;
the secret does not need to be replaced. Brain Settings now links to key
management, and the Connect dialog shows name and access choices before minting.
This brings the registered v2 Brain surface to 39 operations.
2026-09-02 — A correction says who was told
Section titled “2026-09-02 — A correction says who was told”Removed 2026-09-15 — see that entry above. A GET on the correction’s
affected-reads sub-path answers who was served the content a correction
superseded, at the handle, under a read Brain key. eventId names the
corrected change, not the correction.
observedConsumers[] is receipt-backed — each entry is a read that provably
happened — and candidateRuns[] is the separate, weaker claim about runs the
Brain was merely available to; the two are never unioned, and truncated says
when the observed set is a floor rather than a total. An empty answer means no
receipted read of the superseded content, not that nobody could have seen it.
This brings the registered v2 Brain surface to 45 operations.
The knowledge.corrected webhook payload now carries the same
observedConsumers list, capped at 50 entries and bounded by what was observed;
past the cap, read the endpoint above. It has the same 50-entry cap and reports
truncated with the observed floor; it does not offer a continuation cursor
yet.
knowledge.corrected is also subscribable per key now, not only
organization-wide. A correction reaches the keys whose own receipted reads
observed the superseded content, plus any key on that Brain that asked to hear
about corrections at all. Key matching is exact — there is no
organization-wide or Brain-wide fallback on that scope — and a per-key
registration is not also delivered through the organization-wide /v1/webhooks
path, so one correction is never two deliveries to one URL.
2026-08-31 — Record history is explicit about time
Section titled “2026-08-31 — Record history is explicit about time”GET /v1/brains/{handle}/records/{id}/history now returns the accepted audit
transitions for one record, newest first, with deletion, immutable revision
numbers, actor attribution, and both directions of correction lineage. A
correction that writes a different record still carries enough transition
metadata to inspect it from the original record’s history.
The endpoint deliberately returns no historical field values. changedAt is
the append-only audit-event timestamp, not a coordinate that reconstructs past
accepted state; generic at and asOf remain unsupported. Its availability
means only whether that record was present after the event, not the Brain’s
separate active/held/deleted lifecycle. Receipts remain the proof of exact
values served to a caller, and statement valid time remains explicit on the
statement itself.
2026-08-31 — Statements and conflicts are served
Section titled “2026-08-31 — Statements and conflicts are served”Statements are served: a schema-optional literal or record reference claim can be
written (under the path then named /observations, now /statements), a record’s
retained history read back, and one retracted while retaining
its historical value and provenance. Disagreement from overlapping source
claims is available through
GET /findings; POST /findings/{findingId}/decide keeps one statement or
records a corrected value without deleting the losing evidence. These five
operations are now in OpenAPI and the generated TypeScript client, bringing the
registered v2 Brain surface to 37 operations. /context names contested
facts inline — warnings[] kind contested plus per-item status. There is
no conflicts[] field on the read; GET /findings is the operator inbox.
2026-08-31 — Brain schemas are discoverable and governed
Section titled “2026-08-31 — Brain schemas are discoverable and governed”The public Brain schema loop is live. GET /v1/brains/{handle}/schema
returns the complete governed DeclaredSchemaContract and separately verifies
whether its queryable materialization is ready or drifted; the read never
repairs drift. GET /schema/candidates reports snapshot-keyed kind counts,
declared-field coverage, and recurring undeclared keys. PATCH /schema and
POST /schema/promote create governed Review proposals against an exact schema
and Brain snapshot. They do not rewrite source documents or bypass the one
evolve_schema mutation path; Review acceptance reports success only after the
accepted contract verifies ready.
Structured record queries now fail closed while the governed contract and its materialization disagree. Document reads remain available, because evidence is not discarded merely because its structured projection needs repair.
2026-08-27 — /context retrieves, sign-up opens, and the context check ships
Section titled “2026-08-27 — /context retrieves, sign-up opens, and the context check ships”/context ranks against your prompt. It used to order every record by id
and fill the token budget from the top, so the answer to any question was
whichever sources sorted first alphabetically — the prompt was recorded on
the receipt and never used to choose anything. It now ranks two ways at once: a
record scores because a declared field holds something your prompt named,
and because the passage index ranked its text. Neither penalizes the other,
so a typed record with no prose is retrievable on its fields alone and a
document is retrievable on wording alone. Sources nothing matched keep a stable
order, so the tail stays deterministic.
Three consequences worth knowing. A prose record too large for the budget now
contributes the passages your prompt matched instead of being skipped
whole, and the receipt names those passages. Where a deployment configures a
reranker, a model may re-judge the top of the list — so every answer’s
snapshot now carries the retrieval models it was served under, and none when
no model ran. And one oversized document can no longer empty an answer it
happens to lead. See Request context.
Sign-up is open — no invitation code. Signing in needs a verified email address, and the sign-up endpoints refuse a request that did not come from the product frontend. Authentication and the Quickstart describe the current flow; nothing changed for existing keys.
The context check ships in the product. Open a served context — the thing
a receipt records — and run a check that reports
where the sources behind that answer contradict each other, where wording was
duplicated, and where the payload carried no information. Contradiction
findings pass a live-model judgment before they are shown. It is a report over
reads you already made, not a new field on the read and not a gate in front of
one — the /context response shape is unchanged.
2026-08-26 — Organization schemas resolve, and revert stops overclaiming
Section titled “2026-08-26 — Organization schemas resolve, and revert stops overclaiming”A bare schema slug, and an inline schema’s extends, now resolve against
this organization’s own registry. Before tonight, PUT /v1/brains/{handle}
checked only the two built-ins, nicia-base and nicia-core. Both fields now
check built-in first — a tenant registration can never shadow a shipped pack —
and fall back to whatever this organization has registered with
POST /v1/schemas. On an inline extends, the caller’s kinds merge over the
named base and the base wins any name collision. A slug that resolves to
neither is a 400 naming the field and listing the built-ins. On a bare
schema that refusal is scoped to a create or a revive, so a re-PUT that
echoes back a Brain’s own per-Brain inline slug stays legal; on
schema.extends it applies to every PUT, because an inline schema is
re-merged over its base on each one and an unresolvable base yields no
vocabulary at all. No domain pack shipped tonight — the maintained CRM,
support, and research packs the reference describes
still do not exist; what changed is that your own registrations resolve.
GET /v1/brains/{handle}/schema-lineage answers which organization schema a
Brain forked from, and whether that baseline has moved. The response is
{ "lineage": { "status": … } }, one of unrecorded (nothing was ever
stamped — not the same as “no drift”), independent, in-sync, base-moved
(carrying currentBaseHash and an optional drift of missing kinds, edges,
and fields), or base-unresolvable. It takes an organization key only, and
answers 401 for a Brain key: this reports a fact about the organization’s
schema registry, outside what a single Brain’s credential is scoped to.
GET /v1/schemas/{slug}/brains and POST /v1/schemas/{slug}/rollout are in
the reference now. Both sit on the organization /v1/schemas surface. The
fleet read answers each Brain’s alignment — tracking, diverged, or
claimed — against the schema, plus behind and the missing/local kind,
field, and edge names. The rollout applies the schema to every Brain on it (or
to an explicit knowledgeGraphIds list): a plain Brain has the contract
evolved in place, a diverged Brain gets a ChangeSet staged for review instead
of a silent write, and one Brain’s failure never aborts the sweep — the
response counts applied, offered, skipped, and failed outcomes rather
than stopping at the first one. Two skips are worth expecting: a Brain already
carrying the contract is up_to_date, and a Brain nobody has written to yet
is claimed — its first write materializes from the current organization
contract anyway, so rolling it forward now would only force a materialization
early. Both operations read at most 200 Brains, and an unrestricted read or
sweep that hits that ceiling answers truncated: true rather than presenting
a partial fleet as the whole one.
Every Brain response now includes generation beside id. It is the
fence every generation-scoped write — evidence submission, mutation
submission, policy activation — makes you echo, and it was previously
reachable only through a console-plane read that no page of this site
names. Treat it as opaque and required: a revive or
reprovision mints a fresh one, so re-read it rather than caching it forever.
A mutation whose provenance.origin is anything but emitted now says
why. Canonical mutation ingress has always accepted emitted only — that
did not change, and the published schema still advertises emitted | derived
deliberately, because derived is a legitimate origin on internal projection
paths. What changed is the 400: it now names emitted as the accepted
value, quotes back what you sent, points at the offending mutation’s index in
the batch, and says why a computed value still ships as emitted — a
derived mutation is one reconstructed from a before/after diff, never stated
by its own writer.
The reference names a host. https://api.nicia.ai is where this API
answers, and the OpenAPI document now both points its one servers entry at
it and names it in the document’s own prose — a reader looking for where to
send their key does not read a servers array, and a generated client is
pointed at a real address instead of needing one supplied by hand. The docs’
examples already used this host; what’s new is the document saying so, and a
check that keeps it honest: the published URL is pinned against the
custom-domain route the API Worker actually serves, so the two cannot drift
apart. The workers.dev hostname the same Worker also answers on is
deliberately not published — it is a detail of the account it is deployed in,
and an address that can move out from under you.
The documented error kinds are eight, not seven. unavailable (503) joins
the table — it was already on the wire, answered by a
receipt-store outage, and is now the second transient kind alongside
rate_limited.
Reverting a ChangeSet no longer reports an undo it didn’t do. It used to mark a ChangeSet reverted while restoring nothing
for page content or corpus membership. It now plans the revert before
touching the set’s status: if every item can be expressed as a concrete,
symmetric undo, it restores them and marks the set reverted; if any item
cannot — corpus membership changes, schema evolution, and automation
configuration have no supported automatic undo, and some page edits are
missing the facts a restore needs — the whole revert is refused with a 409
and the set stays adopted, with the response
pointing at rolling the affected pages back individually from their version
history. If you call revert today, this is the change to check for:
anything that treated a 2xx from this endpoint as “undone” needs to handle
the 409 instead.
2026-08-20 — A record read answers its links and its undeclared
Section titled “2026-08-20 — A record read answers its links and its undeclared”links and undeclared are populated on every record read.
GET /records/{id} and POST /query have published both
fields all along and emitted neither. A declared relationship came back inside
fields as the raw string "[[acme]]", so a caller who wrote
"links": {"works_at": ["acme"]} had no way to read back what they had written,
and a generated client’s record.links was undefined forever. Both now carry
what the write path already knew: links maps each declared edge to the
addresses it points at, and undeclared names the fields the schema does not
declare for that kind.
Which means fields no longer carries relationship values. If you were
parsing fields.works_at out of "[[acme]]", read links.works_at instead —
an array, with each [[…]] target unwrapped to the address inside it. A target
stored in some other form comes back as written rather than being dropped, which
is the same leniency the write path applies: a link you can see is a link you
can correct, and a silently shortened list would let a PUT clear an edge it
never showed you. Nothing else moves: undeclared
values still come back in fields exactly as stored, and undeclared only
names them. Relationship-ness comes from the schema, never from what a value
looks like, so a field whose value happens to read [[support]] stays a string
unless an edge is declared over it.
2026-08-20 — A create can say it is a create
Section titled “2026-08-20 — A create can say it is a create”PUT /v1/brains/{handle} takes ifExists. The default is "update", and
that is the operation you already have: the Brain at that handle is resolved
first and your name, purpose, and mode are applied to it. Send
"ifExists": "conflict" when the call is a create rather than a reconcile —
a provisioning script standing a Brain up, or anything deriving a handle from a
name a human typed. A taken handle then answers 409 naming the fields that
differ, and nothing is written. A request that states nothing different from
what is stored still answers 200 with that Brain, so retrying a create whose
response you lost is still safe.
This matters most where the handle is derived rather than chosen. Two people
who both call a Brain “Support” derive support, and under plain
create-or-update the second one silently renames the first one’s Brain and
rewrites its purpose.
GET /v1/brains/{handle}/records/{id} answers 409 for an ambiguous
address. More than one record can end up at one address — a retype that
leaves the old typed record behind is the usual way — and the addressed writes
already refuse that case. The read used to answer one of them, chosen by an
internal sort order with no relation to which is current, behind a receipt
asserting that version was what the Brain held there. It now refuses too.
POST /query on the same predicate still returns both, which
is how you see what happened and retire the stale one.
2026-08-20 — One create, one document
Section titled “2026-08-20 — One create, one document”PUT /v1/brains/{handle} is the only way to create a Brain. A POST to
the Brain collection used to answer too — a second create, on the same path the
GET on this page lists, returning a differently-shaped object. One path
taught two nouns split by method, and the published reference resolved that by
hiding the second one, which meant the document you read and the document
@nicia/brain-client is generated from were not the same document. They are
now. That create moved to the console plane, where the platform
record it answers already lives, and nothing is withheld from the reference any
more.
If you called the old create: send the same identity to
PUT /v1/brains/{handle} — the key you chose becomes the handle in the path,
and name, purpose, and subject are the same fields in the body. Two
differences are worth knowing before you switch. This one is
create-or-update: it resolves the Brain first, so re-sending a changed
name renames it instead of answering the 409 the old create answered for a
changed specification — send "ifExists": "conflict" to get that refusal back.
And it defaults a new Brain to mode: "open", which admits writes; state
"mode": "reviewed" if you want them staged.
Three things the old create accepted have no field here, deliberately: a Brain
born over a collection of documents you already hold, a governance policy
object, and a full declared schema at a slug you name. Those are console
affordances and they stay on the console’s own create. What this one takes
instead is schema — a pack slug, or an inline {kinds, links} vocabulary
that is fully typed in the reference.
externalRef is removed from the wire. It was a deprecated alias for the
Brain key, on the old create body and on the Brain list filter. Both are
gone: filter with ?key= and address a Brain by its handle. If you stored an
externalRef, it is your key and it is a legal handle unchanged — the same
value under a different field name.
One of the three is outbound, so check your receiver. The
knowledge.corrected webhook payload carried data.externalRef and now carries
data.key — same value, same meaning, and it is still absent when the Brain was
provisioned without a key. A receiver that maps a delivery to its own object
through data.externalRef reads undefined on every delivery until it is
changed; nothing about the requests you send will tell you, which is why it is
called out separately here.
2026-08-20 — GET /records/{id} is live, and the preview label is retired
Section titled “2026-08-20 — GET /records/{id} is live, and the preview label is retired”The addressed record read answers.
GET /v1/brains/{handle}/records/{id} returns one record at its own address.
The last two entries on this page named it as the one registered operation
without a route; this closes that loop. Every operation the reference
publishes is now one the server answers.
It is not a new way to read. It is POST /query with the
address as its only predicate — the same canonical reader, the same projection,
the same receipt discipline — so the two can never disagree about one record.
Which means it answers what /query answers, minus the page: record instead
of records, no nextCursor, and the same staged, warnings[], and
receipt beside it. Keep the receipt and
GET /receipts/{id} will tell you the exact version you
were shown.
An address the Brain holds nothing at is a 404. The address is the
identity here, and you asked for one record, so an empty envelope would leave
“no such record” indistinguishable from “a record with no values”. That read is
still receipted: the consumption log records that your credential asked for that
address, because an audit of who went looking must not have holes where the
answer was “nothing”.
If you added receipt handling for /context, /query, or /search, this
needs none of its own — it is the same field.
“Preview” is retired in favour of “Beta”. The site banner, the
reference banner, and the published OpenAPI document all carried
a warning naming the operations specified ahead of the server, plus an
x-nicia-preview: true marker on each so a generator could refuse to emit a
method for them. There are none left, so the whole mechanism is gone rather than
standing empty — including the extension field, which no operation carries any
more. What remains is the beta policy it always sat beside, unchanged: this
surface may change before GA, and every breaking change is dated here.
2026-08-20 — A Brain read carries its id again, and PUT accepts a subject
Section titled “2026-08-20 — A Brain read carries its id again, and PUT accepts a subject”Every Brain response now includes an id. The handle is still the address:
every operation on this surface takes it, and none asks for an id back. But the
older id-addressed paths — /v1/brains/{brainId}/connections/...,
/v1/knowledge-graphs/{id}, and the capture, entities, schema-pack, and
changeset paths hanging off them — are still the only address for capabilities
this surface has no operation for yet, and a caller who provisioned through
PUT /v1/brains/{handle} had no way to reach any of them. So the id comes back
in data.brain, on the create, the get, the update, and every row of the
listing. Store it only if you call one of those older paths; a client that
ignores the field is a correct client. It goes away when those paths stop taking
ids, and this page will say so on the day it does.
PUT /v1/brains/{handle} accepts a subject. A subject is what the Brain is
about — the thing it represents, as your system already names it:
{ "kind": "customer", "externalId": "acme", "label": "Acme" }. It is optional,
because nobody should have to invent one: omit it and the server derives a
subject from the handle and the name, which is exactly what it did before. It is
recorded when the Brain is created. A Brain’s identity is generation-bound, so a
subject sent to a handle that already exists does not move it — which is the
same reason this endpoint stays safe to call on every request.
2026-08-20 — The console mints organization keys; every example leads with authorization: Bearer
Section titled “2026-08-20 — The console mints organization keys; every example leads with authorization: Bearer”Settings → API keys has an “Organization keys” section. Minting an nsk_
management key used to require calling POST /v1/keys yourself — the console
only ever created personal app_ keys. It now mints, lists, and revokes
organization keys directly, over the same /v1/keys surface these pages
document; revoking disables the row rather than deleting it, so it stays
listed for the audit trail, same as it does over HTTP.
Every management key minted this way is bound, so it never needs
x-organization-id. That was already true of an nsk_ key minted over
HTTP; it is now also true of one minted from the console’s Organization keys
section, because both mint through the same route. Every management-key
example on this site was rewritten to match: the call now sends
authorization: Bearer $NICIA_KEY alone. x-api-key is still accepted for
management keys, and one minted before /v1/keys existed carries no
organization binding and still needs the header — see
Authentication.
This does not touch personal app_ keys, minted from the same page’s “Your
keys” section: they are a different plane, are not bound to an organization
today, and still need x-organization-id on every write. Cutting unbound
app_ key acceptance is a later wave, gated on this mint flow baking in.
2026-08-20 — /context and /query now see every accepted document /search ranks
Section titled “2026-08-20 — /context and /query now see every accepted document /search ranks”A text/plain or application/json document is no longer invisible to
/context and /query. The read model that surfaces an unclassified page
(one no typed kind has claimed yet) filtered on mimeType === "text/markdown"
— narrower than what gets indexed for /search, which chunks and embeds
anything text/*, application/json, or application/yaml, the same set the
url and Google Drive sources accept as evidence. A text/plain or
application/json document you submitted could rank at the top of /search
while /context and /query answered as if the Brain had never seen it, with
warnings: [] — which the read contract defines as “this answer is complete.”
Both surfaces now agree: any media type /search ranks, /context and
/query show. This changes /context output for any existing Brain holding
text/plain or application/json evidence: those documents now appear where
they previously did not. A genuinely binary type (images, PDFs) is unaffected
— it was never indexed by /search either, and stays out of both.
Only markdown gets its leading --- block read as frontmatter. Splitting a
document into declared fields and prose is a markdown convention, and it is now
applied to text/markdown alone. A text/plain note or an application/yaml
payload that opens with --- means those bytes literally: the whole payload is
the prose, which is the same text /search returns for it. Until now the
splitter ran on every text type, so a plain-text file’s first block was
promoted into that record’s label and fields while /search still showed
the fence — two different renderings of one document. application/json
projects as prose the same way: the JSON text you sent is the text you get
back.
GET /documents says what it lists. It answers what you SUBMITTED, and it
always has. Content that reached a Brain another way, such as a synced folder or
URL source, has no submission to list and no document id to address, but it is
read by /context, /query, and /search — and the widening above means more
of it is. No behavior changed; the reference now states the boundary instead of
leaving you to infer that the listing is the whole Brain.
2026-08-19 — Management keys are in the reference, and the preview banners tell the truth
Section titled “2026-08-19 — Management keys are in the reference, and the preview banners tell the truth”POST /v1/keys, GET /v1/keys, and DELETE /v1/keys/{id} are documented.
The mint, list, and revoke operations for the nsk_ management key have been
serving traffic, but they were missing from the API reference —
so every other operation’s description told you to send a management key while
nothing showed where one comes from. They are in the document now, with the one
thing an integrator most needs to know up front: this surface takes a signed-in
browser session, and refuses an x-api-key credential with 403 forbidden,
including a valid one. A key that can mint its own successor would outlive its
own revocation. See Authentication.
A key minted this way carries its organization in the credential, so
x-organization-id is not needed on any call made with it. A key created in the
web app does not, and still names its organization by header.
The preview banners now say what is actually unserved. The site banner
claimed “endpoints may not respond yet” and the reference’s banner said the v2
operations “mostly return 404” — while 25 of the 26 registered operations had
live routes. Both banners now derive their claim from the same list the
document’s own summary and per-operation notices use, and name the operation:
GET /v1/brains/{handle}/records/{id}, the one still specified ahead of its
server. When it lands, the warning disappears from every carrier at once.
2026-08-19 — Govern is live: /changes, approve, reject, and both receipt reads
Section titled “2026-08-19 — Govern is live: /changes, approve, reject, and both receipt reads”The review queue and the receipt reads answer for real. GET /records/{id} is
the one operation left across all six groups without a route — it still
answers 404, and its own description says so.
GET /changes pages one status at a time. staged/rejected and
applied are genuinely different feeds — a keyset cursor over staged and
rejected proposals, and a separate keyset cursor over the applied
change-event log — with no shared order between them, so a single cursor that
silently mixed the two could hand back a page out of order or missing rows.
Every nextCursor this endpoint returns names the status it continues, so
you never have to restate it; omitting both status and cursor defaults to
staged, the review queue. An applied row’s id is the same id you already
hold from the write that produced it — the batch id an immediate admit’s own
response returned, or the change id a settled proposal was approved under —
so one write is addressable by one id across every page it ever appears on.
records[] on a change reports before, after, and cited evidence.
before is read from the mutation’s own frozen base version
(baseArtifactVersionId) — the exact content the change was written
against — never from live accepted state, so a reviewer sees the diff the
change actually proposed even if the record has moved since it was staged.
It is absent on a create_entity item, which has nothing to hold a before.
applied changes carry no records[] at all — they are a light audit trail,
not a review surface.
approve accepts a whole change, named records, named fields per record,
or corrected values. Naming records or fields is a decision about
everything else in the change too: anything you did not name is explicitly
declined in the same call, never silently admitted alongside what you did
name. A partial approve settles the whole change at once, so declining is
final for it — a later approve naming what was declined 409s, and the
producer has to write again for it to come back up for review. edits
re-validates each corrected field through the same canonical mutation path a
normal write takes, and it is attributed to the reviewer, not the original
writer.
Receipts join to an integer version, addressed by the item’s own
document. GET /receipts/{id} and GET /receipts/{id}/now both publish
the exact numbered version a citation was cut from, not an internal version
id, and both name the record’s own backing document — its evidence locator
when one was cited, its own address otherwise — rather than an opaque
internal id a citation was never supposed to expose. receiptNow reports a
Brain under legal hold as held, alongside superseded and deleted — the
three ways an answer can have changed since it was given.
The console’s own change report moved to a console-plane
route. It answers a different shape —
the aggregated byChangeType/byDay rollup behind the workspace’s history
tab, over a date range rather than a cursor — and
/v1/brains/{handle}/changes is the public, cursor-paged queue.
Walking ?status=applied sees each change exactly once. One write mints one
event per record it admitted, and a three-record batch whose events fell either
side of a page boundary was listed on both pages — so anything counting
“changes applied”, or rendering the queue, double-counted it. Each page’s
cursor now carries the change it ended inside, and the next page suppresses it.
A partial approve answers with what it admitted. Approving named fields on
an UPDATE used to answer status: "applied" with an after containing the
values it had just declined — the reviewer’s own permanent record of a final
decision, wrong about both what landed and how much of it did. after on a
settled change now carries only admitted values (absent for a record that was
declined whole), and any partial admit reports partially_accepted, the same
as one scoped by records. On a change nothing has settled — staged,
rejected — after is still the full proposal, which is what a reviewer is
being asked about.
reason on a rejected change is the reviewer’s reason. It reported the
policy reason that STAGED the change instead — “this Brain’s write policy
requires review”, which is true of every reviewed write there has ever been —
so the rejected queue answered a question nobody asked. It now reads the
decision while a change is settled, and the staging reason only while it is
still waiting.
GET /changes caps limit at 250, and says so. It published 500 and served
a silently smaller page, with nothing in the response to say it had been
reduced. 250 is what the review reader slices to; 251 is now a 400 naming the
bound.
A batch may name a record once. Two entries for one id have no decidable
order, and the write refused them — but as 200 with {"status": "rejected"},
the shape these pages define as “the Brain declined it; the request was fine,
the content was not”. It is a 400 naming the id, like every other structural
refusal on /batch. Merge the entries, or send two batches.
Re-sending a document is visible to the receipts that cited it. Replacing a
document under the same id promoted a new version that GET /documents/{id}
reported — while /context kept serving the old text and
GET /receipts/{id}/now answered unchanged, on precisely the correction that
endpoint exists to surface. A replacement now moves the Brain’s own view of the
document, and every receipt citing the old version reports superseded with the
version that replaced it.
A receipt is readable only by the key that committed it. That was always the
behaviour; the reference and the receipts guide taught the opposite, showing the
management key fetching a receipt a Brain key had produced, which 404s. The
guide now shows the Brain key, both receipt operations state the rule, and the
404 itself says why rather than reading as a wrong id. A receipt records which
credential a read was served under — that binding is what makes it evidence — so
neither the management key nor a second Brain key on the same Brain can read
another key’s receipt.
/query refuses rather than inventing a version. When a record’s pinned
version did not resolve it answered version: 1, a number nothing measured,
while the receipt behind that same read failed closed on the identical miss.
Both now fail closed.
Five fields nothing ever set are gone. receipt.prompt and
receipt.items[].quote (a receipt names a request by hash and pins versions —
it holds neither the prompt nor the passage text), changed[].current on
/receipts/{id}/now (source + currentVersion is the address of the current
text, through GET /documents/{id}), and intent and fields on a change as
GET /changes and the review operations report it — both are echoes of a write
request, and stay on the change a WRITE answers with. Every one was the
undetectable kind of absence: optional, so a missing value read as “this one has
none” rather than “nothing ever sets this”, and two of them appeared in shipped
examples. brain.documentCount, brain.recordCount, and document.chunks stay
declared — they are additive and the shape is right — but now say in the
reference that no read sends them yet.
2026-08-19 — a read says which mode it ranked in, and stops answering with ids nothing accepts
Section titled “2026-08-19 — a read says which mode it ranked in, and stops answering with ids nothing accepts”consumerId is bounded on every read. /context published the receipt’s
own bound (1–512 characters); /query and /search published none, so a value
the contract accepted failed underneath — /query answered 500 with the
substrate’s raw validation error in the message, and /search answered 503 unavailable, which is the retryable status this surface reserves for a
receipt-store outage. Anything building ?consumerId=${userId ?? ""} retried a
permanent input error forever. All three now refuse it as a 400 naming the
field.
/search says which mode it actually ranked in. hybrid needs an embedding
provider, and a deployment without one ranks by keywords instead of failing a
read it can still serve — silently, with warnings: [], which these pages
define as “this answer is complete”. The response now carries mode, and it is
the mode the ranking was performed in, not the one that was asked for.
staged counts changes, and says so. It always counted proposals awaiting
review — one batch of twenty records is one change — while the reference called
it a count of records, so a caller who wrote twenty was told 1. The word now
matches the number. Two adjacent claims are corrected with it: staged is not
always 0 on an open Brain (anything that proposes rather than writes puts
work in the queue whatever the mode), and it can now be null, meaning the
count could not be taken on that read. It used to report a failed count as 0,
which is this surface’s own way of saying “the Brain knows nothing relevant” —
the exact ambiguity staged exists to remove. A processing warning whose
count fails is likewise omitted rather than reported as zero.
sources names document ids, or nothing. For a record written through
PATCH /records/{id} it answered the record’s own internal artifact id — a
21-character token GET /documents/{id} refuses — because such a record cites
its own stored page, which never arrived as a document. A source with no public
address is now omitted.
fields no longer restates kind. A record read carried a type field
holding its own kind: the frontmatter key the capture boundary writes, echoed
back as if it were one of your values, and already published as kind.
A kind you declare called document is your kind. document is also the
name the reader stamps on a page nothing has typed, and it used to decide
between the two by matching that name (and note, doc, page) — so a Brain
declaring document with fields of its own had every typed record served as a
markdown blob, with the record branch of the published item union never
running. The split is now made against your declared schema: a piece is a
document when it is filed under the kind your Brain uses for things nobody has
typed (your own fieldless free-form kind, or document when you declare none),
and a record otherwise.
reviewUrl is removed from /context and from change. It was published
on both and emitted by neither, and an optional field nothing sets reads as “no
review page for this one” rather than as “this never works”. Nothing in the API
can build the link honestly today — there is no configured console address for
it to be built from. It returns when there is.
What a document contributes to text, stated correctly. These pages said a
document contributes “its prose, and nothing else”. A document that carries
frontmatter contributes a name line and every frontmatter key as a
key: value line as well — an authored field is never dropped, in a prompt any
more than in storage. Worth knowing before you paste: a document carrying an
internal_owner: line puts that line in front of your model.
citations[].quote is still the block, character for character.
The slug in these examples is the shape the API produces. Citations and
search hits showed hubspot-deal-412 beside their source; a document written
through this API is addressed internally by a content hash
(brain-evidence-<64 hex characters>), never by a slugified title. source —
your own id — is the address to act on, and slug is for lining a citation up
against a search hit.
2026-08-19 — /context hands back prose, and a document is never invisible
Section titled “2026-08-19 — /context hands back prose, and a document is never invisible”A document your schema has no kind for is in the answer. A Brain that
declares kinds without a generic slot for plain prose had no name for a page
nobody typed, and the reader answered that by dropping the page: GET /documents
reported it indexed, /search ranked it first, and /context and /query
returned nothing at all, with warnings: [] — which these pages define as “this
answer is complete”. A caller on that vocabulary could not tell a Brain holding
their whole corpus from an empty one. Such a page is now in every read, under the
kind document. Naming a page is a labelling decision; withholding it was a
correctness one.
text is prose, not our projection of it. /context returned the
assembler’s internal JSON line per source — one JSON.stringify carrying the
Brain’s opaque ids and storage paths, with the sentence that grounds the answer
escaped inside a string — under a field documented as “paste this into your
prompt”. text is now one readable block per source, blank-line separated:
prose for a document, field: value lines for a record. Each block is the
matching citations[].quote character for character, so what you show a user is
exactly what the model was shown. Structure has not moved: ask for
format: "items" and you get the typed pieces, unchanged.
The processing window stopped growing with the Brain. The bounded window
/search reads to count unindexed documents bounded its RESULT and not its
work: it deduplicated every completed submission for the Brain and sorted the
whole set before taking the newest hundred, which measured linear (1.4 ms at 5
documents, 3.3 ms at 1,000, extrapolating to ~200 ms on a 100k-document Brain,
on the fail-closed path a /search cannot answer without). It is now an index
range scan of the newest writes, the same cost whatever the Brain holds.
2026-08-19 — A read refuses what it cannot apply, and says what it is handing back
Section titled “2026-08-19 — A read refuses what it cannot apply, and says what it is handing back”An undeclared query parameter is a 400 naming it. /context and /query
already refused a retired filter, about, or asOf in their bodies;
GET /search, the one read whose input rides the query string, accepted and
dropped every parameter it did not declare. So
?q=renewal&filter={"source":"zendesk"} was answered 200 with a whole-Brain
ranking — the exact failure the refusal exists to prevent, made worse by being
invisible. Every endpoint now refuses an unknown query parameter and names it
alongside the ones it takes, so a client generated against an older preview
shape fails at the parameter it should stop sending.
/context items say record when the piece is a record. format: "items"
labelled every item document and handed a typed record’s values back as YAML
inside a markdown string, which meant the record branch of the union we
published never ran for any input. Items now carry the kind the piece actually
is: a document with its text, or a record with its fields as numbers
and booleans, addressed by the id PATCH /records/{id} takes. The rendered
text form carries the same typing, so an assembled prompt now names each
source’s kind instead of burying it in frontmatter.
Two shapes nothing produced are gone. The context item union published a
statement variant no read emitted at that release — statement operations had
not landed yet — and a score on the document variant, over a reader that selects
deterministically and ranks nothing. Both return when a reader produces them.
processing on /search counts documents that are not in the passage index
yet. It was taken from active extraction jobs, so a document written with
extract: false — or one in the window after extraction settles and before
indexing finishes — was missing from a ranking while warnings[] was empty,
which the contract defines as “this answer is complete”. /search now also
counts accepted documents whose text is not chunked, over a bounded window of
the Brain’s most recent writes. /context and /query still count extraction
alone, because a page is in an accepted-state answer the moment it is wired,
and warning that a whole answer was short is its own kind of lie.
2026-08-19 — Reads are live: /context, /query, and /search
Section titled “2026-08-19 — Reads are live: /context, /query, and /search”The three reads answer for real, all of them receipted and all of them
fail-closed: if the receipt cannot be committed the response does not leave,
and you get a retryable 503 instead of an answer nothing recorded.
/context is honest about its budget. maxTokens used to accept anything
up to 200,000 over an assembler that spent at most 8,192 and silently clamped
to it — so a request for 50,000 was answered 200, rendered 8,192, and
reported truncated: true, which reads as “your budget ran out” when the
budget was never applied. The published ceiling is now the one the assembler
actually renders to, and asking for more is a 400 naming the limit. The
default is unchanged at 2,048. Both numbers rise together when the assembler
can spend more.
/context refuses about. It promised to walk the relationships from a
named subject; the assembler has no subject coordinate and no link traversal,
so it would have returned a whole-Brain answer to a question that named one
thing. Like filter and asOf before it, it is a 400 naming the field
rather than a parameter that narrows nothing.
/search is fulltext or hybrid, and is not cursor-paged. The preview
published documents and records modes over an index that has no such split
(a record IS a document there), and a cursor/nextCursor pair over a ranked
top-N read that has no stable cursor to hand out — walking it would have served
the same first page forever. Paging is now one limit, capped at 20, which is
what the reader returns. Each result carries rank (1 is the best match)
rather than a score: ranking fuses a keyword leg and a semantic leg into an
ORDER, and the 0.87 the preview showed was a number nothing computes.
Every read carries staged and warnings[]. An empty answer now says why
it is empty — staged counts changes waiting for review, and warnings[]
reports processing (accepted documents still being read), truncated, and
staged_pending. access_scoped is published and not yet emitted by anything:
no read on this surface is filtered by credential scope today.
Citations and search results name your document id. source is the id you
wrote the document under, so a citation round-trips into
GET /documents/{id}; a page authored in the console has no such id and
carries slug alone. Citations also carry the recordedAt of the source and
the exact prose the answer rests on.
The console’s own passage search moved to a console-plane
route. It answers a different shape —
citation tokens and per-brain attribution, off a session — and /v1/brains/{handle}/search
is the public, receipted read.
2026-08-19 — Documents and record writes are live
Section titled “2026-08-19 — Documents and record writes are live”Everything the quickstart’s third call does now answers for real. Write a
document or a batch of them, read one back, list them, and ask what the Brain
read out of one; merge into a record, replace one, delete one, or send up to
twenty as a single atomic batch. Whether a write lands or waits for a human is
the Brain’s mode, decided by its admission policy — the response’s
change.status says which happened, and no request can ask for one.
A record id is a lowercase kebab identifier. The preview accepted any
non-whitespace token and offered crm:account:4471 as an example, which no
write could ever have landed on: a record is stored as a document whose address
is exactly that string, and the store refuses punctuation. Most ids you already
hold pass unchanged (acme, user-4471, a lowercase UUID); one that does not
is lowercased and joined with - once, at your edge. The rule now says what the
store enforces, so an id that parses is an id that writes.
A record write answers what it DID, not what the record holds. The response
carries the address, the kind, the version to send back as If-Match, which of
your fields moved, and the values you submitted — and no others. It used to
answer the record’s whole accepted state: every field value, including
frontmatter no schema declares and no request mentioned. A key minted with
write and not read could therefore page a Brain out one address at a time,
with no receipt behind it, by sending an empty body to each
one. Values now leave through /context, /query, and the record read, which
are reads and are receipted. For the same reason change.records[] reports
set and cleared — your submission, decided — in place of the before and
after snapshots it used to carry.
A write has to state something. A body with no kind, label, fields, or
links is a 400, and so is a batch entry carrying only an id. An empty
write had exactly two uses: reading a record back through a write verb, and
standing an empty record up at an id you typo’d.
A write that changes nothing writes nothing. Re-sending a record you already
sent answers change.fields: [], mints no version, and puts nothing in the
review queue — which is the normal case for a nightly sync and worth counting.
Such a change now carries no id: nothing was recorded, so there is nothing
for GET /changes or the review operations to address, and the id it used to
answer resolved to nothing anywhere.
PUT clears the fields your schema declares, and spares the ones it does
not. Undeclared frontmatter — a source_url from a markdown import, an
imported_by a colleague wrote — is stored data no response on this surface
shows you, so a PUT that omits it leaves it alone rather than deleting what it
never let you see. Send it as null to clear it deliberately. Links are
unaffected: links accepts only names your schema declares, so a link you omit
from a PUT is still removed.
No idempotency key is derived from your request any more. Two writes you
meant separately are byte-identical to a retry — delete a record and resync it
and the resync is the create again, to the byte — so a hashed key answered the
FIRST request’s outcome to the second, reporting applied for a write it had
dropped. Idempotency-Key is now accepted on PATCH, PUT, and DELETE as
well as POST /batch, and absent one every request is its own. Updates stay
naturally idempotent through the address; a create is the case worth a key.
Re-creating a record you deleted is refused today, and says so. Deletion is
soft — the record leaves the Brain and its history stays — and re-using the
address is not yet supported underneath, so the write answers 409 conflict.
That is a change you can see, and it is the honest half of the one above: the
same request used to answer 200 applied while writing nothing at all, because
the derived key replayed the outcome of the create you had already deleted. A
409 you can act on beats a 200 you cannot. Until the address can be re-used,
write the record under a new id — and if you were relying on the old behaviour,
you were relying on a write that never happened.
record carries no version when a write stages onto an address that holds
nothing. There is no accepted record there yet, so there is no version to
name. The change is the answer in that case, and it says staged.
A link needs its target. links writes a typed edge, and an edge names both
endpoints — so a target the Brain does not hold yet is a 400 rather than an
edge that silently never materializes. Send both records in one POST /batch,
where records may reference each other, or write the target first. The preview
said the target could arrive later; it could not.
A document write answers queued. The bytes are stored and the document is
addressable by your id immediately; a read reports processing until body
search can reach it, then indexed. chunks is not reported by any read today
— a chunk count is a traversal rather than a column, and status already
answers the question it was standing in for.
Batch ceilings, stated as they actually work. A record the Brain does not hold yet costs one canonical mutation; updating one costs a mutation per field that actually changes and per link added or removed. Twenty records is the published cap, and a batch that expands past the substrate’s hundred-mutation or 2 MiB ceiling is rejected whole, naming the ceiling it crossed.
Metadata keys beginning nicia: are reserved. A document’s title and
occurredAt are stored beside your own metadata under that prefix and lifted
back out on every read, so metadata round-trips as exactly what you sent.
2026-08-19 — Brains and keys are live
Section titled “2026-08-19 — Brains and keys are live”The nine operations these pages open with now answer for real: create or update a Brain on your own handle, read it, rename it or switch review on, delete it, list your organization’s Brains, and mint, list, rotate, and revoke the keys for one. They are in the published API reference because they are running, not because they are planned.
A management key goes in the same header as a Brain key. Every example here
sends authorization: Bearer $NICIA_KEY, and that is now literally true for
both kinds — the prefix (nsk_ or nbk_) decides how far the key reaches, not
which header you put it in.
A new Brain starts in open mode. That was the documented default before
anything served it — the platform’s own create path provisioned a Brain to
stage its writes — and it is now what you actually get. If you want writes to
wait for a human, say "mode": "reviewed" on the PUT or switch it later with
a PATCH. The default vocabulary is unchanged and stays
nicia-base: a PUT naming no
schema gets it, and nicia-core is selected by name.
A deleted handle stays reserved. PUT on a handle whose Brain was deleted
is a 409, not a new empty Brain — its receipts and audit history still
resolve, and a re-running script should find out rather than quietly start over.
Send "revive": true to mean it.
Removed before anyone could depend on them. asOf on /context and
/query: it described a read as of a moment against a reader with no temporal
coordinate, so you would have been handed today’s answer wearing yesterday’s
date — with a receipt to make it look verified. The temporal question has a real
answer: GET /receipts/{id} pins the exact version of
every source a read was shown, and /receipts/{id}/now says what has changed
since. overlapSeconds on rotate: the overlap is a fixed 60-second window, and
a knob that silently resolved to 60 would close a redeploy nine minutes early
in silence. prefix on the Brain listing stays Planned rather
than shipping as a filter that returns short pages and empty ones with a cursor.
A key can be expired. key.status adds it, because expiresAt always
implied it and reporting a lapsed key as revoked sends you looking for a
revocation nobody performed. Key listings are also paged now — one key per end
user is a documented pattern, and a listing that answers its first page as if it
were the whole set is the one thing a credential inventory must not do.
2026-08-18 — the preview surface matches what will ship
Section titled “2026-08-18 — the preview surface matches what will ship”Several things the preview described could not be built as written, one bound was wrong, and one capability granted more than these pages said it did. Rather than ship operations that answer something other than what the reference says, they are corrected in the contract now.
A key no longer chooses how its writes are admitted. Minting a key used to
take a writes setting of direct or propose, and a key record used to
report one. Both are gone. Whether a write lands or waits is the Brain’s mode,
and the Brain’s admission policy decides it for every write, whoever sent it —
an outcome a caller can select is an outcome an agent can select. If you need a
pipeline writing straight in while agents are reviewed, give it its own Brain.
A mint still carrying writes is a 400 naming the field, for the same reason
a retired query predicate is: a 201 that dropped the setting would promise
immediate writes and deliver staged ones, and you would find out from a Review
queue filling up rather than from the response. See Review.
Documents cannot be deleted. The preview described a delete that also retired every record extracted from the document and marked every receipt that cited it. That is correction work over the accepted state, not a delete, and nothing enforced it. Write the document again under the same id to supersede it; the old version stays resolvable, so past receipts still explain themselves. See Documents.
Queries filter exactly, and say so when they cannot. where now carries
only the two predicates the reader evaluates: a scalar for equality, and
{ "has": … } for one element of a string list, AND-ed, up to eight. Arrays as
or, { "after": …, "before": … } ranges, orderBy, and the linked
relationship filter are removed. A request still carrying one is a 400 naming
the field — never a correctly-shaped page that quietly answered a different
question. See Search and queries.
/context and /search take no filter. The preview described a read-time filter on both — a scalar matching exactly, an array as an or, { "after": …, "before": … } bounding a date. Neither endpoint has a filter of any kind behind it: /context renders every source-backed document in the Brain inside your token budget, and /search ranks a passage index that knows nothing about your document metadata. Two of the three operators were never buildable as written — no declared field type is a date, so an occurredAt is a string whose only operator is equality, and nothing anywhere combines predicates with or. Exact filtering lives on /query, where a predicate is checked against a declared field. A /context request still carrying filter is a 400 naming it; a stripped one would have answered a whole-Brain question confidently and told you nothing. See Context and Search and queries.
Document metadata was never an index. These pages said every metadata key “becomes a filter at read time”. No read has ever indexed it. Metadata still takes any flat JSON object, nothing is declared and nothing is discarded, and every key comes back on the document — but slicing by one means declaring it as a field and querying it, which works retroactively precisely because nothing you already sent was thrown away. See Documents.
A query page carries 100 records, not 500. The reader slices every structured-query page to 100, so a larger limit described a page nothing would ever return. It is now the published bound, and a limit past it is a 400 rather than a page quietly cut down to size. Walk the rest with nextCursor. See Search and queries.
Context is assembled in a stable order, not a relevance ranking. /context renders sources in a deterministic order keyed on each source’s id and stops when the budget is spent — which is what makes two identical calls return the same text and a receipt worth holding. These pages called that order “relevance”, which mattered because it changes what truncation means: truncated tells you the budget ran out, not that what was dropped mattered least. The documented default budget was wrong too. Items no longer carry a score, which nothing ever computed. See Context.
A batch carries 20 records, not 500. A record is not one unit of work: each field and each link becomes its own entry in the canonical change Nicia records, against a ceiling of 100 entries and 2 MiB per batch. A 500-record batch could never have been applied. Twenty is the ceiling for a four-field record; a wider record still crosses it, and a batch that does is rejected whole. See Records.
A change reports two more states. change.status adds
partially_accepted — a reviewer accepted some records and not others — and
superseded, for a staged change whose target moved on before anyone settled
it. Both were already reachable; the wire had nowhere to put them, so they had
to be reported as staged, which meant a queue entry that was not in the queue.
Branch on status with a default case. See Review.
A receipt can report a source under legal hold. Asking what changed since a
receipt can now answer held alongside superseded and deleted. A hold makes
a source temporarily unservable, and the previous wire had to report that as
unchanged — the one answer a hold exists to prevent. See
Receipts.
A handle must survive a path segment. Handles now reject / and
whitespace. Everything else stays permissive: a handle is usually an id you
already hold, and tenant:acme, acme, and a UUID are all fine. A Brain
provisioned before this with a slash or a space in its handle is still reachable
by its opaque id in the same slot.
admin no longer carries export or webhooks — and now really cannot read.
These pages promised that admin grants no access to knowledge while also
listing export under it. Both could not be true: a bulk export is the Brain’s
entire contents, every revision and every source, and a change webhook is the
same contents delivered continuously to a URL the key’s holder picks. admin
now grants exactly one thing on the live surface — PATCH /v1/brains/{handle},
which switches review on and off — so the sentence you can hand a key to a
schema or ops contractor on is the sentence the gate enforces. Reading the
schema is not part of it and never was: GET /schema and GET /schema/candidates
need read, like every other read. Schema evolution, export, and webhook
subscription are off the Brain-key surface until they arrive with a capability
named for what they move; until then they are reached with a credential issued
from the management plane. See Authentication.
2026-08-18 — the surface enters the reference
Section titled “2026-08-18 — the surface enters the reference”Added — the Brain API is published in the API reference. Part
of it is a specification rather than a description: documents, records, reads,
and govern have no route behind them yet. Every such operation carries
x-nicia-preview: true and says so in its own description, in one of two forms.
- Most preview paths have no route at all and answer
404today. GET /v1/brains/{handle}/searchandGET /v1/brains/{handle}/changesshare a path and method with an older, differently-shaped implementation that is live. A request to one of those reaches the older handler instead: it answers in its own shape and ignores the fields this surface defines. Do not read its200as this API working.
Generate nothing against a preview operation expecting a live endpoint. When a route lands, its marker is removed and the change is dated here.
Removed (breaking) — the kind/slug record alias. A record is addressed by
the bare id you chose for it, one path segment, and that is the only form on the
wire. The two-segment typed form was documented but no schema accepted it, so
every request that followed the docs failed. It may return later as an additive
route.
Policy — the beta label. The preview banner on these pages, the banner on the reference, and every preview operation carry one sentence: this surface may change before GA, and breaking changes are dated in this changelog. That is the whole promise. A breaking change without an entry here is a bug — tell us.