Studio Docs

When to Use

Change a live Content Type’s schema (add fields, rename, retype, remove, extract to Global Field) safely. Every change classified by risk. Safe changes (additive) applied directly, unsafe changes (rename, retype, remove) planned as dual-write + backfill + cutover. Dry-run mandatory before any live change. State file records every intended mutation so the run resumes on failure or reverts on demand.

Use when a live CT needs to evolve: field added post-launch, prop renamed to match new component signature, a group extracted into a reusable Global Field, a Reference retargeted from one CT to another. Phrases: “add a field to blog_post“, “rename title to headline“, “extract hero to a Global Field”, “change read_time from string to number”, “migrate CT schema”. Do NOT use for initial CT creation (that’s provision-studio-stack) or for content-only migrations (that’s import-content).

Auth preflight: settle the credential before the first API call. Resolve it OAuth-first per authenticate-cma: CS_OAUTH_ACCESS_TOKEN, else the Contentstack MCP’s stored session. Never ask the user for a session authtoken. If nothing resolves, or a refresh fails with 400 invalid_refresh_token, hand them ! CONTENTSTACK_REGION=<code> npx @contentstack/mcp --auth (it needs a TTY and a browser, so it cannot be run for them) and wait. 403 error_code 316 is a valid credential aimed at another org: fix the org or the api_key, do not re-authenticate.

Migrate a Content Type Schema: Dry-Run Planned, Dual-Write Executed, Rollback Documented

Verification Status

Core mechanics runtime-verified against a live stack. Some risk-table rows (destructive rename, incompatible retype) remain pattern-verified only. Always dry-run on a staging stack before production.

  • CMA endpoints (/v3/content_types/<uid>, entry backfill endpoints) match existing verified skills.
  • Retry + state-file pattern matches agent-idempotency reference implementation.
  • Safe additive (add optional field), dual-write pattern (add parallel field for rename), bulk entry backfill via PUT, and rollback via schema PUT are all runtime-verified end-to-end against a live Contentstack stack. 17/17 structural claims pass: schema shape changes, entries survive rollback with core field data intact. Reproduce with npx ts-node scripts/verify-migrate-ct-schema.ts (requires CS_API_KEY + CS_AUTH_TOKEN in .env).

Best practice: always dry-run on a staging stack with the exact CT + entries first. Never run a high-risk migration against production the first time. Incompatible-retype and in-place destructive renames are explicitly discouraged in the risk-classification table below in favour of the verified dual-write path. The 14-day cool-off is a suggested default. Tune to your release cadence.

Context

The three CT-schema failure modes an agent must not commit blind:

  1. Add a required field to a CT with entries. Live entries have no value, so Contentstack rejects the CT update. Silent failure or partial rollout.
  2. Rename a field. Entries still carry the old field name. Existing bindings on Sections point at the old name. The new name is empty. Live site breaks on first render.
  3. Retype a field. Existing values don’t match the new type, so Contentstack drops them silently or rejects the update. Content vanishes.

Safe changes (additive-optional, help-text edits, choice-option additions) can commit directly. Unsafe changes need a pattern of plan, dual-write, backfill, then cutover, documented per change class.

Risk Classification

ChangeRiskRequires
Add optional field (any type)SafeDirect commit + dry-run smoke test
Add required fieldMediumAdd as optional first, backfill all entries, then flip to required
Add option to a choice fieldSafeDirect commit
Remove option from a choice fieldMediumBackfill entries using the removed value first
Rename a fieldHighDual-write (new name plus old alias), backfill, update every binding, cut over, then remove the alias
Retype a field (compatible: single_line to multi_line)MediumBackfill probably-fine values. Validate no truncation
Retype a field (incompatible: text to number)HighAdd new field + backfill via transform + update bindings + remove old field
Remove a fieldHighBackfill any downstream dependency + update bindings + soft-delete first (rename to <field>_deprecated), then remove after N days
Extract a group to a Global FieldMediumCreate the GF, dual-write to both the CT group and the GF for one release, update bindings, then remove the CT group
Retarget a Reference fieldHighBackfill references (bulk update entries) + update bindings. May involve import-content if the target CT itself needs seeding

Task

Step 1: Read the intended change

The user says one of:

  • “Add a field read_time (number, optional) to blog_post.”
  • “Rename blog_post.title to blog_post.headline.”
  • “Extract blog_post.hero and product.hero into a Global Field.”

Parse: source CT + field(s) + intended mutation. Reject vague inputs: “clean up the schema” is not actionable. Ask for a specific change.

Step 2: Fetch the current CT schema

