Skills
Skill 7 of 9
Write and run Contentful content model migration scripts using the contentful-migration library and the Contentful CLI.
4 minutes · 799 words · 16 sections
Install
npx skills add contentful/skills --skill contentful-migrationnpx skills add contentful/skills/plugin marketplace add contentful/skillsThe first command installs just this skill, by the name in its SKILL.md; the second installs the whole repository.
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-migrationGitHub: https://github.com/contentful/contentful-migration (opens in a new tab)
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 = migrationThe 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 (opens in a new tab) 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 (opens in a new tab) 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 (opens in a new tab) and Patterns — Derive Linked Entries (opens in a new tab) 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 (opens in a new tab) 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.Write and run Contentful content model migration scripts using the contentful-migration library and the Contentful CLI. Covers creating, editing, and deleting content types and fields, validations, editor interface configuration, editor layouts, sidebar widgets, entry transformations, tags, annotations, and the migration context object. Use when asked to write a migration, create or add a content type, add, rename, or delete fields, change or update a content model, transform entries, derive linked entries, configure editor controls, or run a migration script. Also triggers on "migration script", "contentful-migration", "schema migration", "content model migration", "field validation", "editor interface", "editor layout", "sidebar widget", "moveField", "changeFieldId", "rich text field", "reference field", "link field". Not for SDK client setup or Next.js integration (contentful-nextjs). Not for Contentful terminology or API routing (contentful-guide).
The verbatim description from this skill’s front matter — the string an agent matches on to decide whether to load it.
Bash(npxcontentful-migration*)contentfulspacemigrationmcp__contentful-mcp__*mcp__plugin_contentful_contentful-mcp__*skills/contentful-migration/SKILL.mdmain, last pushed 21 September 2026.SKILL.md, not by matching a directory convention. 3 distinct layouts observed: local-skills/skills/*/SKILL.md, skills/*/SKILL.md, skills/contentful-apps/*/SKILL.md.h1 and no skipped levels:.claude-plugin/marketplace.json by Contentful, declaring 1 plugin. It is read for editorial metadata only — never as the skill index, which is always the repository tree./contentful/skills.md, and each skill at its own .md URL.4 files · 38 KB
Everything this skill ships beside its prose. All of it is set here, as subchapters of skill 7.
Documentation the agent loads on demand, rather than up front.
Everything else published alongside the skill.