Help us improve
Share bugs, ideas, or general feedback.
From contentful
Write and run Contentful content model migration scripts using the contentful-migration library and Contentful CLI. Covers content types, fields, validations, editor interfaces, layouts, sidebar widgets, entry transformations, tags, and annotations.
npx claudepluginhub contentful/skills --plugin contentfulHow this skill is triggered — by the user, by Claude, or both
Slash command
/contentful:contentful-migration [task description][task description]migrations/**This skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
The `contentful-migration` tool lets you describe and execute content model changes as code. Migrations are TypeScript scripts that create, edit, or delete content types, fields, editor interfaces, and entries.
Craft CMS 5 content modeling expert — sections, entry types, fields, Matrix, relations, project config, and content architecture. Use when planning content structure or configuring multi-site propagation.
Explains core Contentful concepts and routes users to the right implementation skill or documentation. Clarifies APIs (CDA/CPA/CMA/GraphQL) and Contentful MCP server.
Designs content type hierarchies, reusable parts, and field compositions for headless CMS using Type > Part > Field pattern. Covers composition vs inheritance and multi-channel reusability.
Share bugs, ideas, or general feedback.
The contentful-migration tool lets you describe and execute content model changes as code. Migrations are TypeScript scripts that create, edit, or delete content types, fields, editor interfaces, and entries.
Install:
npm install contentful-migration
GitHub: https://github.com/contentful/contentful-migration
This skill covers:
npx contentful space migration (Contentful CLI) and programmatic APIDo not run migrations with npx contentful-migration. Use contentful-cli for CLI execution, install it as a dev dependency when needed, and run via npx contentful ....
Not covered: SDK client setup (the contentful-nextjs skill), Contentful concepts and API routing (the contentful-guide skill).
https://www.contentful.com/developers/docs/tools/mcp-server/.contentful-migration scripts and contentful-cli for actual migration execution.Every migration file exports a function that receives a migration object:
import type { MigrationFunction } from 'contentful-migration'
const migration: MigrationFunction = (migration) => {
const blogPost = migration.createContentType('blogPost', {
name: 'Blog Post',
description: 'A blog post entry',
displayField: 'title',
})
blogPost.createField('title')
.name('Title')
.type('Symbol')
.required(true)
}
export = migration
The function also receives a context object as its second parameter, providing makeRequest (direct CMA access), spaceId, and accessToken. Use makeRequest when you need data not available through the migration API.
echo "=== Existing migrations ===" && ls migrations/ 2>/dev/null || echo "(no migrations/ directory found)"
echo ""
echo "=== Contentful env vars ===" && grep -h CONTENTFUL .env .env.local 2>/dev/null | sed 's/=.*/=<set>/' || echo "(no Contentful env vars found in .env or .env.local)"
When writing a migration:
.env file before proceeding.contentful environment create --name sandbox --source master.CONTENTFUL_SPACE_ID - Space ID. Find it in the Contentful web app URL (/spaces/<SPACE_ID>/...) or in Space settings -> API keys.CONTENTFUL_MANAGEMENT_ACCESS_TOKEN - CMA token used for migrations. Create it in Account settings -> CMA tokens (https://app.contentful.com/account/profile/cma_tokens) or from a space-scoped CMA tokens page (https://app.contentful.com/spaces/<SPACE_ID>/api/cma_tokens).CONTENTFUL_ENVIRONMENT_ID (optional) - Target environment ID (for example master or sandbox) when you want to avoid passing --environment-id.If any required value is missing, explicitly ask the user for the missing values and tell them where to find each one.
Create a content type:
const page = migration.createContentType('page', {
name: 'Page',
description: 'A generic page',
displayField: 'title',
})
Edit an existing content type:
const page = migration.editContentType('page')
page.description('Updated description')
page.displayField('internalName')
Delete a content type:
migration.deleteContentType('page')
Content type must have zero entries before deletion. Delete all entries first, or use transformEntriesToType to move them.
Create a field:
page.createField('title')
.name('Title')
.type('Symbol')
.required(true)
.localized(true)
Edit an existing field:
page.editField('title')
.name('Page Title')
.required(false)
Delete a field:
page.deleteField('legacyField')
Deleting a field permanently removes its content from all entries.
Change a field ID:
page.changeFieldId('oldName', 'newName')
Existing content is preserved — only the ID changes.
Move a field:
page.moveField('slug').afterField('title')
page.moveField('featured').toTheTop()
page.moveField('metadata').toTheBottom()
page.moveField('author').beforeField('publishDate')
| Type | Description | Extra config |
|---|---|---|
Symbol | Short text (max 256 chars) | — |
Text | Long text (max 50,000 chars) | — |
Integer | Whole number | — |
Number | Decimal number | — |
Date | ISO 8601 date/time | — |
Boolean | True/false | — |
Object | Arbitrary JSON | — |
Location | Lat/lon coordinates | — |
RichText | Structured rich text | enabledNodeTypes, enabledMarks validations |
Array | List of values or references | Requires items: { type, linkType?, validations? } |
Link | Single reference | Requires linkType: 'Asset' or 'Entry' |
ResourceLink | Cross-space reference | Requires allowedResources |
See API Reference — Field Types for full configuration details.
| Validation | Applies to | Example |
|---|---|---|
in | Symbol, Integer, Number | { in: ['draft', 'published', 'archived'] } |
unique | Symbol, Integer, Number | { unique: true } |
size | Array, Text, Symbol | { size: { min: 1, max: 5 } } |
range | Integer, Number | { range: { min: 0, max: 100 } } |
regexp | Symbol, Text | { regexp: { pattern: '^[a-z0-9-]+$' } } |
dateRange | Date | { dateRange: { min: '2020-01-01', max: '2030-12-31' } } |
linkContentType | Link, Array of Links | { linkContentType: ['author', 'organization'] } |
linkMimetypeGroup | Link (Asset) | { linkMimetypeGroup: ['image', 'video'] } |
assetFileSize | Link (Asset) | { assetFileSize: { min: 0, max: 5242880 } } |
assetImageDimensions | Link (Asset) | { assetImageDimensions: { width: { min: 100, max: 2000 } } } |
Apply validations via .validations([...]) on a field. See API Reference — Validations for all options.
Transform entries in place:
migration.transformEntries({
contentType: 'blogPost',
from: ['firstName', 'lastName'],
to: ['fullName'],
transformEntryForLocale: (fields, locale) => {
const first = fields.firstName[locale]
const last = fields.lastName[locale]
if (!first && !last) return
return { fullName: `${first || ''} ${last || ''}`.trim() }
},
})
Options: shouldPublish (true, false, or 'preserve' — default 'preserve').
Derive linked entries:
migration.deriveLinkedEntries({
contentType: 'blogPost',
derivedContentType: 'author',
from: ['authorName'],
toReferenceField: 'authorRef',
derivedFields: ['name'],
identityKey: (fields) =>
fields.authorName['en-US'].toLowerCase().replace(/\s+/g, '-'),
deriveEntryForLocale: (fields, locale) => {
if (locale !== 'en-US') return
return { name: fields.authorName[locale] }
},
})
This creates new author entries from existing blogPost.authorName data and links them via authorRef.
See Patterns — Transform Entries and Patterns — Derive Linked Entries for more examples.
Change the widget for a field:
const page = migration.editContentType('page')
page.changeFieldControl('slug', 'builtin', 'slugEditor', {
helpText: 'URL-friendly identifier',
trackingFieldId: 'title',
})
page.changeFieldControl('category', 'builtin', 'dropdown')
page.changeFieldControl('publishDate', 'builtin', 'datePicker', { format: 'dateonly' })
Widget namespaces: builtin, extension (UI extensions), app (custom apps).
See API Reference — Editor Interface for all built-in widgets and their settings.
001-create-blog-post.ts, 002-add-author-field.ts, 003-transform-categories.ts.shouldPublish: 'preserve' (the default) to maintain existing publish states during transforms.context.makeRequest sparingly — only when the migration API doesn't cover your use case.items on Array fields. type: 'Array' requires an items property specifying the element type.transformEntriesToType.linkType on Link fields. type: 'Link' requires linkType: 'Asset' or linkType: 'Entry'.--environment-id sandbox on the CLI.transformEntryForLocale is called for every locale — return undefined to skip.displayField to a non-Symbol field. The display field must be of type Symbol.