Shape
Each extension lives at a namespace on an item:GET /items?include=extensions (see Items → Lists are lean) to hydrate extensions inline for a batch, filtered by the same permission rule.
Example:
Namespace conventions
Namespace access is controlled by the credential’s
extension_permissions:
extension_permissions: {} can’t read or write any extension — the app must declare each namespace it needs.
When to use extensions vs a custom type
The test: does the data have identity of its own — can it meaningfully be listed, queried, or referenced separately from its parent?- Yes → custom type. Register a type, create an item, link it to the parent with an edge (
derived-from,references,supersedes). The data has its own id, provenance, lifecycle. Multiple instances per parent coexist. Queryable as a first-class type. - No → extension. Bound to the parent. Deleting the parent deletes it. No independent identity.
Examples
The payoff: when multiple apps produce the same kind of structured content (three AI tools all generating cleaner versions of the same note), custom types let them coexist cleanly. Extensions would force overwrites — one namespace key, one value.
Legitimate uses
- Ephemeral per-session state. Reading position, last-viewed timestamp, scroll offset. Rewritten frequently. Bound to the parent’s lifecycle.
- Private app bookkeeping. Sync-agent file hashes, rate-limit counters. Data with no meaning outside the writing app.
What not to use extensions for
- Structured content with its own identity. Use a custom type.
- Cross-app shared state. The semantics drift; apps write the same namespace with different intentions; last-writer-wins collisions lose information.
- Sync cursors. A sync agent’s per-root cursor is agent-local state — it lives on the agent’s own disk, not on any item in the platform. Extensions are for sidecar state attached to a specific item; sync cursors don’t belong to an item.
Permissions reminder
Extensions are not returned in normal
GET /items responses — a client must explicitly request a namespace it has permission for. Extensions can’t leak via generic reads. A credential without extension_permissions for a namespace gets 403 forbidden on that request, regardless of its type permissions on the parent item.