> ## Documentation Index
> Fetch the complete documentation index at: https://docs.maximem.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Scope ladder

> The scope chain is a ladder of named levels, and the familiar Client, Customer, User chain is the default three rungs. This page explains what a level is, why a level's name can change and its key cannot, which direction a read travels, and exactly which parts of custom depth you can reach today.

<Note>
  Read [Identifiers & Scopes](/concepts/memory-scopes) first. This page assumes
  you know what `customer_id` and `user_id` do.
</Note>

<Tip>
  If your account uses the default `Client → Customer → User` chain, nothing here
  changes how you integrate. This page matters when that shape does not describe
  your business.
</Tip>

## What a level is

A scope is a position on a ladder. Your account is the top rung, and each rung
below it narrows who a memory belongs to. The default ladder has three:

```
client     your account
  customer   an organization you serve
    user       one person inside it
```

Every memory is filed at exactly one rung, and it carries that rung plus every
rung above it. That stored list is what a read filters on.

### One ladder per account

You have one ladder, and every instance you run shares it. There is no
per-instance ladder.

This follows from what an id means. A customer id identifies the same customer
across your whole account, and a user id the same person, whichever instance
sent the request. Two instances writing about customer `acme` are writing about
one customer. If they disagreed about what the levels are, the same records
would carry different level names depending on which instance wrote them, and a
read from one would not find what the other stored.

Instances keep their own memory settings. The switch that turns nested scoping
on is also per instance, so you can enable it for one instance and watch it
before enabling the rest. The shape is shared; the moment each instance starts
using it is not.

## If your account is B2C

Some accounts are set up so that a customer and a person are the same thing: one
consumer, using your product for themselves, belonging to no organisation. Synap
calls this `user_context_isolation: equals_customer`, and your integration sends
a `user_id` and no `customer_id`.

**On those accounts the customer rung and the user rung are one node.** Not two
levels that happen to have one member each. The same node, reached by either
name:

```
you send:        user_id = "person-7"
Synap resolves:  customer = person-7   and   user = person-7
```

That has three consequences worth knowing before you change your ladder.

**A rung between customer and user has nowhere to sit.** It would be a container
between one person and the same person. Synap will accept the shape, and a
request naming it will resolve, but it is almost never what anyone means.

**A rung ABOVE the customer is the shape that works.** Something people belong
to, rather than something inside a person:

```
client  >  region  >  person
```

That groups consumers by something real, and does not fight the collapse.

**Your integration must keep sending only `user_id`.** Sending a `customer_id` to
a B2C account is refused with `customer_id_not_accepted_on_b2c`, and a scope path
does not change that. If you send a path, name the customer rung with the same
value as the user rung.

<Warning>
  **A deeper ladder means your writes must send a path.** With a rung between
  the customer and the user, `user_id` alone can no longer say which one a
  memory belongs to, and Synap refuses the write rather than guessing. Upgrade
  to Python SDK 0.4.8 or JS SDK 0.4.7 and send the full path before adding such
  a rung, not after. One gap to know about first: only `memories.create` takes a
  path today. Transcript ingest does not, so if you push transcripts, talk to us
  before you add a rung between your customer and your user.
</Warning>

## Where your ladder comes from

You do not start from a blank ladder, and you do not start from ours either.

When you onboard, you give us a use-case document: what your agent does, who it
talks to, and what it should remember. That document already answers the scoping
question, usually in your own words. A fintech client wrote "**Client**: the
company operating the platform. **Customer**: an Indian retail consumer." A
clinical client wrote "memory is about the patient, not the clinician using the
application," which is a different shape entirely and says so plainly.

So we read it and propose a ladder from it: how many rungs, what each one is
called, and one sentence per rung explaining what it holds. The proposal quotes
the line in your document that each rung came from, so you can see the reasoning
rather than take it on trust.