Use CMA /v3/content_types/<uid> (stack management token from env). Store the current schema locally as docs/_migration-state/<ct_uid>-before.json. This is the rollback artifact. Never proceed without a snapshot.

Step 3: Classify + emit the migration plan

Match the requested change against the risk table. Emit a plan:

MIGRATION PLAN · blog_post.title → blog_post.headline
Risk: 🔴 HIGH (field rename)

Current entries in blog_post: 342 (fetched via CMA /entries?count=true).

Plan:
  1. Add new field `headline` (single_line, optional) alongside `title` — safe, no downtime.
  2. Backfill: for every entry, copy `title` value → `headline`. Idempotent (skip entries where `headline` already populated).
  3. Update Section bindings: enumerate every Section currently binding `title`. Report before applying — user confirms.
  4. Cutover: switch bound Sections to bind `headline`. Republish affected compositions.
  5. Cool-off: leave `title` in place for N days (default 14) — rollback window.
  6. Remove `title` after cool-off + verify no remaining bindings.

Rollback: docs/_migration-state/blog_post-before.json restores pre-change schema. Compositions with bindings to `title` restored via git revert of the composition changes committed in Step 4.

Dry-run: no changes applied yet. Approve to proceed?

Wait for explicit approval. Never mutate the CT without user go-ahead.

Step 4: Execute the plan step-by-step, with checkpoints

Update docs/_migration-state/<ct_uid>-migration.json after every completed step:

{
  "ct_uid": "blog_post",
  "migration": "rename-title-to-headline",
  "steps": [
    { "id": "1-add-headline", "status": "completed", "at": "2026-..." },
    { "id": "2-backfill-headline", "status": "in-progress", "records_completed": 214, "records_total": 342 },
    { "id": "3-list-bindings", "status": "pending" },
    { "id": "4-cutover-bindings", "status": "pending" },
    { "id": "5-cooloff", "status": "pending", "cooloff_ends": "2026-..." },
    { "id": "6-remove-title", "status": "pending" }
  ]
}

Between destructive steps, pause and require explicit user go-ahead. Never remove a field automatically after the cool-off ends. Wait for the user to invoke the skill again with --continue --step=6-remove-title (or equivalent). This protects against accidental destruction if a rollback is silently in progress.

Step 5: Rollback support

At any point, the user can invoke:

  • “Roll back the title → headline migration.”

Skill reads the state file + the before.json snapshot, walks back the completed steps in reverse:

  • Step 6 completed: not applicable (already destroyed).
  • Steps 1-5 completed: revert bindings, remove the new field, unbackfill (if user wants, usually leaves headline values populated but bindings point back to title).

If Step 6 (remove old field) already ran, rollback is destructive. The data is gone. Report clearly + ask for confirmation before continuing.

Inputs Needed From the User

  1. The change (specific: field name, source CT, target state).
  2. Target stack UID + management token (from env or explicit).
  3. Explicit approval after the migration plan is emitted (dry-run before commit).
  4. Confirmation between destructive steps (backfill, cutover, then remove).
  5. Optional: cool-off period override (default 14 days).

Acceptance

  • Migration plan is emitted + explicitly approved before any CMA writes.
  • <ct>-before.json snapshot captured before any mutation.
  • <ct>-migration.json state file updated after every step.
  • Between destructive steps (cutover, remove), user is prompted. The skill does not auto-continue.
  • Every Section binding affected by the change is enumerated + updated. None left dangling.
  • Rollback path documented in the plan + executable via a follow-up skill invocation.
  • For medium/high-risk changes: cool-off period between backfill and destructive step.

Common Pitfalls

PitfallWhy it bitesFix
Direct field rename via CMA /v3/content_types/<uid> PUTCMA does rename the schema, but existing entries retain the old field name in _data, so new bindings return null and the live site breaksNever rename directly. Always dual-write via new field + backfill + cutover.
Skipping the <ct>-before.json snapshotRollback becomes hand-crafted. May miss field metadata (mandatory flags, unique constraints, display names)Snapshot mandatory. Never proceed without one.
Backfilling without idempotency checkRe-run overwrites in-progress author editsSkip records where the target field is already populated by an author.
Auto-continuing after cool-offOld field removed silently, so downstream systems still expecting it breakNever auto-continue destructive steps. Manual re-invocation required.
Not updating Section bindingsField renamed, Sections still bind the old name, so the live site shows nothingEnumerate every affected binding + update in the same run. Use discover-sections or grep composition JSON.
Treating “add required field” as safeLive entries have no value, so the CT update rejectsAdd as optional, backfill all entries, then flip to required. Three-step, not one.

See Also