Skip to content
Agentmart
Submit a tool

sanity-singletons

Configure singleton documents in a Sanity Studio using the document.singletons registry.

Skill by sanity-ioOfficial

IndexedMIT★ 194
Write a review

Install

Works with Claude Code, Claude apps, Codex, Cursor, Gemini CLI, GitHub Copilot, VS Code, OpenCode, Goose, Windsurf. Read the repository before you give any tool access to your data.

npx agentmarket install skill/sanity-io-sanity-singletons

Asks which of your tools to add it to (Claude Code, Codex, Cursor, VS Code, Windsurf, Gemini CLI, Claude Desktop) and shows the change first. See https://agentmart.dev/cli.

Get the weekly diff

The weekly diff: one email a week with new agent tools, notable updates and grade changes, plus security alerts for tools you follow. Double opt-in, one-click unsubscribe.

We use your email only for this newsletter. Privacy

About

Configure singleton documents in a Sanity Studio using the document.singletons registry. Use when a developer wants a fixed-id document (settings, navigation, footer) edited in place, hidden from the default content list, with no "create new" or "duplicate" affordances — or when migrating a userland singleton implementation or a third-party singleton plugin (e.g. sanity-plugin-singleton-management) to the first-class API.

Use cases: documents, backend, writing

No reviews yet. Installed it? Reviews come from verified installers only. How reviews work

Permissions

What this skill can do and what it asks for, from its SKILL.md. Checked Oct 25, 2026.

Tools
No tools found
Secrets
None asked for
Install scripts
None found

Network hosts it talks to 1

  • codeload.github.com · found in install command

Files it touches

  • ~/.claude/skills/sanity-singletons

What's inside

Skill name
sanity-singletons
Description
Configure singleton documents in a Sanity Studio using the document.singletons registry. Use when a developer wants a fixed-id document (settings, navigation, footer) edited in place, hidden from the default content list, with no "create new" or "duplicate" affordances — or when migrating a userland singleton implementation or a third-party singleton plugin (e.g. sanity-plugin-singleton-management) to the first-class API.
License
MIT
Instructions
196 lines in SKILL.md · view on GitHub

Files (1)

  • SKILL.md

SKILL.md (first lines)

# Sanity Singletons

A "singleton" is a document with a fixed id that can be created exactly once. Common examples: site settings, navigation, footer content. Studio supports singletons as a first-class primitive via the `document.singletons` configuration registry plus a small set of Structure Tool helpers.

**Requires `sanity` 6.17.0 or later.** Check the installed version in the lockfile before using this API. On older Studios, either upgrade first or use the userland pattern (fixed `documentId` in a custom structure, plus manual `newDocumentOptions` and `duplicate` filtering) described in `sanity-best-practices` → `studio-structure`.

## When to use this skill

- A developer asks how to create a "settings document" or "singleton".
- A studio configuration needs a `document.singletons` registry entry.
- Structure Tool needs to render a singleton as a list item or document pane.
- You're migrating an existing userland singleton implementation to the first-class API.
- You're migrating from a third-party singleton plugin such as `sanity-plugin-singleton-management` (or its predecessor `sanity-plugin-singleton-tools`).

## Registering a singleton

Singletons are registered in configuration, **not** on the schema type. A singleton definition binds a _definition id_ to a _document id_ and a _schema type_:

```ts
import {defineConfig, defineSingleton} from 'sanity'

export default defineConfig({
  schema: {
    types: [/* siteSettings and campaign are ordinary document types */],
  },
  document: {
    singletons: [
      // String shorthand — expands to
      // {id: 'siteSettings', documentId: 'siteSettings', schemaType: 'siteSettings'}
      'siteSettings',

      // Definition object. `defineSingleton` is an optional typed identity
      // helper; a plain object literal works too. `id` is omitted here, so it
      // inherits each `documentId`.
      defineSingleton({
        documentId: 'springCampaign',
        schemaType: 'campaign',
        title: 'Spring campaign', // optional display metadata
        initialValue: {season: 'spring'}, // optional per-singleton initial value
      }),
      defineSingleton({
        documentId: 'summerCampaign',
        schemaType: 'campaign',
        title: 'Summer campaign',
        initialValue: {season: 'summer'},
      }),
    ],
  },
})
```

Key facts:

