Cosmic content modeling
Object types define the shape of content in Cosmic, an
AI-powered headless CMS. Each object type owns a list of metafields, and every
object of that type carries values for them under metadata.
For reading and writing content once the model exists, see the cosmic-sdk skill.
Object types
// List all object types
const types = await cosmic.objectTypes.find()
// Get one object type
const blogType = await cosmic.objectTypes.findOne('posts')
// Create an object type with metafields
await cosmic.objectTypes.insertOne({
title: 'Blog Posts',
slug: 'posts',
singular: 'Post',
emoji: '📝',
metafields: [
{ title: 'Content', key: 'content', type: 'markdown', required: true },
{ title: 'Image', key: 'image', type: 'file', media_validation_type: 'image' },
{ title: 'Author', key: 'author', type: 'object', object_type: 'authors' },
{ title: 'Tags', key: 'tags', type: 'objects', object_type: 'tags' },
],
})The slug is the identifier used everywhere else. When creating objects you pass
type: 'posts', the slug, never the title.
Metafield types
| Type | Description | Value format |
|---|---|---|
text |
Single line text | string |
textarea |
Multi-line text | string |
markdown |
Markdown editor | string |
rich-text |
Rich text editor (preferred for long-form) | string (markdown + tokens) |
html-textarea |
Rich HTML editor (deprecated, use rich-text) |
string |
number |
Numeric value | number |
date |
Date picker | "YYYY-MM-DD" |
switch |
Boolean toggle | true / false |
select |
Single selection (preferred) | string |
multi-select |
Multiple selection | string[] |
select-dropdown |
Dropdown selection (deprecated, use select) |
{ key: string, value: string } |
radio-buttons |
Radio selection | string |
check-boxes |
Multiple selection | string[] |
file |
Single media | Media name |
files |
Multiple media | Media name[] |
object |
Single relation | Object id |
objects |
Multiple relations | Object id[] |
json |
JSON data | object |
color |
Color picker | "#hex" |
emoji |
Emoji picker | string (the emoji character) |
repeater |
Repeatable group | array |
parent |
Nested group | object |
Rich text
A rich-text value is markdown prose plus optional {{name /}} block tokens that
reference blocks already defined in the bucket. It may also embed an existing
object inline:
{{object type="type-slug" id="OBJECT_ID" slug="object-slug" /}}Only reference objects that already exist. Never invent ids. object and
objects are reserved block names.
Prefer rich-text over the deprecated html-textarea for new long-form fields.
Validation
Metafields support validation properties that enforce data quality at write time:
| Property | Type | Description |
|---|---|---|
required |
boolean |
A value must be provided |
unique |
boolean |
Value must be unique across all objects of the same type. Applies to top-level text, textarea, number, date, and select metafields. Not supported inside parent or repeater groups |
show_when |
object |
Conditional visibility: { key, op, value }. Shows the field when a sibling field matches. Ops: eq, neq, exists, not_exists. Hidden fields skip required validation. Top-level only |
regex |
string |
Restrict the value to a regular expression |
regex_message |
string |
Message shown when regex validation fails |
minlength |
number |
Minimum character length (text, textarea) |
maxlength |
number |
Maximum character length (text, textarea) |
min |
number |
Minimum value (number) |
max |
number |
Maximum value (number) |
helptext |
string |
Guidance shown to editors beneath the field |
await cosmic.objectTypes.insertOne({
title: 'Contacts',
slug: 'contacts',
metafields: [
{ title: 'Email', key: 'email', type: 'text', required: true, unique: true },
{ title: 'Name', key: 'name', type: 'text', required: true, minlength: 2 },
],
})Relationships
Model a relationship with object (one) or objects (many) and pin it to a
target type with object_type:
{ title: 'Author', key: 'author', type: 'object', object_type: 'authors' }
{ title: 'Tags', key: 'tags', type: 'objects', object_type: 'tags' }Values are object ids, not slugs. When reading, use .depth(1) or higher to
resolve the related object rather than getting back a bare id.
Select and multi-select values
For new content models, prefer select for single selection and multi-select
for multiple selection over the legacy select-dropdown type.
selectreturns a plainstring. No helper needed.multi-selectreturns astring[]. No helper needed.select-dropdown(deprecated) returns{ key, value }objects, which cause "Objects are not valid as a React child" errors when rendered directly in JSX.
// select and multi-select: use the values directly
<span>{product.metadata?.status}</span>
{product.metadata?.tags?.map((tag) => <span key={tag}>{tag}</span>)}If a project still has legacy select-dropdown fields, add this helper (for
example in lib/cosmic.ts):
export function getMetafieldValue(field: unknown): string {
if (field === null || field === undefined) return ''
if (typeof field === 'string') return field
if (typeof field === 'number' || typeof field === 'boolean') return String(field)
if (typeof field === 'object' && field !== null && 'value' in field) {
return String((field as { value: unknown }).value)
}
if (typeof field === 'object' && field !== null && 'key' in field) {
return String((field as { key: unknown }).key)
}
return ''
}The function passes strings, numbers, and booleans through unchanged, so it is safe to wrap any metadata value rendered in JSX:
// Wrong: crashes if the field is a select-dropdown
<span>{product.metadata?.category}</span>
// Correct: safe for legacy select-dropdown
<span>{getMetafieldValue(product.metadata?.category)}</span>Modeling guidance
- Slug is the contract. Renaming an object type slug or a metafield key breaks every query and template that references it. Choose deliberately.
- Prefer
selectoverselect-dropdownandrich-textoverhtml-textareain new models. Both legacy types still work but return awkward shapes. - Reach for
repeaterbefore ajsonfield. A repeater stays editable in the dashboard; a raw JSON blob does not. - Use
parentto group related fields rather than prefixing keys by hand. - Put
requiredonly on fields that are genuinely required at creation time. A required field with no default blocks programmatic object creation. show_whenis top-level only. Conditional visibility does not work inside repeater or parent groups.- Schema changes need the write key, so run them from server-side code or a local script, never from the browser.