Migrating legacy code
Legacy migrations work well when you treat them like a sequence of small, repeatable steps. Start by discovering patterns, then migrate in slices.
Identify migration patterns first
Run deterministic scans to find candidates and group them.
- id: list_candidates
run: rg "oldApiCall" src -l
- id: group_patterns
structured_prompt:
prompt: |
Group these files by migration pattern:
{{ outputs.list_candidates }}
schema:
type: object
required: [groups]
properties:
groups:
type: array
items:
type: object
required: [pattern, files]
properties:
pattern: { type: string }
files:
type: array
items: { type: string }
Generate a migration guide artifact
Use a prompt or agent to write a guide, then save it as an artifact so the rest of the workflow can reuse it.
Guides are useful for:
- codifying the new API or pattern
- listing constraints and exceptions
- defining test or verification steps
Migrate in slices
Use for_each to apply one pattern or file at a time. Keep instructions narrow and add constraints that prevent unrelated changes.
- name: Migrate one file
for_each:
from: '{{ outputs.group_patterns.groups[0].files | json_encode() }}'
agent:
extends: Coding
instructions: "Migrate {{ for_each.value }} to the new API."
constraints:
- "Do not change other files."
- "Keep public behavior the same."
Handle long runs
- Use
continue_on_error: trueon the migration step so you can collect failures. - Use task graphs if you want to branch to a recovery path.