# I built an AI agent that migrates Next.js Pages Router to App Router

> Source: <https://dev.to/garoro0920/i-built-an-ai-agent-that-migrates-nextjs-pages-router-to-app-router-5747>
> Published: 2026-05-25 14:36:24+00:00

Most Next.js teams have a Pages Router → App Router migration sitting in their backlog. It's mechanical but careful work, and it keeps getting deprioritized. I built [migrate-bot](https://migrate-bot.dev) to automate it end-to-end, and this post is about how it works under the hood.

App Router isn't just "move files to a new folder". The semantic changes:

`getStaticProps`

/ `getServerSideProps`

→ `async`

Server Components`getStaticPaths`

→ `generateStaticParams`

`next/router`

→ `next/navigation`

(`useRouter`

/ `usePathname`

/ `useSearchParams`

)`next/head`

→ the Metadata API (`export const metadata`

)`pages/_app.tsx`

+ `pages/_document.tsx`

→ `app/layout.tsx`

`pages/api/x.ts`

(single handler) → `app/api/x/route.ts`

(`export async function GET/POST/...`

)`pages/`

→ `app/`

restructure, including `[slug]`

dynamic routesThe whole thing runs serverless + ephemeral:

```
queued → analyzing → planning → migrating → verifying → pr_ready
                                                ↓ (on failure)
                          aborted_blocker → refunding → refunded
```

Each transition is persisted to D1 so every job is observable, and a mid-pipeline crash transitions to `aborted_blocker`

→ automatic refund rather than leaving the customer charged for nothing.

Early on, every repo with a `pages/_document.tsx`

failed. The planner maps both `_app.tsx`

and `_document.tsx`

to the same target (`app/layout.tsx`

), and my migrate step recorded the second one as a "failed task" when it detected the target was already written. The pipeline treated any failed task as fatal — so the whole migration aborted.

The fix was to distinguish *intentional skips* (planned target collisions) from *real failures* (the LLM aborting or erroring). I added a `skippedTaskIds`

field separate from `failedTaskIds`

, and surfaced the skip in the PR description so the customer knows to manually merge any custom `<Html>`

attributes from their `_document.tsx`

.

Since this rewrites real production code, the trust mechanisms matter:

`next build`

) can't pass for reasons attributable to the service.It's live at [migrate-bot.dev](https://migrate-bot.dev). Pricing is per-repo by file count ($99 / $249 / $499). I'd genuinely value feedback on the approach — especially the pricing shape and the draft-PR + refund trust model.

(Currently not offered to EEA/UK/Switzerland residents due to GDPR territorial scope; revisiting post-launch.)
