Migrations with Agents
Migrations are the best-fit workload for agents — high volume, mechanical-looking, pattern-repetitive — and the workload where agents fail most quietly. A translated call site that compiles, passes lint, and is subtly wrong (different null semantics, different transaction boundary, different timezone handling) looks identical to a correct one in review. The playbook: build an exhaustive inventory before touching anything, choose a strategy deliberately, then migrate in small reviewed batches where a checklist document — not the agent's memory — is the single source of migration state.
Objective
Move every usage of the old framework/API/schema to the new one with zero semantic drift, in batches small enough to review honestly, resumable across any number of sessions.
When to use
- Framework upgrades (Express 4→5, React class→hooks, Vue 2→3, Python 2→3 remnants)
- Library swaps (Moment→date-fns, request→fetch, Sequelize→Prisma)
- Database migrations (MySQL→Postgres, raw SQL→ORM, schema restructures)
- Internal API version bumps with many call sites
If there are fewer than ~10 usage sites, skip this playbook and just do it as a feature-sized change. The overhead pays off from a few dozen sites up.
Inputs required
- Old and new API/framework documentation (or a written mapping of old→new idioms, including the semantic differences — not just the syntactic ones)
- A test suite you trust at least partially; where you don't, see Verification Strategy
- A decision on strategy (step 2) signed off by whoever owns the system
Step-by-step process
1. The inventory prompt — build the checklist doc first
Never let an agent start migrating from a grep it ran five minutes ago. Make the inventory a committed document, because it will be the migration's state machine.
We are migrating <old thing, e.g. Moment.js> to <new thing, e.g. date-fns> in
this repo. Do NOT migrate anything yet.
Build a complete inventory at docs/migrations/<name>-inventory.md:
1. Find EVERY usage site. Search by: direct imports, re-exports, type
references, string-based/dynamic references (config keys, DI tokens,
reflection, template usage), test fixtures, and build/tooling references.
List the exact search commands you ran so I can verify coverage.
2. Categorize each site by usage PATTERN (e.g. "format with locale",
"duration arithmetic", "timezone conversion"), not by file. Give each
pattern an ID (P1, P2, ...) and a one-line old→new translation rule.
3. Flag every pattern where old and new semantics differ (nulls, mutability,
timezone defaults, error behavior, lazy vs eager). Mark these NEEDS-REVIEW.
4. Emit the inventory as a checklist: one - [ ] item per file+pattern, grouped
into batches of 5-15 items, lowest-risk batches first. Shared utilities and
anything NEEDS-REVIEW go in their own late batches.
5. End with a count: total sites found, sites per pattern, and any place you
were UNSURE whether something is a usage site.Human review point: verify the inventory before batch 1. Run 2–3 of your own searches the agent didn't list (especially for dynamic usage: grep -r "moment" including strings, not just imports). Every miss you find now is a production bug you didn't ship later. If you find one, tell the agent and make it re-sweep with the failing search pattern added.
2. Choose the strategy
| Strategy | Choose when | Agent implication |
|---|---|---|
| Big-bang (one change, one deploy) | < ~50 sites, strong test suite, easy rollback, one repo | One session may suffice, but still batch the review; never batch the diff |
| Incremental (batches to main behind compat layer) | 50–1000s of sites, continuous delivery, weeks-long tail | The default. Checklist doc is mandatory — this outlives any session |
| Strangler (new code path grows beside old, traffic shifts gradually) | Semantics differ enough that translation is risky; DB/service migrations; need production validation per slice | Agent builds the new path from spec, not by translating the old; comparison harness catches drift |
Record the choice and why in the inventory doc's header — future sessions (and future you) need it. For the decision itself, see Decision Records.
3. Build the compat layer
For incremental and strangler strategies, have the agent build the seam first: a thin module exposing the new interface, implemented over the old system (or vice versa), so old and new code coexist on main for the whole migration. For a DB migration this is dual-write or a read-shim; for a library swap it's a wrapper module that becomes the only permitted import path.
Add an enforcement gate immediately — a lint rule or CI grep that fails on new direct imports of the old thing. Otherwise the codebase grows new legacy sites faster than you migrate old ones, and agents working on unrelated features are the worst offenders (they copy existing patterns). Wire it as a quality gate, and add one line to CLAUDE.md: "use src/lib/dates.ts, never import moment directly."
4. Migrate in batches — the batch prompt
One batch per session or per clean context. The checklist doc is read first and updated last, every time:
Read docs/migrations/<name>-inventory.md, including the strategy header and
the translation rules for each pattern.
Migrate ONLY the items in Batch <N>. Constraints:
- Apply the pattern's translation rule exactly. If a site doesn't actually
match its assigned pattern, STOP on that item, mark it in the doc as
MISCLASSIFIED with a note, and continue with the rest.
- Preserve semantics exactly: same null/undefined behavior, same error types,
same timezone/locale behavior, same lazy/eager evaluation. When old and new
semantics differ, add an explicit adaptation and a code comment "MIGRATION:
<why>".
- Touch nothing outside the batch's listed files. Do not reformat, rename,
restructure, or "improve" adjacent code, even code you just migrated around.
- Run <test command for affected areas> and fix only failures your changes caused.
- Update the inventory doc: check off completed items, note anything
surprising in a "Batch <N> notes" section (new sites discovered, semantic
landmines, rules that need refinement).Human review point, every batch: diff review before merge. You are not reviewing "did it change the syntax" — you're reviewing the NEEDS-REVIEW semantic flags and anything the agent marked MISCLASSIFIED or surprising. Batches are sized 5–15 items precisely so this review is honest instead of rubber-stamped. If a batch review finds a systematic error, fix the translation rule in the doc first, then re-check earlier batches for the same error — it's systematic by definition.
5. Verify per batch, then converge
Per batch: affected tests green, enforcement gate green, and one targeted behavioral check per NEEDS-REVIEW item (not just "it compiles" — actually exercise the differing semantic). For strangler migrations, run the comparison harness on real traffic samples before shifting share.
Convergence: when the checklist reaches all-checked, run the inventory searches one final time — the codebase moved while you migrated. New sites appear as a final batch. Then remove the compat layer (its removal is itself a mini-migration: inventory its importers first), keep the lint rule.
Expected artifacts
docs/migrations/<name>-inventory.md— checklist with strategy header, pattern translation rules, per-batch notes; the doc is the migration state and survives sessions, laptops, and personnel- Compat layer module + lint/CI rule blocking new legacy usage
- One reviewed PR per batch
- Final sweep confirming zero remaining sites
Common failures
| Symptom | Root cause | Early detection | Fix |
|---|---|---|---|
| Migrated code compiles and passes lint but behaves differently (off-by-one-day dates, empty string vs null, swallowed errors) | Silent semantic drift: agent translated syntax, not semantics | NEEDS-REVIEW flags in inventory; per-batch behavioral checks on differing semantics; strangler comparison harness | Fix the translation rule in the doc, then audit every already-checked item using that pattern |
| Production breaks in code the inventory never listed | Missed dynamic usages: string-keyed config, DI containers, reflection, templates, raw SQL in strings | Independent human searches at step 1; final convergence sweep; grep for the old thing's strings, not just imports | Add the failing search pattern to the inventory prompt, re-sweep, migrate as an emergency batch |
| Batch diff includes renames, reformats, and "modernized" code beyond the checklist | Agent "upgrades" adjacent code while it's in the neighborhood | Diff touches files or lines not in the batch's item list | Reject the batch, rerun with the touch-nothing constraint; never hand-salvage a mixed diff — see Architecture Erosion |
| Later batches contradict earlier ones (two different translations of the same pattern) | Translation rules lived in chat context instead of the doc; new session improvised | Rules in the doc are terse or missing; batch notes empty | Backfill explicit rules into the doc; grep for both translations, unify |
| Migration stalls at 80% for months | No enforcement gate, so new legacy sites appear as fast as old ones close | Checklist total grows between sessions | Land the lint rule before the next batch; add the convergence sweep to CI |
Recovery strategy
A bad batch is cheap by design: revert the single batch PR, mark its items unchecked with a note on what went wrong, fix the translation rule, rerun. If drift is discovered after several batches merged, don't hand-patch — the error is systematic: have the agent grep for the flawed translation's signature across all migrated files, list matches into the doc as a repair batch, and run the repair batch through the same review gate. If the whole approach was wrong (wrong target library, wrong compat seam), that's a strategy failure, not a batch failure — go to Recovering from Bad Agent Output and re-decide at step 2.
Acceptance criteria
- [ ] Inventory doc exists, was verified by independent human searches, and every item is checked
- [ ] Every NEEDS-REVIEW semantic difference has an explicit adaptation and a behavioral check
- [ ] No batch diff touched files outside its item list
- [ ] Lint/CI gate blocks new usage of the old system, and stays after the compat layer is removed
- [ ] Final convergence sweep found zero unmigrated sites
Related
- Legacy Refactor — the sibling playbook when you're restructuring rather than swapping
- Large Tasks — the general pattern behind "the doc is the state"; migrations are its highest-value instance
- Reviewing AI PRs — the per-batch review protocol in full
- Quality Gates — wiring the enforcement gate that stops backsliding
- Architecture Erosion — what unreviewed "upgrades of adjacent code" compound into
- Maintenance Prompts — more inventory and batch prompt variants