Plan and execute Angular upgrades and modernisation — `ng update` one major at a time, NgModule to standalone, `*ngIf`/`*ngFor` to built-in control flow, constructor DI to `inject()`, decorators to signal inputs, outputs and queries, eager to lazy routes, `CommonModule` and `ngClass`/`ngStyle` removal, zone.js to zoneless, Karma to Vitest, `@angular/animations` to `animate.enter`/`animate.leave`, and Reactive Forms to Signal Forms.
When agents use itUse this whenever the user is on Angular 19 or older, asks how to upgrade Angular or "modernise" an Angular codebase, mentions `ng update`, a migration schematic, `ng generate @angular/core:…`, a breaking change after an upgrade, removing zone.js, or getting off Karma, `RouterTestingModule` or `TestBed.flushEffects()`. Also use it when a new-feature request turns out to be blocked by an unsupported Angular version.
Install
Copilot (VS Code, Visual Studio, Copilot CLI and github.com) reads skills from the repository — commit them so the whole team gets them.
npx skills add AGCO-Global/org-skills --skill angular-migration -a github-copilot
# or with the org installer (adds .github/skills/angular-migration):
npx -y github:AGCO-Global/org-skills add skill angular-migration
Uses the open skills CLI. Works with Claude Code, Codex, Cursor, Copilot, Gemini CLI, OpenCode, Windsurf and 60+ others — it asks which agent to install into.
Installs the frontend-angular-skills plugin, which bundles all Frontend / Angular skills and keeps them updated.
/plugin marketplace add AGCO-Global/org-skills
/plugin install frontend-angular-skills@org-skills
# or just this skill, in this repository:
npx skills add AGCO-Global/org-skills --skill angular-migration -a claude-code
Use Download .zip above, then upload it under Customize → Skills → Upload skill. Team and Enterprise admins can sync this repo as a plugin marketplace instead.
Installs into .agents/skills/, which Codex reads.
npx skills add AGCO-Global/org-skills --skill angular-migration -a codex
Installs into Cursor's skills folder.
npx skills add AGCO-Global/org-skills --skill angular-migration -a cursor
Installs into Gemini CLI's skills folder.
npx skills add AGCO-Global/org-skills --skill angular-migration -a gemini-cli
Any tool with rules, instructions or custom prompts: use Copy SKILL.md above and paste it in. It is plain Markdown.
Commands use your normal git sign-in to GitHub, so they work while the repository is private. Node.js 20+ required.
Try it — example prompts
Prompts this skill is tested against, and what a good answer includes.
Give me the exact commands to migrate our Angular 16 NgModule app to standalone + signals.
ng update one major at a time up to a supported version firstng generate @angular/core:standalone run in its three modes in orderng generate @angular/core:control-flowng generate @angular/core:injectng generate @angular/core:signal-input-migrationng generate @angular/core:output-migrationng generate @angular/core:signal-queries-migrationng generate @angular/core:route-lazy-loadingone PR per schematic with tests green between steps
We're on Angular 19. Is that fine to stay on for another year?
v19 is out of support and receives no fixes including security fixesnames v22 as Active and v21 and v20 as LTS with end datesupgrade plan is the first deliverableone major at a time via ng update
We removed zone.js and now our third-party datepicker doesn't update the view until I click elsewhere.
explains that zone.js patching no longer triggers change detectionlists what notifies Angular in zoneless mode (signal write, template or host listener, markForCheck, setInput)fix is to write a signal or call markForCheck in the widget callbackdoes not recommend restoring zone.js
How do we get off @angular/animations? We use trigger/state/transition for a slide-in panel.
@angular/animations is deprecatedanimate.enter and animate.leave with a CSS classclass binding plus a CSS transition for state changesremove provideAnimations / provideNoopAnimationsprefers-reduced-motion handled
Our Angular 22 build fails with 'afterRender is not exported from @angular/core'. What happened?
afterRender was renamed afterEveryRender in v20 with no aliasafterNextRender for a one-offphases are object keys, the AfterRenderPhase enum is goneshows the corrected call
A migration succeeds when every step is small, mechanical and independently shippable, and CI is green after each one. Angular ships schematics for almost all of this work — running the schematic and reviewing its diff beats a hand-rewrite on both speed and accuracy. The failure mode is the "big modernisation branch" that is six weeks old, conflicts with everything and can never be merged.
1. Decide first
Question
Default
Change when
Upgrade or modernise first?
Upgrade to a supported major first; modernisation schematics assume current APIs
A v22 codebase already: skip to section 4
How many majors at once
One at a time, ng update per major, commit and deploy between them
Never batch. Skipping a major skips its migrations and its deprecation warnings
Order of modernisation
Standalone → control flow → inject() → signal inputs/outputs/queries → lazy routes → cleanup
A single blocker (a library with no standalone support) can reorder, but standalone gates most of the rest
Branch strategy
One PR per schematic, straight to trunk, behind no flag
A schematic touching > 300 files → split with --path per feature folder
Test suite during the move
Keep Karma running until the app is on v21+ and standalone
The suite is already the bottleneck → move to Vitest first, it is independent of the rest
Hand edits after a schematic
Review the diff, fix what it flagged, commit as one change
Never hand-edit the same file the next schematic will rewrite
Below v20
The upgrade plan is the deliverable, before any feature work
Only a hotfix is in scope
Version status as of 2026-09: v22 Active (released 2026-06-03, LTS ends 2028-06), v21 LTS (ends 2027-06), v20 LTS (ends 2026-11-28). v19 and below get no fixes, including security fixes.
2. Upgrading majors
ng update # shows what is available for this workspace
ng update @angular/core@21 @angular/cli@21 # one major, core and cli together
npm run test && npm run build # green before the next major
ng update runs that major's own migrations automatically. That is why skipping majors is expensive: the migrations do not run retroactively.
Check https://angular.dev/update-guide for each hop with your exact from/to versions before starting; it lists the manual steps a schematic cannot do.
Third-party packages (@angular/material, @ngrx/*, @angular/cdk) are updated in the same command as the core hop they support, not after.
A library with no release for the target major is a decision, not a blocker to discover in week three: replace it, fork it, or stay one major back — but decide before the first ng update.
Commit each major separately. A bisect across a combined 19→22 commit tells you nothing.
3. What changed per version
Per-version defaults — zoneless (v21), OnPush (v22), Vitest (v21), Signal Forms, resource/httpResource and Angular Aria (v22), incremental hydration by default (v22) — are tabulated in version-notes.md in ../angular-development/references/. Read it before generating any code for a mixed-version repo.
The API changes most likely to break a build or mislead a generated fix: afterRender was renamed afterEveryRender in v20 with no alias; TestBed.flushEffects() is deprecated in favour of TestBed.tick(); withIncrementalHydration() is deprecated (incremental hydration is on by default in provideClientHydration()) and it enables event replay, so withEventReplay() should be dropped; @angular/animations is deprecated; ChangeDetectionStrategy.Default is deprecated in favour of Eager.
4. The modernisation schematics, in order
Run one, review the diff, run the tests, commit. Every command takes --path <dir> to scope it to one feature folder.
ng generate @angular/core:standalone # 3 modes, in this order:
# 1 convert components/directives/pipes to standalone
# 2 remove unnecessary NgModule classes
# 3 bootstrap with standalone APIs
ng generate @angular/core:control-flow # *ngIf/*ngFor/*ngSwitch → @if/@for/@switch
ng generate @angular/core:inject # constructor DI → inject()
ng generate @angular/core:signal-input-migration # @Input → input()
ng generate @angular/core:output-migration # @Output → output()
ng generate @angular/core:signal-queries-migration # @ViewChild/@ContentChild → viewChild()/contentChild()
ng generate @angular/core:route-lazy-loading # eager route components → loadComponent
ng generate @angular/core:common-to-standalone # CommonModule → the directives/pipes actually used
ng generate @angular/core:ngclass-to-class # ngClass → [class] bindings
ng generate @angular/core:ngstyle-to-style # ngStyle → [style] bindings
ng generate @angular/core:cleanup-unused-imports # drop imports the template no longer uses
ng generate @angular/core:self-closing-tag # <x></x> → <x />
ng generate @angular/core:router-testing-module-migration # RouterTestingModule → RouterModule + provideLocationMocks()
Note the singular in self-closing-tag, and that the inject() schematic is :inject — :inject-migration does not exist.
:inject takes --migrate-abstract-classes (off by default, because Angular does not validate abstract-class parameters), --backwards-compatible-constructors and --non-null-assertion. :ngstyle-to-style has --best-effort-mode for object-reference bindings it would otherwise skip as unsafe.
Review every diff. The schematics are conservative and correct on what they touch, but they leave the judgement calls: a converted component may now import more than it needs, and :control-flow cannot know that an *ngFor should have gained an @empty branch.
5. The moves with no schematic
Four migrations are manual. Each is independently shippable, and each has a decision rather than a mechanical rule — details and step-by-step recipes in references/manual-migrations.md.
zone.js → zoneless. Add provideZonelessChangeDetection() on v20, or confirm nothing provides provideZoneChangeDetection() on v21+, remove zone.js from polyfills, then fix what stops updating: anything relying on zone patching now needs a signal write, a template/host listener, markForCheck() or ComponentRef.setInput. NgZone.runOutsideAngular calls become no-ops and should be deleted.
Karma → Vitest.npm install --save-dev vitest jsdom, set the test builder to @angular/build:unit-test, then ng g @schematics/angular:refactor-jasmine-vitest to convert spec syntax. It does not install dependencies, change angular.json or delete karma.conf.js — you do those. fakeAsync/tick stop working once zone.js is gone; replace with fake timers or await fixture.whenStable().
@angular/animations → CSS.animate.enter="<class>" and animate.leave="<class>" in templates, ordinary class bindings for state transitions, CSS transitions and keyframes for the animation itself.
Reactive Forms → Signal Forms. Only on v22+, only for forms you are already changing, and never half a form at a time. Typed Reactive Forms are not deprecated; a working reactive form is not technical debt.
6. Deliverable format
## Current state — Angular version, support status, NgModule/standalone split, test runner, zone.js, blockers
## Sequence — numbered steps, each one PR, each with its command and how it is verified
## Per-step risk — what could break and what test covers it
## Manual work — what no schematic does, with the estimate
## Not doing — what is deliberately left alone, and why
Checklist
Current and target versions stated, with support status
Upgrade hops are one major at a time, each with its own commit and green CI
Every third-party Angular dependency checked for a release supporting the target major
The update guide consulted for each hop's manual steps
Schematic commands are exact (:inject, :self-closing-tag, :signal-input-migration) and scoped with --path where the diff would be huge
Each schematic is its own PR with a reviewed diff, not a batch
Zoneless step verified by clicking through, not just by a green unit suite
Nothing rewritten that works and is not deprecated (typed Reactive Forms, @Injectable, existing Jest suites)
Anti-patterns
One long-lived "modernisation" branch → one PR per schematic onto trunk; a six-week branch merges never.
Skipping majors ("19 straight to 22") → each ng update runs that major's migrations; skipping means doing them by hand, later, without the warnings.
Hand-rewriting what a schematic does → slower and less accurate; run it, review it, then hand-fix only what it flagged.
ng generate @angular/core:inject-migration → the schematic is @angular/core:inject.
Converting Reactive Forms to Signal Forms across the app → Reactive Forms are supported; convert only forms you are already rewriting, on v22+.
Removing zone.js while the suite still uses fakeAsync → move the tests first, or the suite fails for a reason unrelated to the app.
Keeping withEventReplay() and withIncrementalHydration() after v22 → both are redundant; incremental hydration is the default and enables event replay itself.
Mixing .component.ts and suffix-less names as the rename proceeds → do the rename as one mechanical PR, or not at all (see angular-architecture).
Go deeper
references/manual-migrations.md — zoneless, Karma → Vitest, animations → CSS, Reactive → Signal Forms, RouterTestingModule, step by step
references/upgrade-playbook.md — a worked 17 → 22 sequence with per-hop commands, checkpoints and rollback
version-notes.md in ../angular-development/references/ — what is default in v20, v21 and v22
Siblings: angular-architecture (target structure), angular-development (the modern idioms), angular-testing (the new suite), angular-ssr
---
name: angular-migration
description: Plan and execute Angular upgrades and modernisation — `ng update` one major at a time, NgModule to standalone, `*ngIf`/`*ngFor` to built-in control flow, constructor DI to `inject()`, decorators to signal inputs, outputs and queries, eager to lazy routes, `CommonModule` and `ngClass`/`ngStyle` removal, zone.js to zoneless, Karma to Vitest, `@angular/animations` to `animate.enter`/`animate.leave`, and Reactive Forms to Signal Forms. Use this whenever the user is on Angular 19 or older, asks how to upgrade Angular or "modernise" an Angular codebase, mentions `ng update`, a migration schematic, `ng generate @angular/core:…`, a breaking change after an upgrade, removing zone.js, or getting off Karma, `RouterTestingModule` or `TestBed.flushEffects()`. Also use it when a new-feature request turns out to be blocked by an unsupported Angular version.
metadata:
technology: Angular
type: migration
---
# Angular Migration
> **Targets:** upgrades into Angular 20 LTS – 22 · **Verified:** 2026-09 against https://angular.dev/reference/releases, https://angular.dev/reference/migrations, https://angular.dev/update-guide
A migration succeeds when every step is small, mechanical and independently shippable, and CI is green after each one. Angular ships schematics for almost all of this work — running the schematic and reviewing its diff beats a hand-rewrite on both speed and accuracy. The failure mode is the "big modernisation branch" that is six weeks old, conflicts with everything and can never be merged.
## 1. Decide first
| Question | Default | Change when |
|---|---|---|
| Upgrade or modernise first? | Upgrade to a supported major first; modernisation schematics assume current APIs | A v22 codebase already: skip to section 4 |
| How many majors at once | One at a time, `ng update` per major, commit and deploy between them | Never batch. Skipping a major skips its migrations and its deprecation warnings |
| Order of modernisation | Standalone → control flow → `inject()` → signal inputs/outputs/queries → lazy routes → cleanup | A single blocker (a library with no standalone support) can reorder, but standalone gates most of the rest |
| Branch strategy | One PR per schematic, straight to trunk, behind no flag | A schematic touching > 300 files → split with `--path` per feature folder |
| Test suite during the move | Keep Karma running until the app is on v21+ and standalone | The suite is already the bottleneck → move to Vitest first, it is independent of the rest |
| Hand edits after a schematic | Review the diff, fix what it flagged, commit as one change | Never hand-edit the same file the next schematic will rewrite |
| Below v20 | The upgrade plan is the deliverable, before any feature work | Only a hotfix is in scope |
Version status as of 2026-09: **v22 Active** (released 2026-06-03, LTS ends 2028-06), **v21 LTS** (ends 2027-06), **v20 LTS** (ends 2026-11-28). v19 and below get no fixes, including security fixes.
## 2. Upgrading majors
```bash
ng update # shows what is available for this workspace
ng update @angular/core@21 @angular/cli@21 # one major, core and cli together
npm run test && npm run build # green before the next major
```
- `ng update` runs that major's own migrations automatically. That is why skipping majors is expensive: the migrations do not run retroactively.
- Check https://angular.dev/update-guide for each hop with your exact from/to versions before starting; it lists the manual steps a schematic cannot do.
- Third-party packages (`@angular/material`, `@ngrx/*`, `@angular/cdk`) are updated in the same command as the core hop they support, not after.
- A library with no release for the target major is a decision, not a blocker to discover in week three: replace it, fork it, or stay one major back — but decide before the first `ng update`.
- Commit each major separately. A bisect across a combined 19→22 commit tells you nothing.
## 3. What changed per version
Per-version defaults — zoneless (v21), OnPush (v22), Vitest (v21), Signal Forms, `resource`/`httpResource` and Angular Aria (v22), incremental hydration by default (v22) — are tabulated in `version-notes.md` in `../angular-development/references/`. Read it before generating any code for a mixed-version repo.
The API changes most likely to break a build or mislead a generated fix: `afterRender` was renamed `afterEveryRender` in v20 with no alias; `TestBed.flushEffects()` is deprecated in favour of `TestBed.tick()`; `withIncrementalHydration()` is deprecated (incremental hydration is on by default in `provideClientHydration()`) and it enables event replay, so `withEventReplay()` should be dropped; `@angular/animations` is deprecated; `ChangeDetectionStrategy.Default` is deprecated in favour of `Eager`.
## 4. The modernisation schematics, in order
Run one, review the diff, run the tests, commit. Every command takes `--path <dir>` to scope it to one feature folder.
```bash
ng generate @angular/core:standalone # 3 modes, in this order:
# 1 convert components/directives/pipes to standalone
# 2 remove unnecessary NgModule classes
# 3 bootstrap with standalone APIs
ng generate @angular/core:control-flow # *ngIf/*ngFor/*ngSwitch → @if/@for/@switch
ng generate @angular/core:inject # constructor DI → inject()
ng generate @angular/core:signal-input-migration # @Input → input()
ng generate @angular/core:output-migration # @Output → output()
ng generate @angular/core:signal-queries-migration # @ViewChild/@ContentChild → viewChild()/contentChild()
ng generate @angular/core:route-lazy-loading # eager route components → loadComponent
ng generate @angular/core:common-to-standalone # CommonModule → the directives/pipes actually used
ng generate @angular/core:ngclass-to-class # ngClass → [class] bindings
ng generate @angular/core:ngstyle-to-style # ngStyle → [style] bindings
ng generate @angular/core:cleanup-unused-imports # drop imports the template no longer uses
ng generate @angular/core:self-closing-tag # <x></x> → <x />
ng generate @angular/core:router-testing-module-migration # RouterTestingModule → RouterModule + provideLocationMocks()
```
Note the singular in `self-closing-tag`, and that the `inject()` schematic is `:inject` — `:inject-migration` does not exist.
`:inject` takes `--migrate-abstract-classes` (off by default, because Angular does not validate abstract-class parameters), `--backwards-compatible-constructors` and `--non-null-assertion`. `:ngstyle-to-style` has `--best-effort-mode` for object-reference bindings it would otherwise skip as unsafe.
**Review every diff.** The schematics are conservative and correct on what they touch, but they leave the judgement calls: a converted component may now import more than it needs, and `:control-flow` cannot know that an `*ngFor` should have gained an `@empty` branch.
## 5. The moves with no schematic
Four migrations are manual. Each is independently shippable, and each has a decision rather than a mechanical rule — details and step-by-step recipes in `references/manual-migrations.md`.
- **zone.js → zoneless.** Add `provideZonelessChangeDetection()` on v20, or confirm nothing provides `provideZoneChangeDetection()` on v21+, remove `zone.js` from `polyfills`, then fix what stops updating: anything relying on zone patching now needs a signal write, a template/host listener, `markForCheck()` or `ComponentRef.setInput`. `NgZone.runOutsideAngular` calls become no-ops and should be deleted.
- **Karma → Vitest.** `npm install --save-dev vitest jsdom`, set the `test` builder to `@angular/build:unit-test`, then `ng g @schematics/angular:refactor-jasmine-vitest` to convert spec syntax. It does not install dependencies, change `angular.json` or delete `karma.conf.js` — you do those. `fakeAsync`/`tick` stop working once `zone.js` is gone; replace with fake timers or `await fixture.whenStable()`.
- **`@angular/animations` → CSS.** `animate.enter="<class>"` and `animate.leave="<class>"` in templates, ordinary class bindings for state transitions, CSS transitions and keyframes for the animation itself.
- **Reactive Forms → Signal Forms.** Only on v22+, only for forms you are already changing, and never half a form at a time. Typed Reactive Forms are not deprecated; a working reactive form is not technical debt.
## 6. Deliverable format
```
## Current state — Angular version, support status, NgModule/standalone split, test runner, zone.js, blockers
## Sequence — numbered steps, each one PR, each with its command and how it is verified
## Per-step risk — what could break and what test covers it
## Manual work — what no schematic does, with the estimate
## Not doing — what is deliberately left alone, and why
```
## Checklist
- [ ] Current and target versions stated, with support status
- [ ] Upgrade hops are one major at a time, each with its own commit and green CI
- [ ] Every third-party Angular dependency checked for a release supporting the target major
- [ ] The update guide consulted for each hop's manual steps
- [ ] Schematic commands are exact (`:inject`, `:self-closing-tag`, `:signal-input-migration`) and scoped with `--path` where the diff would be huge
- [ ] Each schematic is its own PR with a reviewed diff, not a batch
- [ ] Removed APIs handled: `afterRender`, `TestBed.flushEffects()`, `withIncrementalHydration()`, `withEventReplay()`, `@angular/animations`
- [ ] Zoneless step verified by clicking through, not just by a green unit suite
- [ ] Nothing rewritten that works and is not deprecated (typed Reactive Forms, `@Injectable`, existing Jest suites)
## Anti-patterns
- **One long-lived "modernisation" branch** → one PR per schematic onto trunk; a six-week branch merges never.
- **Skipping majors ("19 straight to 22")** → each `ng update` runs that major's migrations; skipping means doing them by hand, later, without the warnings.
- **Hand-rewriting what a schematic does** → slower and less accurate; run it, review it, then hand-fix only what it flagged.
- **`ng generate @angular/core:inject-migration`** → the schematic is `@angular/core:inject`.
- **Converting Reactive Forms to Signal Forms across the app** → Reactive Forms are supported; convert only forms you are already rewriting, on v22+.
- **Removing `zone.js` while the suite still uses `fakeAsync`** → move the tests first, or the suite fails for a reason unrelated to the app.
- **Keeping `withEventReplay()` and `withIncrementalHydration()` after v22** → both are redundant; incremental hydration is the default and enables event replay itself.
- **Mixing `.component.ts` and suffix-less names as the rename proceeds** → do the rename as one mechanical PR, or not at all (see `angular-architecture`).
## Go deeper
- `references/manual-migrations.md` — zoneless, Karma → Vitest, animations → CSS, Reactive → Signal Forms, `RouterTestingModule`, step by step
- `references/upgrade-playbook.md` — a worked 17 → 22 sequence with per-hop commands, checkpoints and rollback
- `version-notes.md` in `../angular-development/references/` — what is default in v20, v21 and v22
- Siblings: `angular-architecture` (target structure), `angular-development` (the modern idioms), `angular-testing` (the new suite), `angular-ssr`