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

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.

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.

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.

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.

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.

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.

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 404 today.
  • GET /v1/brains/{handle}/search and GET /v1/brains/{handle}/changes share 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 its 200 as 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.