---
name: cosmic-content-modeling
description: Design Cosmic CMS object types and metafields - choosing metafield types, validation rules, relationships, repeaters, rich-text fields, and conditional visibility. Use when creating or changing a Cosmic content model or schema, when the user asks what field type to use, when adding fields to an existing object type, when modeling relationships between content, or when a metafield value renders incorrectly in the UI.
license: MIT
---

# Cosmic content modeling

Object types define the shape of content in [Cosmic](https://www.cosmicjs.com), 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

```typescript
// 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 |

```typescript
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`:

```typescript
{ 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.

- **`select`** returns a plain `string`. No helper needed.
- **`multi-select`** returns a `string[]`. 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.

```tsx
// 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`):

```typescript
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:

```tsx
// 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

1. **Slug is the contract.** Renaming an object type slug or a metafield key
   breaks every query and template that references it. Choose deliberately.
2. **Prefer `select` over `select-dropdown`** and `rich-text` over
   `html-textarea` in new models. Both legacy types still work but return
   awkward shapes.
3. **Reach for `repeater` before a `json` field.** A repeater stays editable in
   the dashboard; a raw JSON blob does not.
4. **Use `parent` to group related fields** rather than prefixing keys by hand.
5. **Put `required` only on fields that are genuinely required at creation time.**
   A required field with no default blocks programmatic object creation.
6. **`show_when` is top-level only.** Conditional visibility does not work inside
   repeater or parent groups.
7. **Schema changes need the write key**, so run them from server-side code or a
   local script, never from the browser.

## Resources

- [Documentation](https://www.cosmicjs.com/docs)
- [REST API reference](https://www.cosmicjs.com/docs/api)
- [MCP server](https://www.cosmicjs.com/mcp-server)
- [Discord community](https://discord.gg/cosmicjs)