<Note>
  The proposal is a draft. It is not applied, and nothing about how your memories
  are stored or read changes until you accept it. See [Ratifying
  it](#ratifying-it).
</Note>

If you did not upload a use-case document, or it does not say anything about who
your users are, we propose the default three rungs and tell you that is what
happened. We would rather show you a plain default and say so than invent a
structure from a document that does not support one.

### What we will and will not infer

We name rungs from what you wrote. We do not invent privacy boundaries from
prose that does not ask for one.

| From your document                                          | What we do                                                                  |
| ----------------------------------------------------------- | --------------------------------------------------------------------------- |
| A section naming what Client, Customer and User mean to you | Use your words as the rung names and descriptions                           |
| "Memory is about the patient, not the clinician"            | Propose a rung for the subject, separate from the person using the agent    |
| Users grouped by team, branch, practice or region           | Propose that grouping as a rung between your customers and your people      |
| Nothing about grouping                                      | Propose the default three rungs, and say the document did not indicate more |

A rung decides who can read whose memories. Adding one that you did not ask for
would be a privacy decision made by inference, so a rung we are not confident
about is raised as a question in the proposal rather than added to it.

## Ratifying it

A proposed ladder does nothing. It is a draft until a person accepts it, and
until then your account keeps using the default chain.

Accepting records who accepted and when. That record matters because a ladder is
close to permanent: a rung can be renamed at any time and can never be deleted,
only retired, because memories already resolve through it.

Before you accept, you can:

* **rename any rung**, which is free and changes nothing about who reads what
* **remove a proposed rung** you do not want
* **add one** we did not propose

After you accept, renaming stays free, adding a rung is a migration we show you
the size of first, and removing is no longer possible.

<Warning>
  An unratified ladder is never used at runtime. If you never accept the
  proposal, your account keeps the default `Client → Customer → User` chain
  indefinitely and nothing breaks.
</Warning>

## Which direction a read travels

This is the rule worth reading twice, because it is easy to hold backwards.

**A request sees its own rung and every rung above it. It never sees a rung
below, and never a neighbour.**

So on the default ladder:

| A request identifying | Can read memories filed at              |
| --------------------- | --------------------------------------- |
| a user                | that user, their customer, your account |
| a customer            | that customer, your account             |
| your account only     | your account                            |

A memory filed at the user level is **not** visible to a request that identifies
only the customer. One person's private context does not surface for their
colleague, and it does not surface for a customer-wide question.

<Warning>
  A memory's stored chain is the path down **to** it. That is the list of rungs
  the memory can be read **from**, not a list of places the memory is visible.
  A fact filed at the user rung carries `client, customer, user`, and it is still
  readable only by that user.
</Warning>

### Nothing sits below the person

The rung that identifies a person is always the last one. You cannot add a rung
under it, and Synap refuses a ladder that tries. Two reasons, and both are hard
to undo later. A rung under a person multiplies the work of consolidating
memories against the busiest part of your tree, and it makes deletion
ambiguous: erasing a person would first have to decide what happens to
everything filed underneath them.

The account rung at the top is fixed in the same way. It is your account itself,
so nothing can go above it. Every rung you add goes somewhere in between.

## Ranking: closer material wins

Filtering decides what a request is allowed to read. Ranking decides what it sees
first. Among the memories a request may read, the ones filed closer to the person
asking rank above the ones filed further up.

A person's own preference outranks a fact that is true of their whole
organization, when both match the question equally well. This is a ranking
adjustment only: it never makes a memory readable that was not, and it never
hides one that was.

## A level's name can change, its key cannot

Every level has two parts.

* The **label** is what people read: "Customer", "Practice", "Region". Change it
  whenever you like. Nothing stored depends on it.
* The **key** is what stored memories resolve through: `customer`, `user`. It
  never changes, because every memory already filed under it would stop
  resolving.

For the same reason a level can never be deleted, only **deprecated**. A
deprecated level stops being offered for new structure, and everything already
filed under it stays readable.

## Sending a scope path

Requests normally identify a scope with `customer_id` and `user_id`, and Synap
maps those onto the default three rungs. That is what almost every integration
does, and it needs no change.

**Why two ids only ever reach three rungs.** They are matched by ROLE, not by
position: `customer_id` goes to whichever rung holds `tenant_of_record`, and
`user_id` to whichever holds `identity`. There are exactly three reserved roles,
so two ids can address exactly three rungs. A rung you added yourself holds no
reserved role, so there is no field for it and no way to guess one. That is the
whole difference:

|            | what you send                                       | what it means                                 |
| ---------- | --------------------------------------------------- | --------------------------------------------- |
| Ids        | `customer_id`, `user_id`                            | "here are two values, work out where they go" |
| Scope path | `{"customer": "...", "team": "...", "user": "..."}` | "here is what belongs on each rung, by name"  |

On the default three rungs the two are equivalent, and a path is simply the
longer way to say the same thing. A path starts to matter only when your ladder
has a rung the ids cannot name.

To address a level beyond those three, pass a scope path from the SDK, naming
every rung by its key. On a read it is the `scope_path` argument:

<CodeGroup>
  ```python Python theme={null}
  context = await sdk.user.context.fetch(
      user_id="d-42",
      search_query=["what did we agree on pricing"],
      max_results=10,
      scope_path={
          "customer": "acme",
          "team": "payments",
          "user": "d-42",
      },
  )
  ```

  ```javascript JavaScript theme={null}
  const context = await sdk.user.context.fetch({
    user_id: 'd-42',
    search_query: ['what did we agree on pricing'],
    max_results: 10,
    scope_path: {
      customer: 'acme',
      team: 'payments',
      user: 'd-42',
    },
  });
  ```
</CodeGroup>

On a write it is the `scope` argument on `memories.create`:

<CodeGroup>
  ```python Python theme={null}
  await sdk.memories.create(
      document="Dana wants invoices on the first of the month.",
      user_id="d-42",
      customer_id="acme",
      scope={
          "customer": "acme",
          "team": "payments",
          "user": "d-42",
      },
  )
  ```

  ```javascript JavaScript theme={null}
  await sdk.memories.create({
    document: 'Dana wants invoices on the first of the month.',
    user_id: 'd-42',
    customer_id: 'acme',
    scope: {
      customer: 'acme',
      team: 'payments',
      user: 'd-42',
    },
  });
  ```
</CodeGroup>

Two rules apply, and both fail loudly rather than guessing:

* **Every rung must be named.** A path that skips a level is refused, and the
  error names the level you left out. Synap never infers a missing id, because a
  wrong guess at or above the organization level would cross a tenant boundary.
* **The feature must be enabled for your instance.** If it is not, sending a
  `scope` map is an error rather than being ignored. Ignoring it would widen the
  request to the whole organization when you asked to narrow it to one team.

<Warning>
  **Check your SDK version before you rely on this.** Reads take a path from the
  Python SDK at 0.4.6 or later, and from the JS SDK at 0.4.4 or later. Writes
  take one on `memories.create` from Python 0.4.8 and JS 0.4.7. On anything
  older, the `scope` argument on a fetch names which method you called (`user`,
  `customer`, `client`), not a path, so an older SDK reaches the default three
  rungs and nothing beyond them. Upgrade with `pip install -U maximem-synap` or
  `npm install @maximem/synap-js-sdk@latest`. gRPC cannot carry a path at all.
</Warning>

<Note>
  Transcript ingest does not take a scope path in any released SDK yet. If your
  ladder has a rung between your customer and your user and you push
  transcripts, talk to us before you add it.
</Note>

## What you can reach today

Being precise about this matters more than making the feature sound finished.

|                                                 | Available now                                                                                                         |
| ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| The default three-rung ladder                   | Yes, on every account, from signup                                                                                    |
| Reading and ranking across those rungs          | Yes                                                                                                                   |
| Reading with a `scope` path                     | Python SDK 0.4.6 or later, JS SDK 0.4.4 or later                                                                      |
| Writing with a `scope` path                     | `memories.create` only, from Python 0.4.8 and JS 0.4.7                                                                |
| Sending a path on transcript ingest             | Not yet, in any released SDK                                                                                          |
| Sending one from an older SDK, or from gRPC     | No. Upgrade the SDK; gRPC cannot carry a path at all                                                                  |
| Adding a level beyond the default three         | In the dashboard, with a preview of how many records move. Your integration has to be able to send a scope path first |
| Renaming a level's label                        | In the dashboard                                                                                                      |
| Deleting a level                                | Never; deprecate instead                                                                                              |
| A level below the person, or above your account | Never                                                                                                                 |
| Sharing between levels that are not ancestors   | No                                                                                                                    |

The honest summary: **Synap's scoping is correct and safe on the shape accounts
already use. Driving a different shape needs an SDK new enough to send a scope
path, and on writes that means `memories.create`.**

Which of these you can reach is decided by what your own integration sends, and
that is something you can change. You do not have to go and find out what it
sends: the Scope Ladder page in your dashboard lists the SDK versions we have
seen in your recent requests, tells you the oldest one, and says so plainly when
it has not seen any requests from you at all.

## Adding a level

<Warning>
  **Check what your integration can send before you add one.** A level outside
  the default three is only reachable by a call that sends a scope path. If your
  calls come from a Python SDK older than 0.4.8, or a JS SDK older than 0.4.7,
  your writes cannot send one, so adding a level does not give you a level:
  writes aimed at it fail with `CannotPlaceWrite`. Upgrade first, with
  `pip install -U maximem-synap` or `npm install @maximem/synap-js-sdk@latest`.
</Warning>

This is a fact about your integration, not a limit on your account, and it goes
away when you upgrade. The Scope Ladder page in your dashboard checks it for
you: it names the SDK versions in your recent traffic, and it will not let you
add a level the oldest of them cannot address. If it has not seen any requests
from you, or your requests do not report a version, it says that rather than
guessing.

A level goes between two existing ones. It cannot go above your account rung or
below the rung that identifies a person, so on the default ladder that means
above Customer or above User. Either way it is a migration rather than a
setting: every node beneath the insertion point is re-parented under a new node
at the new level, in one transaction, and Synap creates one placeholder parent
per existing group so nothing is left without one. Those placeholders are yours
to rename.

Reads are unaffected while this happens. Memories above the insertion point are
never touched, and no memory changes the group it belongs to.

<Warning>
  One thing lags. Existing memories do not immediately carry the new level as
  one of their ancestors, so a query asking for everything beneath the new level
  is incomplete until Synap finishes stamping them. Ordinary retrieval is not
  affected. Ask us to run the backfill after you add a level.
</Warning>