- **`defineSingleton` produces a singleton _definition_, not a document schema.** The schema type it references is defined separately with `defineType` and needs no singleton-specific configuration.
- **Multiple singletons may share a schema type** (like the two campaigns above), each with its own `title`, `icon`, and `initialValue` (falling back to the schema type's), and a shared schema type may also back ordinary documents.
- **The definition id is the singleton's stable identity**, and it is **optional** — it inherits `documentId` when omitted. Structure code references it (`S.listItem().singleton('springCampaign')`). Set it explicitly only to be verbose, to guard against future collisions, or to address a singleton universally when different workspaces or environments map it to different document ids.
- A resolver function is also accepted: `singletons: (prev, context) => [...prev, ...]`. Resolvers only ever receive fully resolved definitions (string shorthands are expanded first).

Validation at config-resolution time (all failures aggregate into one `ConfigResolutionError`):

- `id`: non-empty, unique across definitions (inherited ids count — an explicit id may not collide with another definition's inherited one). Used verbatim as a Structure Tool node id, so it is limited to `[a-zA-Z0-9._-]` and may not start with `__edit__`. It also may not match the name of a document type that no singleton claims, since the default content list keys both by id (the string shorthand never trips this — the type it names is claimed by the definition itself).
- `documentId`: non-empty, unique, published-id shaped (no `drafts.` / `versions.` prefix, only `[a-zA-Z0-9._-]`).
- `schemaType`: must name an existing document type. Need not be unique.
- Templates: an untagged template must not reuse a definition id (the generated singleton template claims it); at most one template may carry a given `singleton` tag; tags must reference registered definitions.

## What Studio does automatically

1. **Generates a dedicated initial value template per singleton** — tagged with the definition id via `ResolvedTemplate.singleton`, value from `definition.initialValue ?? schemaType.initialValue` — instead of the plain per-type template, and **removes tagged templates from every "create new" surface** (global create menu, structure panes, reference inputs). The tagged template stays in `source.templates`, so opening the singleton before it exists still applies its initial value.
2. **Injects `context.singleton`** (the definition id) into document-scoped configuration contexts — `document.actions`, `badges`, `inspectors`, field actions, `comments.enabled`, `askToEdit.enabled` — looked up by _published_ document id, so drafts and release versions of the singleton match, however the document was opened (structure, intent, deep link, search).
3. **Filters the `duplicate` document action**, after all user resolvers run, so it cannot be reintroduced via `document.actions`.
4. **Hides the schema type from the implicit default content list** in Structure Tool, and **appends the singleton itself to that list** — one item per definition, after the document type lists, in registration order, each equivalent to `S.listItem().singleton(id)`. A studio that has not configured Structure Tool needs no structure code at all. This applies only to the default structure; a `structure` resolver replaces it wholesale.

What Studio deliberately does **not** do:

- **Delete, unpublish, and discard stay available.** Deleting a singleton is a legitimate "reset": structure still points at the fixed id, and editing recreates the document. To hide delete as well: `document: {actions: (prev, context) => (context.singleton ? prev.filter((action) => action.action !== 'delete') : prev)}`.
- **Explicit structure is never filtered.** `S.documentTypeList(typeName)` works for singleton schema types and intentionally lists the singleton documents alongside ordinary ones.
- **No warning for "dangling" singletons.** Structure resolves lazily, so Studio cannot detect a registered singleton that a _custom_ structure never adds. (The default structure adds them all, so this only affects studios with a `structure` resolver.) Any heuristic short of eager resolution would misfire, because manual `S.document().documentId(...).schemaType(...)` wiring is supported. In a custom structure, always pair a registry entry with a structure entry.

## Surfacing singletons in Structure Tool

**With the default structure there is nothing to do** — registered singletons are appended to the top-level content list automatically.

In a **custom structure** (any studio passing a `structure` resolver to `structureTool()`), add them explicitly. All helpers are keyed by **definition id** and throw a `SerializeError` for unknown ids.

Watch for this trap: `S.documentTypeListItems()`, the usual way to reproduce the default lists inside a custom structure, excludes singleton schema types and does **not** add singleton items. `S.list().items([...S.documentTypeListItems()])` therefore drops every singleton. Use `.singletons()` alongside it:

```ts
S.list().id('__root__').title('Content').items(S.documentTypeListItems()).singletons()
```

```ts
// 1. Highest level: a list of singletons — a given set, or all of them.
S.list().id('singletons').title('Singletons').singletons(['siteSettings', 'springCampaign'])
S.list().id('singletons').title('Singletons').singletons()

// 2. A single list item (title/icon default from the definition, then the schema type).
S.listItem().singleton('siteSettings')

// 3. Low level: just the document pane.
S.listItem().title('Settings').id('settings').child(S.document().singleton('siteSettings'))
```

Composition rules:

- Every default is overridable by normal chaining: `S.listItem().singleton('siteSettings').title('Global settings')`, `S.document().singleton('siteSettings').documentId('override')`.
- **Titles are not disambiguated for you.** The list item id defaults to the definition id (unique by construction), but the title falls back to the schema type's, so singletons sharing a schema type share a label. Give each definition a `title` — it applies everywhere the singleton is rendered, including `S.list().singletons()`, which has no per-item overrides.
- `S.document().singleton()` pins the singleton's tagged initial value template so its initial value applies even when the type has several templates; override with `.initialValueTemplate(...)`.
- **`S.list().singletons()` returns a builder without `.items()`.** Calling `.items()` afterwards would replace the whole array and silently drop the singleton items, so the type forbids it. Call `.items([...])` _before_ `.singletons([...])`, or put `S.listItem().singleton()` entries inside a regular `.items()` array. Repeated `.singletons()` calls append.
- **`singletons()` with no argument means every registered definition** (registration order, including plugin-registered ones); `singletons([])` means none. Combining it with a hand-written `S.listItem().singleton(id)` in the same list adds that singleton twice and fails with a duplicate-id `SerializeError` — pass an explicit array when mixing.
- **`S.documentListItem().singleton(id)`** renders a live document preview instead of a static title, and defaults its id to the definition's `documentId` (not the definition id), because a document list item's id _is_ the document it previews. Prefer plain `S.listItem()` when the singleton document may not exist yet.

## Shared schema types and the "create new" escape hatch

Declaring a singleton over schema type `T` replaces `T`'s plain per-type template with the singleton's tagged template, so `T` disappears from create menus. If ordinary `T` documents should stay creatable, define an explicit untagged template — any id works, including `T` itself, since the plain template no longer exists:

```ts
schema: {
  templates: (prev) => [
    ...prev,

From sanity-io/agent-toolkit, MIT.

More from sanity-io/agent-toolkit

Signals

Stars
194
Forks
30
Open issues
6
Contributors
21
Last commit (this folder)
Oct 12, 2026 (16 days ago)

From GitHub (fetched Oct 24, 2026).

Details

License
MIT
First seen
Oct 24, 2026
Last checked
Oct 24, 2026

More from sanity-io

Cite this page

sanity-singletons skill. Agent Mart. https://agentmart.dev/skills/sanity-io-sanity-singletons (last updated 2026-10-24)