contentful-migration
Write and run Contentful content model migration scripts using the contentful-migration library and the Contentful CLI. Covers creating, editing, and deleting…
npx skills add https://github.com/contentful/skills --skill contentful-migrationContentful Migration
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
Scope
This skill covers:
- Content type and field CRUD operations
- Field types, validations, and editor interface configuration
- Entry transformations (in-place transforms, deriving linked entries, cross-type transforms)
- Tags, annotations, taxonomy validations
- Editor layouts, sidebar widgets
- Running migrations via
npx contentful space migration(Contentful CLI) and programmatic API
Do 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).
Contentful MCP note
- For users who want easier agent interaction with Contentful while planning or reviewing migrations, point them to the Contentful MCP server docs:
https://www.contentful.com/developers/docs/tools/mcp-server/. - Continue to use
contentful-migrationscripts andcontentful-clifor actual migration execution.
Migration Script Format
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.
Project state
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)"
Workflow
When writing a migration:
- Confirm required env vars first. If values are missing, ask the user to add them to a local
.envfile before proceeding. - Assess the change. Identify which content types and fields need to change. Check the current content model in the Contentful web app or via CMA.
- Write the migration script. Use the operations below. Prefer chaining over object notation — it gives better error messages with line numbers.
- Test in a sandbox environment. Never run untested migrations against production. Create a sandbox environment first:
contentful environment create --name sandbox --source master. - Run the migration. See Running Migrations for CLI and programmatic options.
- Verify. Check the content model in the web app. Confirm entries are intact.
Required environment variables
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 examplemasterorsandbox) 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.
Content Type Operations
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.
Field Operations
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')
Field Types Quick Reference
| 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.
Validations Quick Reference
| 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.
Entry Transformations
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.
Editor Interface
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.
Best Practices
- Number migration files sequentially:
001-create-blog-post.ts,002-add-author-field.ts,003-transform-categories.ts. - One logical change per migration. Easier to debug, revert, and review.
- Always test in a sandbox environment before running against production.
- Use
shouldPublish: 'preserve'(the default) to maintain existing publish states during transforms. - Prefer chaining over object notation — chaining gives line-level error messages.
- Split data transforms from schema changes. First migration changes the schema, second transforms data. This makes each step independently verifiable.
- Use
context.makeRequestsparingly — only when the migration API doesn't cover your use case.
Common Mistakes
- Forgetting
itemson Array fields.type: 'Array'requires anitemsproperty specifying the element type. - Deleting a content type with entries. You must delete all entries first, or move them with
transformEntriesToType. - Missing
linkTypeon Link fields.type: 'Link'requireslinkType: 'Asset'orlinkType: 'Entry'. - Running against master. Always test in a sandbox environment. Use
--environment-id sandboxon the CLI. - Not handling missing locales in transforms.
transformEntryForLocaleis called for every locale — returnundefinedto skip. - Setting
displayFieldto a non-Symbol field. The display field must be of typeSymbol.
References
- API Reference — complete migration API surface
- Patterns — common migration examples
- Running Migrations — CLI, programmatic API, CI/CD
Related Skills
- the contentful-guide skill — Contentful concepts, terminology, API routing
- the contentful-nextjs skill — Next.js integration with Contentful