> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://contentful.com/developers/docs/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://contentful.com/developers/docs/_mcp/server.

# Scripting migrations with the Contentful CLI

> Guide to evolving your applications using Contentful's CLI scripting migrations | Scripting model and content migrations

This tutorial details how to use the [Contentful CLI](https://github.com/contentful/contentful-cli/) to script changes to a content model and entries in a structured and reproducible way, for example when developing new features for an existing application.

## Requirements

* A (free) [Contentful account](https://www.contentful.com/sign-up/)
* Locally [installed](/tutorials/cli/installation) `contentful-cli`

## References

To learn more about merging content types changes, refer to the following resources:

* [Contentful Migrations CLI Deep Dive](https://contentful.wistia.com/medias/kkw7k4j7lp) — Watch the video with some hands-on examples on how to change content and content model with Contentful CLI.
* [Merge content type changes with Merge app](/tutorials/general/merge-app) — Follow the instructions to merge your content type changes from one environment to another with Merge app.

## Preparation step: Install the blog example application using the Contentful CLI

Run the guide and follow the steps until you have the app up and running on your workstation:

```bash
contentful guide
```

**Note:** Creation of a space may result in additional charges if the free spaces available in your [plan](https://www.contentful.com/pricing/?faq_category=payments\&faq=what-type-of-spaces-can-i-have#what-type-of-spaces-can-i-have/) are exhausted.

Navigate to the folder where you stored the blog application code, called `contentful-custom-app`, and do the following:

* Initialize a git repository which will be used to reflect changes to the application:

  ```bash
  git init
  git add .
  git commit -m 'Initial commit'
  ```

* Export the space ID that was created by the `contentful guide` command to make it reusable. Set the current space as the "active" space for development using the `contentful` CLI. This avoids adding the `--space-id` argument to every command (this command will add a line to the `.contentfulrc.json` file located in our your directory).

  ```bash
  export SPACE_ID=my-space-id
  contentful space use --space-id $SPACE_ID
  ```

* Create a sandbox environment – a full copy of your model and all the content in your space – ready for safe manipulation. To learn more about space environments, see [Managing multiple environments](/concepts/multiple-environments).

  ```bash
  contentful space environment create --environment-id 'dev' --name 'Development'
  ```

## Scripting model and content migrations

The initial content model for the blog app looks like this:
![migrations model](/developers/docs/_fern-files/contentful.docs.buildwithfern.com/961a732c18685985a4209dea68acaea23d233fce10db283855de2130f8e96a3d/docs/assets/images/migrations-model-1.0.svg)

Our first change will add a category to field to our blog-post so it can be displayed on the main page of the blog:
![migrations model with category example](/developers/docs/_fern-files/contentful.docs.buildwithfern.com/666d7b2661c114765c51441b4df743f4985ce278a5713fe9dc344d3fad30bdcd/docs/assets/images/migrations-model-1.1.svg)

## Adding a category field

Put all of your migrations in one location so it's easier for others to find them:

```bash
mkdir migrations
```

Use the following script to add the category field as a `Symbol`:

```javascript
module.exports = function (migration) {
  // Create a new category field in the blog post content type.
  const blogPost = migration.editContentType('blogPost');
  blogPost.createField('category')
    .name('Category')
    .type('Symbol');
}
```

Name the script `01-add-category-field.js`, save it in `migrations` and run it on the development environment in your space:

```bash
contentful space migration --environment-id 'dev' migrations/01-add-category-field.js
```

You can see the migration plan and agree or disagree to its execution.

### Initializing the blog-post categories

So far, existing blog post entries will not have any content in the `category` field.

If there are many existing blog post entries, the task of manually updating the category for each becomes unmanageable. Luckily, the `migration` object comes with functions that apply transformations to the content in entries.

For this blog post use case, use the `transformEntries` function to derive values for the recently created `category` field from the existing values in our `tags` field.

The [`transformEntries`](https://github.com/contentful/contentful-migration/blob/master/README.md#transformentriesconfig) function takes each entry for a content type, extracts the content from the specified source fields `from`, and applies a transformation function before populating values for the destination `to` fields.

Use the following script:

```javascript
module.exports = function (migration) {
  // Simplistic function deducing a category from a tag name.
  const categoryFromTags = (tagList) => {
    if (tagList.includes('javascript')) {
      return 'Development'
    }
    return 'General'
  }

  // Derives categories based on tags and links these back to blog post entries.
  migration.transformEntries({
    // Start from blog post's tags field
    contentType: 'blogPost',
    from: ['tags'],
    // We'll only create a category using a name for now.
    to: ['category'],
    transformEntryForLocale: async (from, locale) => {
      return {
        category: categoryFromTags(from.tags[locale])
      }
    }
  })
}
```

Name the script `02-transform-content.js`, save it in `migrations`, and run it on your space:

```bash
contentful space migration --environment-id 'dev' migrations/02-transform-content.js --yes
```

After the script executes, see the results in the Contentful web app:

```bash
open https://app.contentful.com/spaces/$SPACE_ID/entries/environments/dev/entries/2PtC9h1YqIA6kaUaIsWEQ0
```

The example app changes look like this:
![migrations transformed](/developers/docs/_fern-files/contentful.docs.buildwithfern.com/35b60f7110620013f3d5af754e4c893a9b0042fd6fb6159c2af74fb76626a14a/docs/assets/images/migrations-transformed-entry.png)

The `blogPost` entries have been updated with the category information computed using the tags of the post.

Next, version the changes in Git:

```bash
git checkout -b blog-v1.1
git add .
git commit -m 'Add category field to blog posts based on tags.'
```

### Displaying the category in the example app

You can now make changes to the code of the application to display the category alongside blog posts.

Download this patch

Use the patch to make all required changes on your code at once:

```bash
git apply migrations-1.0-1.1.patch
```

To see the changes, run the example app again:

```bash
npm run dev
```

The example app changes look like this:

![migrations blog](/developers/docs/_fern-img/07fc3a6f105bc36fc81e855903d18c062347c8412fed3780cbcc0f308519d187.webp)

Commit the changes in Git:

```bash
git add .
git commit -m 'Display blog-post category in the article-preview component.'
```

The branch is now ready for a pull request for other colleagues to review. This allows any developer to run a migration on their own environment and see how the change looks.

#### Merging changes to master

When you're ready to merge your code, you should execute your migration scripts against the `master` environment in your space. Our documentation on [multiple environments](/concepts/multiple-environments) and [continuous integration and deployment](/concepts/deployment-pipeline) further detail approaches for bringing changes from development environments to `master`.

## Transforming the category to a reference field

This section will explain how to update the example application to display the list of existing categories on the home page, and add a dedicated page which lists all blog posts that are part of a given category.

The update requires a new `category` content type with a URL slug.
To do this, create a migration script to transform the content model as follows:
![migrations model with new category](/developers/docs/_fern-files/contentful.docs.buildwithfern.com/bf754333734152bcdb3beb9d390e9c4a2f42f67bc78fdf2814f0baad760a4638/docs/assets/images/migrations-model-1.2.svg)

It also requires the creation of categories using the existing blog post information so that the updated home page looks like this:
![migrations blog updated example](/developers/docs/_fern-img/40727afb615120116b4cf1d24e76a67418e774d99831379b227acfb3662d5576.webp)

This approach implements the forward-only migration principle explained in our ["Infrastructure as code"](https://www.contentful.com/r/knowledgebase/cms-as-code/) article.

### Initializing a new branch

Create a separate branch to make these changes:

```bash
git checkout -b blog-v1.2
```

### Migrating the content

The following migration script uses a function called [`deriveLinkedEntries`](https://github.com/contentful/contentful-migration/blob/master/README.md#derivelinkedentriesconfig) to generate new categories from existing blog posts by using the original blog post category field:

```javascript
module.exports = function (migration) {
  // New category content type.
  const category = migration.createContentType('category')
    .name('Category')
    .displayField('name');
  category.createField('name').type('Symbol').required(true).name('Name');
  category.createField('slug').type('Symbol').required(true).name('URL Slug').validations([{ "unique": true }]);
  category.createField('image').type('Link').linkType('Asset').name('Image');

  // Create a new category field in the blog post content type.
  const blogPost = migration.editContentType('blogPost')
  blogPost.createField('category_ref')  // Using a temporary id to be able to transform entries.
    .name('Category')
    .type('Link')
    .linkType('Entry')
    .validations([
      {
        "linkContentType": ['category']
      }
    ])

  // Derives categories based on the existing category Symbol, and links these back to blog post entries.
  migration.deriveLinkedEntries({
    // Start from blog post's category field
    contentType: 'blogPost',
    from: ['category'],
    // This is the field we created above, which will hold the link to the derived category entries.
    toReferenceField: 'category_ref',
    // The new entries to create are of type 'category'.
    derivedContentType: 'category',
    // We'll only create a category using a name and a slug for now.
    derivedFields: ['name', 'slug'],
    identityKey: async (from) => {
      // The category name will be used as an identity key.
      return from.category['en-US'].toLowerCase()
    },
    deriveEntryForLocale: async (from, locale) => {
      // The structure represents the resulting category entry with the 2 fields mentioned in the `derivedFields` property.
      return {
        name: from.category[locale],
        slug: from.category[locale].toLowerCase()
      }
    }
  })

  // Disable the old field for now so editors will not see it.
  blogPost.editField('category').disabled(true)
}
```

Name the script `03-category-link.js`, save it in `migrations` and run it on your space:

```bash
contentful space migration migrations/03-category-link.js --yes
```

### Updating the code

Download this patch

```bash
git apply migrations-1.1-1.2.patch
```

To see the changes, run the example app again:

```bash
npm run dev
```

The new version changes look like this:

![migrations category page](/developers/docs/_fern-img/1eace047b96634162d986c42ecf64440715a702f09993bb5566799db2c6fd59c.webp)

Commit the changes to Git:

```bash
git add .
git commit -m 'v2: Category to reference with dedicated page'
```

The change is now ready to be reviewed and deployed.

## Next steps

* [Check out the documentation in the GitHub repository](https://github.com/contentful/contentful-cli/blob/master/docs/space/migration/)
* [Reference documentation of the migrations DSL for full details of its capabilities](https://github.com/contentful/contentful-migration/blob/master/README.md#reference-documentation)
* ["Infrastructure as code" article on the forward-only migration principle](https://www.contentful.com/r/knowledgebase/cms-as-code/)
* [Example video showing a hands-on content migration](https://contentful.wistia.com/medias/kkw7k4j7lp)
* [Read the Environments guide showing how to get started with Contentful and environments](/concepts/multiple-environments/)
* [Learn more about managing content at scale in our Learning Center](https://training.contentful.com/courses/running-content-operations-at-scale)