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. PUT on the same handle is safe to repeat: the first response is 201, later reconciles are 200.
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. An empty sync is a successful no-op. 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 is filterable and sortable retroactively; undeclared fields are still stored and round-trip but are not query predicates. Use POST /query for exact predicates and POST /context for the prompt-shaped read.
{ "kind": "company", "where": { "plan": "pro" }, "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":{"plan":"pro"},"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.
Read errors, idempotency, authentication, and the OpenAPI reference for the complete wire contract.