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

Review

A Brain has one of four admission levels. The level is an owner decision: the Brain’s policy decides each write, never the caller. Human writes land, with one exception: a source snapshot that would change a value a person or another source supports waits in Review below L4, whoever sent it. The ladder governs agent writes. A write made with a management key (nsk_) is a person’s: it acts as the person who minted the key, and is recorded against the key’s id so it stays distinguishable from that person’s console writes.

Level Name Agent writes that land automatically
L1 Human approval only None; every agent write stages for review
L2 Auto-approve trivial No-op and formatting-only edits
L3 Auto-approve new and non-conflicting Writes admitted by the consequence rules (the default)
L4 Auto-approve all Everything, including destructive deletes

The API keeps the historical mode literals for compatibility: reviewed is L1 and open is L3. The new literals are trivial (L2) and unrestricted (L4). Legacy propose write-policy values resolve to L1 and direct values resolve to L3; responses use the level’s canonical literal. mode is a property of the Brain, never of the caller.

One Brain · chain of custodyIllustrative lifecycle
01Sources
Records
Documents
Agent writes
02Review
ProposedHuman or policy decisionAccept · edit · reject
03Context
Accepted stateCited answer[1] source passage
04Receipt
Verifiedrcp_019fWhat the read consumed
Terminal window
curl -X PATCH "$API/v1/brains/tenant:acme" \
-H "authorization: Bearer $NICIA_KEY" \
-H "content-type: application/json" \
-d '{ "mode": "reviewed" }'

Keyed API-created Brains default to L3 (open), so governed routine writes do not require a person on every change. Console-created Brains default to L1 (reviewed), so new knowledge appears in Review while trust is being built. Choose a looser or tighter level as the Brain’s role changes.

Nothing about how you write. The same call returns a different status:

{
"data": {
"records": [{ "id": "acme", "kind": "company" }],
"change": {
"id": "chg_44a1e0",
"status": "staged",
"createdAt": "2026-08-14T10:59:41Z"
}
}
}

Branch on change.status, always:

Status Meaning
applied It landed. Readable now.
staged Waiting for a decision. Not readable yet.
rejected The Brain declined it. The request was fine; the content was not.

A rejection is a 200. The request was well-formed and the Brain made a decision — that is not an HTTP error.

A write only ever returns one of those three. A change you read back from GET /changes can also carry superseded (a selection, below, replaced it with a successor, or its target moved on before it was reviewed). The ?status= filter on /changes still only selects the three admission outcomes above.

Reads tell you the same thing from the other side: every /context response carries staged, so an empty answer distinguishes “nothing known” from “waiting on you.”

Terminal window
curl "$API/v1/brains/tenant:acme/changes?status=staged" \
-H "authorization: Bearer $NICIA_KEY"
{
"data": {
"changes": [
{
"id": "chg_44a1e0",
"status": "staged",
"author": {
"kind": "key",
"id": "key_7f3a1c",
"name": "support-agent"
},
"records": [
{
"id": "bob-smith",
"kind": "person",
"before": { "role": "Head of Platform" },
"after": { "role": "VP Platform" },
"evidence": [
{
"source": "zendesk:ticket:8821",
"quote": "Bob Smith, now VP Platform, confirmed…"
}
]
}
],
"createdAt": "2026-08-14T10:55:12Z"
}
],
"nextCursor": "eyJrIjoi…"
}
}

Every staged change carries the diff and the passage it came from, so a reviewer is deciding about evidence rather than about a model’s assertion. A source snapshot’s change also lists the documents it serves or retires ({ source, id, action }), apart from its records.

Deciding a change is two steps on the lifecycle verbs. Approve binds the exact candidate you reviewed; publish applies it. Both need the review capability. Approve’s body may be omitted, except for a change that carries a standing answer: see below.

Terminal window
curl -X POST "$API/v1/brains/tenant:acme/changes/chg_44a1e0/lifecycle/approve" \
-H "authorization: Bearer $NICIA_KEY"
curl -X POST "$API/v1/brains/tenant:acme/changes/chg_44a1e0/lifecycle/publish" \
-H "authorization: Bearer $NICIA_KEY" \
-H "content-type: application/json" \
-H "idempotency-key: publish-chg_44a1e0" \
-d '{}'

Publishing again under the same Idempotency-Key returns the committed outcome instead of applying twice.

A change that proposes a standing answer is approved normally only when its regression check passed for that exact revision. Its outcome is on the change (evaluationState), and an edit after the check makes a pass stale. Any other finished outcome needs a recorded reason, which is kept with the reviewer:

Terminal window
curl -X POST "$API/v1/brains/tenant:acme/changes/chg_44a1e0/lifecycle/approve" \
-H "authorization: Bearer $NICIA_KEY" \
-H "content-type: application/json" \
-d '{"evaluationOverrideReason": "The judge misread the refund window; checked by hand."}'

