TL;DR: Migration prompting AI well means treating a framework or language migration as a series of small, gated passes instead of one giant prompt. Scope each pass to a file or a pattern, feed the model the exact validation error and a working example, and verify with real tests before the next pass starts.
Ask a model to "migrate this codebase to the new framework" and it will happily produce a diff. The diff will compile. Half of it will also be wrong in ways that only show up at runtime, three weeks later, in a code path nobody re-ran the test suite against. That gap between "the model produced output" and "the output is correct" is the entire problem with migration prompting, and it is a different problem from ordinary prompt engineering. A bad marketing prompt gets rewritten. A bad migration gets merged, and the mistake ships to production wearing the shape of working code.
This post is a framework, not a magic incantation: how to chunk a migration so a model can actually reason about each piece, what a migration prompt should contain, and what to check between passes before you trust the next one. It leans on two verified, dated examples: TypeScript 7.0's July 2026 release, and Airbnb's own published account of migrating 3,500 test files with an LLM.
Why Can't You Just Prompt "Migrate This to X"?
Two things make migrations harder than a normal coding prompt, and both come from the same root cause: the model cannot fit your whole codebase inside its context window at once.
First, a real migration touches call sites the model was never shown. Rename a function's signature in one file and every caller elsewhere needs to change too, but a single-file prompt has no way to know those callers exist, let alone what they look like. Second, a model asked to fill a gap in its knowledge will fill it, whether or not the fill is accurate. Ask it to port an unfamiliar API and it will sometimes invent a plausible-looking method that does not exist, a hallucination dressed up as a working migration. Neither failure is visible in the diff. Both are visible in the test run, if you actually run one.
This is also why reading the codebase first matters more here than in almost any other prompting task. If you have not already scoped what actually depends on what, start there before you write a single migration prompt. A model that has to guess at the blast radius of a change will guess wrong at exactly the moment it matters.
The Chunking Strategy: Five Passes, Not One Prompt
The fix is not a smarter prompt. It is a smaller one, repeated with a gate in between. Treat a migration as a pipeline of narrow passes, each one small enough that a human can actually review what changed and why.
| Pass | What changes | What a human checks before the next pass |
|---|---|---|
| 1. Inventory and risk mapping | Nothing yet — list every file, pattern, or API call the migration touches | The list is complete, and the riskiest 10% is flagged for manual-first treatment |
| 2. Mechanical / syntax pass | One-to-one renames, import swaps, signature updates | The build still compiles and the diff contains no logic changes |
| 3. Semantic / behavior pass | Anything where old and new frameworks disagree on when or how something runs | Existing tests still pass, and new tests cover the behavior difference explicitly |
| 4. Test migration pass | Test files themselves, updated to the new testing approach | Test intent and coverage are unchanged, not just "tests are green" |
| 5. Integration / wiring pass | Config, build tooling, CI, anything that glues the pieces together | A full pipeline run, not a local build, since this is where silent breakage hides |
Each pass is a separate prompt, scoped narrowly enough that the model's output stays reviewable. A single sprawling prompt that tries to do all five at once collapses the same way a single giant pull request does: nobody can meaningfully review it, so nobody really does.
What Belongs in Each Pass?
A migration prompt earns its keep on four things: scope, context, constraints, and an explicit permission to stop.
Scope is one file, one pattern, or one directory, never "the codebase." Context is whatever the model actually needs to do that narrow job well: the file being changed, a sibling file already migrated in the house style, and, on a retry, the exact validation failure from the last attempt rather than a vague "fix the errors." Constraints state what must not move: no renamed exports, no new dependencies, no deleted test to make a red suite go green. And the model needs explicit permission to stop and flag uncertainty instead of guessing, because a model that always produces an answer will produce a wrong one exactly as confidently as a right one.
This mirrors real practice, not just a house theory. Refactoring legacy code with AI runs into the identical problem: the model needs the surrounding pattern, not just the target file, or it invents a style that does not match the rest of the codebase.
What Should the Migration Prompt Itself Contain?
Here is the shape, as a reusable template. Fill the placeholders per pass, per file.
ROLE: You are migrating ONE FILE from {{source_pattern}} to {{target_pattern}}.
Do not touch files outside SCOPE.
SCOPE: {{file_path}}
CONTEXT (read-only, do not modify):
- The code that calls or depends on this file: {{call_sites}}
- A sibling file already migrated, for house style: {{example_file}}
- The exact validation failure from the last attempt, verbatim: {{validation_error}}
CONSTRAINTS:
- Preserve existing behavior and test coverage. Do not delete a check to make it pass.
- Do not rename exported symbols.
- Do not add new dependencies.
- If the migration is not possible without changing code outside SCOPE,
STOP and say so instead of guessing.
OUTPUT:
1. The full migrated file.
2. A short list of anything you were not certain about.
Notice the last constraint. It is doing more work than everything above it. A model that stops and asks is annoying for exactly one migration. A model that guesses and stays silent about it is a production incident with your name on the commit. The fixed OUTPUT shape matters too: asking for the same two-part structured output every time, the file plus an uncertainty list, is what makes fifty passes skimmable instead of fifty different essays to parse by hand.
What Do Verification Gates Actually Catch?
A gate is a guardrail, not a formality: whatever you would run against a human's pull request, applied to the model's output with the same seriousness. The existing test suite, the linter, the type checker, and, where one exists, a behavioral diff rather than a purely syntactic one. The point of running it between passes rather than once at the end is that a gate catches drift while it is still cheap to fix, one file at a time, instead of after fifty files have compounded the same mistake.
The failure mode that a single end-of-migration test run misses is drift that cancels itself out at the aggregate level but not at the file level: one file silently drops a null check while another silently adds a redundant one, and the overall test count looks unchanged. Gating pass by pass is what catches that, because you are diffing fifty files' worth of change instead of five hundred.
Is a Version Upgrade Secretly a Migration Too?
Often, yes, and it is the migration people are least likely to review carefully, because it arrives disguised as a routine dependency bump. TypeScript 7.0, released July 8, 2026 according to Microsoft's own announcement, is a useful example: the release notes describe it as "a 10x faster native port of TypeScript", and among "the notable default changes to configuration" it lists plainly that "strict is true by default." A project whose tsconfig.json never set strict at all picks up strict null checks, strict function types, and the rest of the strict family the moment it upgrades, with no line in the diff to review.
That is a language-level migration wearing a version number. The same chunking discipline applies: don't bump the compiler across an entire monorepo in one pass and hope the CI run catches everything. Turn strict mode on module by module, or accept the new default one directory at a time, and gate each step the same way you would gate a hand-written framework port. If your team keeps a repo-level instruction file for how the codebase is supposed to work, this is exactly the kind of convention worth writing down there once rather than re-explaining to every new session; see what actually belongs in a CLAUDE.md if you have not set one up.
How Airbnb Migrated 3,500 Test Files With an LLM
Airbnb's engineering blog documents a real large-scale migration: nearly 3,500 React component test files moved from Enzyme to React Testing Library, using an LLM to do most of the rewriting rather than a person doing it file by file. The shape of what worked matches the framework above almost exactly, which is a large part of why it is worth citing rather than inventing a hypothetical.
Their pipeline broke the migration into per-file validation and refactor steps, modeled as a state machine: a file only advanced once it passed the previous stage. Each retry prompt carried the source of the component under test, the file being migrated, the specific validation failures from the last attempt, and related tests from the same directory to preserve team-specific patterns, the same scope-plus-context-plus-constraint shape covered above. The result, in their own numbers: 75% of target files migrated automatically in four hours. Closing the rest took what the team calls a "sample, tune, and sweep" loop, run for four more days, to reach 97%. The final few percent, files that kept failing after dozens of retries, went to manual work instead of more automation.
The team's own framing of success is worth repeating exactly, because it is the standard this whole post argues for: the goal was not a green test run, it was that the migration preserved "original test intent" and "code coverage", verified explicitly rather than assumed from a passing build. That verification step functioned as a code review gate in everything but name, the same discipline covered in how to prompt for a genuinely useful code review.
Where Do Vendors Draw the Verification Line?
Enterprise migration tooling reaches the same conclusion by a different route. AWS Transform describes itself as "a collaborative enterprise IT transformation workbench powered by expert agents that accelerates cloud migration, application modernization, and continuous tech debt reduction", covering .NET modernization, mainframe COBOL-to-Java conversion, and VMware workload migration, claiming to modernize legacy applications "up to 5x faster" than manual work. Its mainframe workflow does not hand back one finished rewrite. It runs assessment first, then a staged "reimagine" step that AWS says "deterministically maps business functions" and keeps every generated requirement "traceable back to the source code".
That is the chunking-and-gating pattern from this post, built into a product a large vendor is charging enterprises for. Nobody selling migration automation, including AWS, is selling a single prompt that rewrites a codebase unattended. Worth being honest here too: Prompt Architects does not run code migrations, and it should not be your first stop for one. What it is good for in this workflow is the boring, unglamorous part: keeping the per-pass prompt template above as a reusable, versioned prompt instead of retyping "scope, context, constraints, stop-and-ask" into a chat window fifty times across fifty passes.
The Part Nobody Re-Reads Is the Real Risk
Every guard rail in this post exists because of one plain fact: a language model does not see your entire codebase, and generated migrations do not earn trust from compiling, they earn it from tests that were run and actually checked, not assumed to have passed. The riskiest code in any AI-assisted migration is not the file that clearly needs careful review. It is the one that looks mechanical enough that the reviewer skims it, approves it, and moves on, exactly the file where a subtle behavior change hides best. Chunk the work small enough that skimming stops being tempting, gate every pass on a real test run, and treat a version bump with the same suspicion as a hand-written port, because the defaults changed whether anyone read the changelog or not.
Stop rewriting prompts. Start shipping.
Works with ChatGPT, Claude, Gemini, Grok, Midjourney, Ideogram, Veo3 & Kling. 4.8★ on the Chrome Web Store.
Create An Account