Documentation
Taxonomies
A taxonomy scheme is a controlled vocabulary your whole organization shares: a named list of concepts with an optional hierarchy, localized labels, and editorial notes. Content fields assign concepts by ID, so labels and structure live in one place. Renaming a concept never rewrites a single content document.
Schemes are organization-level, not per space. They sit beside models and blocks under schemas/, their write routes need schema write permission (developer or admin), and their published versions appear in the same public manifest that generated clients use for drift checks.
Schemes and concepts
A scheme has an ID, a localized label, and a map of concepts keyed by concept ID. IDs use lowercase letters, numbers, underscores, and hyphens. They are stable and are never generated from labels.
{
"label": { "en": "Topics", "is": "Umfjöllunarefni" },
"description": { "en": "Editorial topics" },
"concepts": {
"nature": { "prefLabel": { "en": "Nature" }, "broader": [], "order": 0 },
"geology": {
"prefLabel": { "en": "Geology" },
"broader": ["nature"],
"order": 0
},
"volcanoes": {
"prefLabel": { "en": "Volcanoes", "is": "Eldfjöll" },
"altLabels": { "en": ["Volcanism"] },
"hiddenLabels": { "en": ["vulcanoes"] },
"definition": { "en": "Active and dormant volcanoes." },
"related": ["glaciers"],
"broader": ["geology"],
"order": 1
}
}
}prefLabel is the display label, and every concept needs a non-empty preferred label in the organization default locale. Readers fall back from the requested locale to the scheme's default locale. altLabels are synonyms editors see. hiddenLabels are search-only spellings: put misspellings and retired names there so the picker still finds the concept. related connects concepts without implying hierarchy. broader is the parent, written as an array holding zero or one ID, and order sets editorial order among siblings.
Where a scheme is stored
Each scheme uses one logical key, schemas/taxonomies/{schemeId}.json, in two places. The private authoring copy holds everything, including hidden labels, notes, audit data, tombstones, and unpublished edits. An explicit publish writes a sanitized public projection at the same logical key at the bucket root. The projection is an allowlist: it carries prefLabel, broader, order, and, when set, altLabels, definition, related, and externalUri. Hidden labels, notes, and tombstones never leave the private copy.
The public schemas/manifest.json lists one entry per scheme:
{
"taxonomies": {
"topics": { "version": 3, "conceptCount": 214 }
}
}version is the published projection's version, and 0 means the scheme has never been published. No labels or concept IDs appear in the manifest.
The taxonomy field
A taxonomy field points at one scheme and stores concept IDs on the entry.
{
"topics": {
"type": "taxonomy",
"scheme": "topics",
"multiple": true,
"maxItems": 10,
"rootConcept": "nature",
"allowedConcepts": ["nature", "culture"]
}
}scheme is required. multiple allows more than one concept. maxItems caps the count from 0 to 50. rootConcept restricts the picker to one subtree and allowedConcepts restricts it to several. A rootConcept or allowedConcepts entry that is not a concept of the named scheme is a model validation error. A model may define at most 10 taxonomy fields.
The stored value is always an array, even when multiple is false:
{ "topics": [{ "scheme": "topics", "id": "volcanoes" }] }Taxonomy fields cannot be translatable, because concepts are language-independent and the scheme carries the localized labels. They are always included in draft and published summaries whether or not summaryFields lists them, projected as the raw { scheme, id } array with no labels or ancestors added. See Summaries and indexes for why a taxonomy field cannot be a field index or a list-index sort source.
Publishing a scheme is separate from publishing content
Scheme edits are private until you publish the scheme. Publishing content does not publish the scheme, and publishing the scheme does not republish content. A normal state for a scheme with pending edits is "draft v4, published v3", which the dashboard shows on the scheme card.
Because the two are independent, an editor can tag an entry with a brand new concept and publish that entry right away. The publish validators re-check every assignment against the public projection and warn for each concept that is not published yet. They never block. Publish the scheme afterwards and the label appears on the site.
Reading taxonomies from the generated client
Code generation emits a TaxonomyRef<S> type, a Taxonomy projection type, and one concept-ID union per published scheme, so TaxonomyRef<'topics'> narrows its id. A taxonomy field always types as an array.
A scheme is one small file, so the client loads it once and answers every tree question in memory:
const topics = await client.getTaxonomy('topics');
conceptLabel(topics, 'volcanoes', 'is');
conceptPath(topics, 'volcanoes', 'en'); // ['Nature', 'Geology', 'Volcanoes']
ancestorsOf(topics, 'volcanoes'); // ['nature', 'geology']
descendantsOf(topics, 'nature'); // ['geology', 'glaciers', 'volcanoes']
childrenOf(topics, 'geology');
rootsOf(topics);Every helper is pure, synchronous, and guards against a malformed parent cycle. The fetch is cached under the client's cache policy and tagged blobify:taxonomy:{schemeId}.
findOne and findMany accept a taxonomy filter as one concept ID, a list of IDs, or a subtree expansion built by the taxonomy() helper:
const articles = await client.findMany(
'article',
{ topics: taxonomy('nature', { includeDescendants: true }) },
'en',
);Expansion happens from the loaded scheme, so a filtered query costs one summary manifest fetch, one shard fetch per model, and one scheme fetch. Items keep raw refs. Ask for labels explicitly with client.resolveTaxonomy(model, item, locale), which returns a per-field map annotated with label, path, ancestors, and missing, without mutating the item and without another fetch.
In the dashboard
Schemes live in the schema hub beside Models and Blocks. Opening one gives a concept tree you can search, reorder among siblings, reparent, and edit per locale using the locale switcher. The Edit tab holds the tree and the concept form. The Entries tab is the usage view: pick a concept and see which entries use it, per space, with a toggle for counting sub-concepts.
In the content list, every taxonomy field on the visible models becomes a filter. Filtering runs in memory over the draft summaries the list already holds, so it costs no request. The filter state lives in a repeatable tx URL parameter that names the field and the scheme, so a filtered view is shareable:
?tx=topics::subjects:geology,reykjanes sub-concepts included (the default)
?tx=topics::subjects:geology!exact that one concept onlyValidation keeps content
Exactly one taxonomy rule blocks a write. Everything else warns and keeps the entry usable.
| Rule | Outcome |
|---|---|
More concepts than the field's maxItems | Error, blocks the write |
| More than 50 assignments across all taxonomy fields on one entry | Warning, values kept |
Value not an array, malformed, wrong scheme, duplicated, or past multiple: false | Warning, value normalized or dropped |
| Concept ID absent from the draft scheme | Warning, ID kept |
Concept outside rootConcept or allowedConcepts | Warning, ID kept |
| Scheme cannot be loaded at all | Warning, every ID kept |
| Concept not in the published projection (publish only) | Warning, ID kept |
Normalization wraps a single value in an array, drops malformed or wrong-scheme values, removes duplicates while keeping order, and keeps only the first value when multiple is false.
Deleting concepts and schemes
Replacing a scheme replaces its whole concept map, so any stored ID the new map omits becomes a permanent tombstone. Dropping an ID is allowed and returns one warning per removed ID: existing assignments keep the ID and warn until an editor replaces them. Tombstoned IDs may never be reused, and a later write naming one is rejected.
Deleting a single concept is refused while it still has children, so reparent or remove those first. On success its ID is also pruned out of every other concept's related list.
Deleting a whole scheme returns 409 Conflict while any model field still names it, listing the offending model.field pairs. ?force=true deletes anyway and returns the same list as warnings. Those fields then point at a scheme that no longer exists, which becomes a warning on the next model save rather than an error, and the field is kept.
Limits
| Limit | Value |
|---|---|
| Schemes per organization | 20 |
| Concepts per organization | 6,000 |
| Concepts per scheme | 2,000 |
| Tombstones per scheme | 2,000 |
| Hierarchy depth | 5, roots at depth 1 |
| Parents per concept | 1 |
| Related concepts per concept | 20 |
| Alternate and hidden labels per locale | 10 each |
| Characters per label | 256 |
| Characters per definition or note | 2,000, with 10 notes per locale |
| Published projection per scheme | 5 MiB, checked at publish |
| Taxonomy fields per model | 10 |
| Assignments per entry | 50 recommended, warning only |
REST and MCP entry points
GET /v1/orgs/{orgId}/schemas/taxonomies
GET /v1/orgs/{orgId}/schemas/taxonomies/{schemeId}
PUT /v1/orgs/{orgId}/schemas/taxonomies/{schemeId}
DELETE /v1/orgs/{orgId}/schemas/taxonomies/{schemeId}
POST /v1/orgs/{orgId}/schemas/taxonomies/{schemeId}/publishReplacing an existing scheme requires the version token from the read, either as an If-Match header or as expectedEtag in the body. See Automation API for the exact preconditions and for taxonomies inside a schema import bundle.
Over MCP the tools are listTaxonomies, getTaxonomy, upsertTaxonomy, upsertConcept, deleteConcept, and publishTaxonomy. Reads are open to organization members, and every mutation needs developer or admin. Tagging content is not a taxonomy tool: write the field with patchFields, saveDraft, or bulkImport as an array of { scheme, id } objects. findContent matches a taxonomy field by concept ID and accepts includeDescendants. See MCP server.