Without one, approve answers 400 validation. A check still running answers 409 conflict and cannot be overridden. An answer published over an override is never reported verified; its verification keeps the outcome and your reason. This gate governs Review only. Whether an agent’s write enters Review at all is the Brain’s admission level (above), and an answer an agent writes directly is served as not_evaluated.

Publishing never overwrites a newer write. Every record a change was staged against is re-checked when it publishes, and if any of them was edited, created or deleted since staging, nothing is applied: publish answers 409 conflict naming each record that moved, and the change’s lifecycle becomes blocked, with blockedReason: "candidate_base_moved" and blockedRefreshAction: "restage_against_current_state". Approving it again cannot help — have the producer write the change again against the current records, then review that one. A blocked change can still be rejected or cancelled. Rejecting takes a reason, which is required — a rejection is never silent — and which GET /changes?status=rejected reports back:

Terminal window
curl -X POST "$API/v1/brains/tenant:acme/changes/chg_44a1e0/lifecycle/reject" \
-H "authorization: Bearer $NICIA_KEY" \
-H "content-type: application/json" \
-d '{ "reason": "Title is from a signature block, not a statement." }'

Approve part of a change by selecting operations. GET /changes/{changeId}/lifecycle lists the change’s operations (one per record written or deleted, with the operations each depends on), and select partitions them:

Terminal window
curl -X POST "$API/v1/brains/tenant:acme/changes/chg_44a1e0/lifecycle/select" \
-H "authorization: Bearer $NICIA_KEY" \
-H "content-type: application/json" \
-d '{ "selectedOperationIds": ["…"], "omittedOperationIds": ["…"] }'

The answer names a successorChangeId (what you selected) and a remainderChangeId (what you set aside, still pending). The original change is superseded and can never apply in full, so approve and publish the successor; the remainder stays in the queue until someone decides it. Selection is per record, not per field, and it does not edit values: when the substance is right and the wording is not, reject with a reason and have the corrected value written.

Undoing a change that has already landed is a console action today, not an operation on this API surface. When it runs, it either restores the content or refuses the whole revert up front — it never marks a change reverted while leaving the content in place.

Attribution is derived from the key that made the call — never from a field somebody filled in. “Who approved this” will be answered by the Brain, not by your logging.

Review is a checkpoint, not necessarily a human. A key with the review capability can settle changes, so you can put your own evaluator in front of the queue and only escalate what it is unsure about.

const changes = `${API}/v1/brains/tenant:acme/changes`;
const auth = { authorization: `Bearer ${NICIA_KEY}` };
// One page of the queue; walk the rest with `data.nextCursor`.
const { data } = await fetch(`${changes}?status=staged`, {
headers: auth,
}).then((response) => response.json());
for (const change of data.changes) {
const verdict = await myEvaluator(change);
if (verdict.confident) {
const lifecycle = `${changes}/${change.id}/lifecycle`;
await fetch(`${lifecycle}/approve`, { method: "POST", headers: auth });
const published = await fetch(`${lifecycle}/publish`, {
method: "POST",
headers: {
...auth,
"content-type": "application/json",
"idempotency-key": `publish-${change.id}`,
},
body: "{}",
});
if (!published.ok) {
const { data: state } = await fetch(lifecycle, { headers: auth }).then(
(response) => response.json(),
);
if (state.lifecycle.blockedReason === "candidate_base_moved") {
// A record it was staged against moved since staging. Re-run
// whatever produced the change against the current records.
await restage(change);
} else {
throw new Error(`publish ${change.id}: ${String(published.status)}`);
}
}
}
// leave the rest for a person
}

mode sets the policy’s default outcome — it is not a per-write switch. A key cannot ask for its writes to skip review, and no request body can either. The Brain’s admission policy evaluates the write’s consequence, magnitude, duplicates, and conflicts at the selected level.

“Wait” means the change stages until a human or a trusted service settles it; “land” means it is admitted immediately and shows up in /changes. L1 keeps all agent output in Review. L2 admits only trivial changes. L3 admits routine new, non-conflicting writes while retaining consequence gates. L4 turns those gates off, including the destructive-write hold, so use it only for a pipeline you explicitly trust. Human writes land at every level, except a snapshot’s change to a value a person or another source supports.

If you want a trusted pipeline writing straight in while agents are reviewed, give them separate Brains rather than caller-specific exceptions inside one. A Brain is cheap, its handle is yours to choose, and two Brains with different levels make the trust boundary explicit.

Register a webhook so a staged change reaches your queue instead of waiting to be polled:

Terminal window
curl -X POST "$API/v1/brains/tenant:acme/webhooks" \
-H "authorization: Bearer $NICIA_KEY" \
-H "content-type: application/json" \
-d '{ "url": "https://ops.example/nicia", "events": ["change.staged"] }'