Add a Brain to your product
Agents don’t need memory. They need a Brain.
This guide is for an existing SaaS or agent product. You already have stable tenant ids, canonical structured data, source documents, and one or more agents. Add a Brain without replacing your system of record.
1. Choose an immutable address
Section titled “1. Choose an immutable address”Derive the Brain handle once from your stable, non-human tenant id:
tenant:${stableTenantId}Keep the display name editable. Do not use a mutable company slug as the address. In the examples below, tenant:acme_123 is the stable handle and $API is https://api.nicia.ai.
2. Provision idempotently
Section titled “2. Provision idempotently”Send this from your provisioning backend. The examples below send
x-organization-id alongside x-api-key for a key minted before organization
binding shipped; a key minted from Organization keys in the console (or
from /v1/keys, see Authentication) is bound to
its organization and never needs it. PUT on the same handle is safe to repeat: the first response is 201, later reconciles are 200. The schema is applied on that first PUT only — a later PUT reconciles name, purpose, and mode, while governed evolution uses PATCH /v1/brains/{handle}/schema (see Schemas).
Changing which schema this call NAMES is a different thing, and it is a hard 400, not a silent skip: a Brain’s schema is evolved in place, never swapped by re-sending schema. An inline schema is filed under a slug derived from the handle, so editing the kinds and fields inside it keeps the same slug and still answers 200 — but switching between an inline schema and a named one, or between two named ones, is refused. Know the blast radius before you edit that field in a provisioning template every tenant runs: new tenants succeed on the create arm while every existing tenant’s idempotent PUT starts failing.
API="https://api.nicia.ai"HANDLE="tenant:acme_123"ORGANIZATION_ID="org_acme"
curl -X PUT "$API/v1/brains/$HANDLE" \ -H "x-api-key: $MANAGEMENT_KEY" \ -H "x-organization-id: $ORGANIZATION_ID" \ -H "content-type: application/json" \ -d '{"name":"Acme","purpose":"Customer account context","mode":"reviewed","schema":{"extends":"nicia-core","kinds":[{"name":"company","description":"A customer account in the product.","fields":[{"name":"plan","type":"string","description":"Current subscription plan."},{"name":"ownerEmail","type":"string","description":"The account owner email."}]}]}}'{ "name": "Acme", "purpose": "Customer account context", "mode": "reviewed", "schema": { "extends": "nicia-core", "kinds": [ { "name": "company", "fields": [ { "name": "plan", "type": "string" }, { "name": "ownerEmail", "type": "string" } ] } ] }}The handle is the stable customer-owned address. Store the returned opaque Brain id only when an older id-addressed integration needs it.
3. Mint two scoped Brain keys
Section titled “3. Mint two scoped Brain keys”Use the management credential only in this backend. The management-key issuance flow is interactive; once you have that credential, mint two Brain-scoped keys and keep each one in its narrow runtime:
- Sync service:
write+review(addreadonly when the sync needs to inspect accepted state). - Product agent:
read+write, neverreview.
The Brain’s mode decides whether a write is accepted or staged. A key cannot choose a direct-write bypass.
{ "name": "Acme sync service", "capabilities": ["write", "review"], "client": "api", "idempotencyKey": "acme-123-sync-key-v1"}curl -X POST "$API/v1/brains/$HANDLE/keys" \ -H "x-api-key: $MANAGEMENT_KEY" \ -H "x-organization-id: $ORGANIZATION_ID" \ -H "content-type: application/json" \ -d '{"name":"Acme sync service","capabilities":["write","review"],"client":"api","idempotencyKey":"acme-123-sync-key-v1"}'The secret is returned once. Store it in your secret manager immediately; replaying the same idempotency key returns the key record with a null secret.
4. Sync application-owned records
Section titled “4. Sync application-owned records”Use deterministic record ids derived at your edge, and keep Idempotency-Key stable for non-addressed batch writes. A sync that finds nothing to send makes no call at all — /batch requires at least one record and rejects an empty records array with a 400. Missing rows never mean deletion.
For one record, use an addressed merge. The write response tells you the address, version, and admission change; it does not read accepted values back.
curl -X PATCH "$API/v1/brains/$HANDLE/records/customer-acme-123" \ -H "authorization: Bearer $SYNC_KEY" \ -H "content-type: application/json" \ -d '{"kind":"company","label":"Acme","fields":{"plan":"pro","ownerEmail":"ops@acme.example"}}'For a group of records, send one atomic batch and retry with the same idempotency key:
{ "records": [ { "id": "customer-acme-123", "kind": "company", "fields": { "plan": "pro", "ownerEmail": "ops@acme.example" } } ], "intent": "nightly customer sync"}curl -X POST "$API/v1/brains/$HANDLE/batch" \ -H "authorization: Bearer $SYNC_KEY" \ -H "content-type: application/json" \ -H "Idempotency-Key: sync-acme-123-2026-08-23" \ -d '{"records":[{"id":"customer-acme-123","kind":"company","fields":{"plan":"pro","ownerEmail":"ops@acme.example"}}],"intent":"nightly customer sync"}'Because this Brain is reviewed, the change is staged. Approve only the change id returned by this write; do not scan the queue and approve everything:
curl -X POST "$API/v1/brains/$HANDLE/changes/$SYNC_CHANGE_ID/approve" \ -H "authorization: Bearer $SYNC_KEY" \ -H "content-type: application/json" \ -d '{}'5. Add source text at a deterministic address
Section titled “5. Add source text at a deterministic address”Documents provide cited context independently of custom-kind extraction. Give each document a stable id and send non-empty text. Set extract: false when the text should be indexed without extracting records.
{ "id": "customer-acme-123-handbook", "title": "Acme account handbook", "text": "Acme renews annually. Support escalation begins with the account owner.", "metadata": { "source": "crm" }, "extract": false}curl -X POST "$API/v1/brains/$HANDLE/documents" \ -H "authorization: Bearer $SYNC_KEY" \ -H "content-type: application/json" \ -d '{"id":"customer-acme-123-handbook","title":"Acme account handbook","text":"Acme renews annually. Support escalation begins with the account owner.","metadata":{"source":"crm"},"extract":false}'An empty document fails validation and creates nothing. Check the document status if indexing is queued or processing; a missing source in one sync is never interpreted as deletion.
6. Query typed state and read cited context
Section titled “6. Query typed state and read cited context”A declared field applies retroactively because undeclared values were never dropped: its values become governed and are returned once it is declared. Today, where supports exact slug equality only. A predicate on every other field — declared or undeclared — returns 400 index_not_ready until property indexes ship. Use POST /query for exact slug resolution and POST /context for the prompt-shaped read.
{ "kind": "company", "where": { "slug": "customer-acme-123" }, "limit": 20, "consumerId": "acme-agent"}curl -X POST "$API/v1/brains/$HANDLE/query" \ -H "authorization: Bearer $AGENT_KEY" \ -H "content-type: application/json" \ -d '{"kind":"company","where":{"slug":"customer-acme-123"},"limit":20,"consumerId":"acme-agent"}'{ "prompt": "What should the support agent know before responding to Acme?", "maxTokens": 2048, "consumerId": "acme-agent", "format": "text"}curl -X POST "$API/v1/brains/$HANDLE/context" \ -H "authorization: Bearer $AGENT_KEY" \ -H "content-type: application/json" \ -d '{"prompt":"What should the support agent know before responding to Acme?","maxTokens":2048,"consumerId":"acme-agent","format":"text"}'Pass the response’s text, citations, and receipt through to your product. warnings[] explains processing, access scope, truncation, or staged material; it does not report that two sources disagree.
7. Propose an agent change, then settle it
Section titled “7. Propose an agent change, then settle it”Give the agent key read + write, never review. Have the agent submit an addressed merge or batch write. On a reviewed Brain the response is staged, accepted state remains unchanged, and the sync service or a human later approves or rejects that one change.
Verify the boundary explicitly: an agent-key request to /changes/{changeId}/approve must return 403 forbidden. A reviewer settles the exact returned id:
curl -X POST "$API/v1/brains/$HANDLE/changes/$AGENT_CHANGE_ID/approve" \ -H "authorization: Bearer $SYNC_KEY" \ -H "content-type: application/json" \ -d '{}'Retries, failures, and cleanup
Section titled “Retries, failures, and cleanup”- Retry only
429and5xx, honorRetry-After, and cap attempts. - Repeat a timed-out addressed write at the same address; repeat a batch with the same
Idempotency-Key. - Stop on other
4xxresponses and report the parsed error plus request id. - Never continue after a malformed response.
- Revoke every test key in a
finallypath, even when Brain cleanup fails. - Delete a test Brain explicitly; deletion is a lifecycle action, never an interpretation of missing sync data. Deleting tombstones the handle so receipts and audit history still resolve — a later
PUTon the same handle is a409until it carries"revive": true. See One Brain per customer.
Read errors, idempotency, authentication, and the OpenAPI reference for the complete wire contract.