Contentstack CLI and content migration
Contentstack CLI and content migration
TL;DR
- Use csdx cm:stacks:export and csdx cm:stacks:import to replicate content model changes across stacks in a scriptable, auditable way.
- Always export assets alongside entries to avoid broken references in the target stack, and back up production before importing with --replace-existing.
- Wrap CLI export/import workflows in CI/CD pipelines for consistent, automated content model synchronization across a multi-brand portfolio.
Managing a multi-brand publishing platform through the Contentstack web interface works until it does not. When you operate three brands with separate stacks, each with their own content types, entries, and assets, clicking through the UI to replicate a content model change across all three stacks is slow, error-prone, and impossible to audit. The Contentstack CLI (csdx) turns these operations into repeatable, scriptable commands that belong in your development workflow alongside your code.
This lesson is hands-on. By the end, you should be able to install the CLI, authenticate, export content from one stack, import it into another, and understand how these operations fit into automated pipelines for a multi-brand content operation.
Installing the Contentstack CLI
The CLI is distributed as an npm package. Install it globally:
npm install -g @contentstack/cli
After installation, verify it works:
csdx --version
The CLI uses a plugin architecture. Core commands cover stack management, content export/import, and authentication. Additional plugins extend functionality for specific use cases. For a multi-brand publishing platform, the core commands handle the majority of migration work.
Authenticating with the CLI
Before running any stack operations, you need to authenticate. The CLI supports multiple authentication methods:
Interactive login
csdx auth:login
This opens a browser-based OAuth flow. After authenticating, the CLI stores your session credentials locally. This method works well for local development but is not suitable for CI/CD pipelines.
Token-based authentication
For automated workflows and CI/CD, add the management token as an alias, then reference that alias in commands:
csdx auth:tokens:add \ --alias "source-stack" \ --stack-api-key "blt_matrix_link_001" \ --management \ --token "cs1234567890abcdef" \ --yes csdx cm:stacks:export \ --alias "source-stack" \ --data-dir ./export-data
Management tokens are created in the Contentstack dashboard under Settings > Tokens. Each token can be scoped to specific permissions, which matters when running migrations across a multi-brand portfolio where different teams own different stacks.
Listing and managing tokens
# List stored tokens csdx auth:tokens # Add a named token for a specific stack csdx auth:tokens:add \ --alias "brand-alpha-dev" \ --stack-api-key "blt_alpha_key" \ --management --token "cs_alpha_mgmt_token" \ --yes # Remove a stored token csdx auth:tokens:remove --alias "brand-alpha-dev"
Token aliases simplify repeated operations. For a publishing platform managing Brand Alpha, Brand Beta, and Brand Gamma, you might configure:
csdx auth:tokens:add --alias "alpha-prod" --stack-api-key "$NEXT_PUBLIC_CONTENTSTACK_ALPHA_API_KEY" --management --token "$NEXT_PUBLIC_CONTENTSTACK_ALPHA_MANAGEMENT_TOKEN" --yes csdx auth:tokens:add --alias "beta-prod" --stack-api-key "$NEXT_PUBLIC_CONTENTSTACK_BETA_API_KEY" --management --token "$NEXT_PUBLIC_CONTENTSTACK_BETA_MANAGEMENT_TOKEN" --yes csdx auth:tokens:add --alias "gamma-prod" --stack-api-key "$NEXT_PUBLIC_CONTENTSTACK_GAMMA_API_KEY" --management --token "$NEXT_PUBLIC_CONTENTSTACK_GAMMA_MANAGEMENT_TOKEN" --yes
Now every subsequent command can reference --alias alpha-prod instead of passing raw keys.
Key CLI commands for content operations
Seeding a new stack
When launching a new brand in the publishing platform, you often want to start from a known content model rather than building from scratch. The seed command creates a new stack from an existing template:
csdx cm:stacks:seed \ --repo "contentstack/stack-starter-app" \ --stack-name "Brand Delta - Development"
This clones a predefined content model into a fresh stack. For a multi-brand platform, you might maintain a custom seed repository that contains your standardized product, product line, category, page, and header content types - the shared foundation that every brand starts from. The Veda kickstart seed is an example.
Exporting content
The export command extracts content from a stack into a local directory structure:
# Full stack export csdx cm:stacks:export \ --alias "alpha-prod" \ --data-dir ./exports/alpha # Export specific modules only csdx cm:stacks:export \ --alias "alpha-prod" \ --module content-types \ --data-dir ./exports/alpha-content-types # Export multiple specific modules csdx cm:stacks:export \ --alias "alpha-prod" \ --module content-types \ --module global-fields \ --module assets \ --data-dir ./exports/alpha-schema
The exported directory contains JSON files organized by module:
exports/alpha/
content-types/
product.json
product_line.json
category.json
header.json
entries/
product/
en-us/
blt_entry_1.json
blt_entry_2.json
product_line/
en-us/
blt_entry_3.json
assets/
blt_asset_1.json
blt_asset_2.json
global-fields/
seo_metadata.json
environments/
development.json
production.jsonThis file structure is both human-readable and machine-processable. You can inspect it, commit it to version control, modify individual files, and import the result into another stack.
Importing content
The import command pushes exported content into a target stack:
# Full import into a different stack csdx cm:stacks:import \ --alias "beta-prod" \ --data-dir ./exports/alpha # Import specific modules only csdx cm:stacks:import \ --alias "beta-prod" \ --module content-types \ --data-dir ./exports/alpha-content-types
Import respects dependencies: global fields are created before content types that reference them, and content types are created before entries that use them. However, the CLI cannot resolve every dependency automatically, particularly when entries reference other entries across content types that may not exist in the target stack.
Migrating content between stacks
The primary migration use case for a multi-brand publishing platform is replicating content model changes across brand stacks. When the platform team adds a new seo_metadata global field to the shared product model, that change needs to propagate to every brand stack.
Step-by-step migration workflow
- Make the change in the source stack (e.g., Add the social_links global field to Brand Alpha's development stack).
- Export the changed modules.
csdx cm:stacks:export \ --alias "alpha-dev" \ --module content-types \ --module global-fields \ --data-dir ./migration/social-links-update
- Review the export: inspect the JSON files to confirm only the intended changes are present. Remove any content type files that were not modified to avoid overwriting unchanged types in the target.
- Import into target stacks.
# Apply to Brand Beta csdx cm:stacks:import \ --alias "beta-dev" \ --module content-types \ --module global-fields \ --data-dir ./migration/social-links-update # Apply to Brand Gamma csdx cm:stacks:import \ --alias "gamma-dev" \ --module content-types \ --module global-fields \ --data-dir ./migration/social-links-update
- Verify in each target stack: confirm the global field and content type changes appear correctly in the Contentstack dashboard for each brand.
- Promote through environments: once verified in development stacks, repeat the import against staging and production stacks.
Handling conflicts
Import operations can encounter conflicts when the target stack already has a content type with the same UID but a different structure. The CLI provides flags to control behavior:
# Overwrite existing content types during import csdx cm:stacks:import \ --alias "beta-dev" \ --module content-types \ --data-dir ./migration/social-links-update \ --replace-existing
Without --replace-existing, the CLI skips content types that already exist. For schema migrations, you typically want to overwrite. For entry imports, the decision depends on whether you want to merge or replace.
Programmatic migrations using the CMA
For migrations that go beyond simple export/import - renaming fields, transforming data, backfilling values - you need programmatic scripts that use the Content Management API directly.
Consider a scenario where the Veda platform needs to migrate all product entries to include a new description_word_count field, calculated from the description text:
// scripts/backfill-description-word-count.ts
// Veda: backfill description_word_count across all products
import contentstack from "@contentstack/management";
const client = contentstack.client({
host: "https://api.contentstack.io",
});
async function backfillDescriptionWordCount(stackApiKey: string, managementToken: string) {
const stack = client.stack({
api_key: stackApiKey,
management_token: managementToken,
});
// Fetch all product entries
const products = await stack
.contentType("product")
.entry()
.query({ include_count: true })
.find();
console.log(`Processing ${products.items.length} products...`);
for (const product of products.items) {
const descText = product.description || product.short_description || "";
const wordCount = descText.split(/\s+/).filter(Boolean).length;
product.description_word_count = wordCount;
try {
await product.update();
console.log(`Updated "${product.title}" → ${wordCount} words`);
} catch (err: unknown) {
console.error(`Failed to update "${product.title}": ${(err as Error).message}`);
}
}
}
// Run across all brand stacks
async function main() {
const brands = [
{ name: "Alpha", apiKey: process.env.NEXT_PUBLIC_CONTENTSTACK_ALPHA_API_KEY!, token: process.env.NEXT_PUBLIC_CONTENTSTACK_ALPHA_MANAGEMENT_TOKEN! },
{ name: "Beta", apiKey: process.env.NEXT_PUBLIC_CONTENTSTACK_BETA_API_KEY!, token: process.env.NEXT_PUBLIC_CONTENTSTACK_BETA_MANAGEMENT_TOKEN! },
{ name: "Gamma", apiKey: process.env.NEXT_PUBLIC_CONTENTSTACK_GAMMA_API_KEY!, token: process.env.NEXT_PUBLIC_CONTENTSTACK_GAMMA_MANAGEMENT_TOKEN! },
];
for (const brand of brands) {
console.log(`\n--- Backfilling ${brand.name} ---`);
await backfillDescriptionWordCount(brand.apiKey, brand.token);
}
}
main().catch(console.error);Run the script with:
npx tsx scripts/backfill-reading-time.ts
Programmatic migrations give you full control over transformation logic, error handling, and execution order. They complement CLI export/import for cases where raw JSON file manipulation is insufficient.
Using the CLI in CI/CD pipelines
For a multi-brand publishing platform, content model consistency across stacks should not depend on manual CLI runs. Automate it.
# .github/workflows/content-model-sync.yml
name: Sync Content Model Across Brands
on:
workflow_dispatch:
inputs:
source_alias:
description: "Source stack alias (e.g., alpha-dev)"
required: true
modules:
description: "Modules to sync (e.g., content-types,global-fields)"
required: true
default: "content-types,global-fields"
jobs:
export:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Contentstack CLI
run: npm install -g @contentstack/cli
- name: Configure source token
run: |
csdx auth:tokens:add \
--alias source \
--stack-api-key ${{ secrets.SOURCE_API_KEY }} \
--management --token ${{ secrets.SOURCE_MGMT_TOKEN }} \
--yes
- name: Export from source
run: |
IFS=',' read -ra MODULES <<< "${{ inputs.modules }}"
MODULE_FLAGS=""
for mod in "${MODULES[@]}"; do
MODULE_FLAGS="$MODULE_FLAGS --module $mod"
done
csdx cm:stacks:export --alias source $MODULE_FLAGS --data-dir ./export-data
- name: Upload export artifact
uses: actions/upload-artifact@v4
with:
name: content-model-export
path: ./export-data
import:
needs: export
runs-on: ubuntu-latest
strategy:
matrix:
brand: [beta, gamma]
steps:
- name: Install Contentstack CLI
run: npm install -g @contentstack/cli
- name: Download export artifact
uses: actions/download-artifact@v4
with:
name: content-model-export
path: ./export-data
- name: Configure target token
run: |
csdx auth:tokens:add \
--alias target \
--stack-api-key ${{ secrets[format('{0}_API_KEY', matrix.brand)] }} \
--management --token ${{ secrets[format('{0}_MGMT_TOKEN', matrix.brand)] }} \
--yes
- name: Import to target
run: |
IFS=',' read -ra MODULES <<< "${{ inputs.modules }}"
MODULE_FLAGS=""
for mod in "${MODULES[@]}"; do
MODULE_FLAGS="$MODULE_FLAGS --module $mod"
done
csdx cm:stacks:import --alias target $MODULE_FLAGS --data-dir ./export-data --replace-existingThis workflows exports the content model from one stack and fans out imports across brand stacks in parallel. The workflow_dispatch trigger lets the platform team run it on demand with specific parameters, and the matrix strategy scales to any number of brands.
Exercise: export content types from one stack and import into another
Put the concepts together in a practical exercise.
Prerequisites: Two Contentstack stacks (or two environments within the same organization) with management tokens configured.
Steps:
- Install the CLI and authenticate:
npm install -g @contentstack/cli csdx auth:tokens:add --alias "source" --stack-api-key "$SOURCE_KEY" --management --token "$SOURCE_TOKEN" --yes csdx auth:tokens:add --alias "target" --stack-api-key "$TARGET_KEY" --management --token "$TARGET_TOKEN" --yes
- Export content types and global fields from the source stack:
csdx cm:stacks:export --alias "source" --module content-types --module global-fields --data-dir ./exercise-export
- Inspect the exported files:
ls -la ./exercise-export/content-types/ # You should see JSON files for each content type
- Import into the target stack:
csdx cm:stacks:import --alias "target" --module content-types --module global-fields --data-dir ./exercise-export
- Open the target stack in Contentstack and verify the content types match the source.
- Modify a content type in the source stack (add a field), re-export, and re-import with --replace-existing to practice incremental migration.
Expected outcome: The target stack contains identical content types to the source, including any modifications applied in step 6.
Common mistakes
Forgetting to export assets
Entries often reference assets (images, documents, videos). Exporting entries without their assets produces an import that contains broken asset references. When migrating entries across stacks, always include the assets module in your export, or confirm that referenced assets already exist in the target stack.
# Correct: export entries WITH assets csdx cm:stacks:export \ --alias "source" \ --module entries \ --module assets \ --data-dir ./full-migration
Ignoring reference integrity
Entries reference other entries by UID. When you import entries into a stack that does not contain the referenced entries, those references break silently: the entry imports successfully, but the reference field points to a non-existent UID. Always import referenced content types and their entries before importing the entries that reference them. For Veda where products reference categories and product lines, import categories and product lines first, then products.
Common pitfall:
Running --replace-existing imports against a production stack without first exporting a backup leaves you with no rollback path if the migration produces unexpected results.
Running migrations without a backup
Before running any import with --replace-existing against a production stack, export the current state first. This gives you a reliable rollback path if the migration produces unexpected results:
# Backup before migrating csdx cm:stacks:export --alias "prod" --data-dir ./backup/$(date +%Y%m%d) # Then run the migration csdx cm:stacks:import --alias "prod" --data-dir ./migration-data --replace-existing
Hardcoding stack credentials in scripts
Migration scripts that contain raw API keys and management tokens are security liabilities. Use environment variables or the CLI's token alias system. Never commit credentials to version control, even in "internal" repositories.
Assuming export/import is atomic
The CLI processes modules sequentially and entries individually. A network interruption mid-import can leave the target stack in a partial state. For large migrations, consider breaking the operation into smaller batches and tracking progress with explicit logging to redirect success/failure statuses for post-migration verification:
csdx cm:stacks:import --alias "target" --data-dir ./migration-data 2>&1 | tee migration-log.txt
Summary
The Contentstack CLI transforms content operations from manual, click-heavy processes into scriptable, auditable commands. For a multi-brand publishing platform, the CLI is essential: exporting content models from a reference stack, importing them across brand stacks, seeding new brand stacks from templates, and running programmatic migrations for data transformations that go beyond structural changes.
The export/import workflow follows a clear pattern: export modules from the source, inspect and optionally modify the JSON files, import into targets. Wrapping this pattern in CI/CD automation ensures consistency across a growing portfolio of brands without relying on manual coordination. The key operational discipline is treating migrations like database migrations in application development: plan them, test them against non-production stacks, back up before applying to production, and log everything.