Skip to main content
Extensions are namespaced JSON objects attached to an item. They’re for sidecar state — data that belongs to a specific app, bound to the parent item’s lifecycle, without its own identity.

Shape

Each extension lives at a namespace on an item:
For list responses, the per-item endpoints above are an N+1 trap. Use 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:
Wildcards are supported. A credential with 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.