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

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.

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.

Send this from your provisioning backend. PUT on the same handle is safe to repeat: the first response is 201, later reconciles are 200.

Terminal window
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.

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 (add read only when the sync needs to inspect accepted state).
  • Product agent: read + write, never review.

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"
}
Terminal window
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.

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.

Terminal window
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"
}
Terminal window
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:

Terminal window
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
}
Terminal window
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"
}
Terminal window
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"
}
Terminal window
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:

Terminal window
curl -X POST "$API/v1/brains/$HANDLE/changes/$AGENT_CHANGE_ID/approve" \
-H "authorization: Bearer $SYNC_KEY" \
-H "content-type: application/json" \
-d '{}'
  • Retry only 429 and 5xx, honor Retry-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 4xx responses and report the parsed error plus request id.
  • Never continue after a malformed response.
  • Revoke every test key in a finally path, 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.