Org Trees
Model a company's memory as an org tree, with teams, groups, people, compartments and private regions, and know the rules a shared scope follows.
An org tree is a tenant whose root scope is registered with "tree": {"kind": "org"}. Its scopes
carry a kind (a team, a group, a person), a parent (where it sits in the chart) and optionally
private. The tree decides who reads what by default, and a few rules keep shared material from
mixing with private material. A tenant without an org tree is unaffected by anything on this page.
When to use it
Use an org tree for a company or team brain: several people, their AI assistants and the company's
sources in one memory, with team spaces, cross-team groups and each person's own private memory. For an
application that only keeps each end user's memory apart, plain scopes under one root
(org:<tenant>/user:<id>) are enough; see Scopes.
Kinds
tree.kind | Use it for | Notes |
|---|---|---|
org | The tenant root | No parent |
team | A team or a department | Units are flat siblings under the root; tree.parent gives the chart, so a reorg changes parent and moves no memories |
group | A project, a committee, a meeting series, a 1:1 pair | Cross-team, read by its roster |
person | One person's own memory | Usually private |
employee | An AI employee's own working memory | Usually private |
compartment | An audience compartment of a unit or a person: aud:pub, aud:lead, aud:roll, aud:work | Published material, the unit's leads, rollups, and the part of a person's work that rolls up |
A path's kind is the kind of the nearest registered record at or above it, so an unregistered path under a person's scope answers as that person's.
Private regions
"private": true on a record makes it, and everything under it, a private region: read only by
name, by the people on its roster; never through an ancestor or a descendant; never an input to
anything CortexDB synthesizes outside it. "private": false reopens a part of a private region (for
example a person's aud:work, the part of their work that rolls up to their team). Left out, a record
is private when its nearest registered ancestor that says so is.
What changes at a shared scope
An org, team, group or compartment scope is shared. Four rules hold there:
- No walking views.
holistic,descend,structuredandlineageanswer422 VIEW_NOT_ALLOWED_AT_SCOPE(details.allowed_views: ["granular", "raw"]). A read that names no view readsgranular, the scope's own memories. To read across scopes, name them withextra_scopes(up to 40). A person's or employee's own scope keeps the default views. - No auto-registration. A first write to an unregistered path under a shared scope answers
403 SCOPE_REGISTRATION_REQUIRED. Register every scope of the tree before anyone writes to it. - Private stays private. No walk enters a private region it did not start in, whatever a roster says, the operator key included. An export of a shared scope reads that scope's own rows.
- No implicit blending. CortexDB never homes a synthesis of two scopes at a shared scope, and never homes a private scope's material outside its region.
Names belong in the directory, not in records: since
0.14.0 a registry write that sets tree.display_name answers 422 INVALID_SCOPE_TREE (the records are
kept for ever in the log; a directory entry can be erased).
Set one up
Tokens and records must belong to the same tenant. For an org tree, make the tenant's id the root's id:
org:acme is tenant acme. On a server that holds one organization and mints its own tokens, start it
with:
CORTEX_API_KEY=<operator key>
CORTEX_V1_MINTER_ENABLE=1
CORTEX_V1_AUDIENCE=cortexdb:tenant:acme # minted tokens belong to tenant acme
CORTEX_OPERATOR_INHERIT_TENANT=on # the operator key's registrations take the root's tenantTokens from your identity provider name their tenant through the issuers file instead (Identity and Tokens).
Register the root, bound to its tenant, and make it an org:
curl -sS -X POST "$CORTEX_URL/v1/admin/access/tenant-binding?scope=org:acme" \
-H "Authorization: Bearer $CORTEX_API_KEY" -H "X-Cortex-Actor: user:admin" \
-H "Content-Type: application/json" -d '{"from": "default", "dry_run": false}'
# {"scope":"org:acme","from":"default","to":"acme","root_registered":true,…}
curl -sS -X PUT "$CORTEX_URL/v1/scopes/tree?path=org:acme" \
-H "Authorization: Bearer $CORTEX_API_KEY" -H "Content-Type: application/json" \
-d '{"kind": "org"}'The binding call registers org:acme for tenant acme with user:admin (the X-Cortex-Actor) as its
Owner. Under the deny operator mode (the default on cloud_shared_saas) the operator key's
registrations need a break-glass header (Operator Modes). Then
register the people and units. A person's private region:
curl -sS -X POST "$CORTEX_URL/v1/scopes" \
-H "Authorization: Bearer $CORTEX_API_KEY" -H "X-Cortex-Actor: user:admin" \
-H "Content-Type: application/json" \
-d '{"path": "org:acme/user:alice",
"members": [{"actor": "user:alice", "role": "owner"}],
"tree": {"kind": "person", "parent": "org:acme", "private": true}}'A team, with a reorg's worth of people or more, is best added as a
change set, which previews who gains and loses access before
anything changes. POST /v1/scopes with a tree works for a single record:
curl -sS -X POST "$CORTEX_URL/v1/scopes" \
-H "Authorization: Bearer $CORTEX_API_KEY" -H "X-Cortex-Actor: user:admin" \
-H "Content-Type: application/json" \
-d '{"path": "org:acme/group:payments",
"members": [{"actor": "user:alice", "role": "writer"}, {"actor": "user:bob", "role": "writer"}],
"tree": {"kind": "group", "parent": "org:acme"}}'Then give each person their own token (Identity and Tokens), and name people, teams and groups in the directory:
curl -sS -X PUT "$CORTEX_URL/v1/directory/user:alice?scope=org:acme" \
-H "Authorization: Bearer $CORTEX_API_KEY" -H "Content-Type: application/json" \
-d '{"display_name": "Alice Kim", "emails": ["alice@acme.example"], "title": "Engineering manager"}'Reading in an org tree
A read at a team scope reads that team's memories. A person who reads a team and their own memory names both:
curl -sS -X POST "$CORTEX_URL/v1/recall" \
-H "Authorization: Bearer $ALICE_TOKEN" -H "X-Cortex-Actor: user:alice" \
-H "Content-Type: application/json" \
-d '{"scope": "org:acme/user:alice", "extra_scopes": ["org:acme/team:eng"],
"query": "payments migration", "coverage": true}'Asking for a walking view at the team scope is refused:
{"error_code": "VIEW_NOT_ALLOWED_AT_SCOPE",
"details": {"allowed_views": ["granular", "raw"], "field": "view", "scope_kind": "team", "view": "holistic"},
"retriable": false}Publish and rollups
Agents draft; people publish. POST /v1/publish is how a person widens a memory: it copies an event (or
a rollup) the person is served into a scope they write, with the original as its provenance, so erasing
the original erases the copy. Only a person's own token publishes; an agent's token, a service and the
operator key get 403 PUBLISH_PERSON_REQUIRED.
curl -sS -X POST "$CORTEX_URL/v1/publish" \
-H "Authorization: Bearer $ALICE_TOKEN" -H "X-Cortex-Actor: user:alice" \
-H "Content-Type: application/json" \
-d '{"target": "org:acme/group:payments",
"source": {"scope": "org:acme/user:alice", "event_id": "evt_01a186bd6fa679d3be8735fa0d80ffd6"}}'
# 202 {"event_id":"evt_…","status":"captured","published_from":{"scope":"org:acme/user:alice","event_id":"evt_01a186bd…"},…}A unit (a team, or the org) whose aud:roll compartment is registered with the rollup audience gets
rollups: typed monthly totals of its inputs and, where the background LLM stages run, synthesis
items, built on a cadence within a daily budget (CORTEX_ROLLUP_CADENCE, CORTEX_ROLLUP_BUDGET_USD). GET /v1/rollups?scope=<unit>/aud:roll serves
them, each only to a reader of every input it was built from.
See also
Shared Memory and the Access Model
One CortexDB for a tenant's sources, applications and org brain. Every memory has a home, every read is decided against it, and the access API says who may read what and why.
Change Sets
Stage access changes, preview who gains and loses access, and activate them at once or on a date, with the tenant's own assertions as a guard.