# Why is hs project migrate failing?

**TL;DR** — Identify your starting point first: 2025.2 apps just bump `platformVersion` and re-upload (no migrate command); 2023.1/2023.2/2025.1 project apps run `hs project migrate`; non-project legacy public apps run `hs app migrate`; legacy private apps can't migrate at all. The documented failure causes: a missing `crm.objects.custom.read` scope ("Build failed or timed out"), serverless `environment` variables that never migrate (re-add with `hs secret add`), invalid project config (`hs project validate`), and UID churn. CLI ≥ 8.4.0 throughout.

## The mental model: three commands, one-way doors

"Migrating to 2026.03" is not one operation. Per [HubSpot's migration guide](https://developers.hubspot.com/docs/apps/developer-platform/build-apps/migrate-an-app/migrate-to-the-latest-platform-version), there are three distinct paths, and each tool refuses or misbehaves when pointed at the wrong starting state:

1. **Already on 2025.2** → there is no migrate step. Edit `platformVersion` from `2025.2` to `2026.03` in `hsproject.json`, then `hs project upload`. If you ran a migrate command here and got nothing useful, this is why.
2. **Project app on 2023.1, 2023.2, or 2025.1** → `hs project migrate`, run *inside the project's working directory*. It prompts per feature, then converts the whole project to 2026.03.
3. **Legacy public app, no project** → `hs app migrate` (you may find `hs project migrate-app` in older tutorials — same operation, current command is `hs app migrate`). This creates a brand-new project around your live app: features become source files, the project builds and deploys as **Build #1**, and your original auth credentials, features, and installs are preserved.
4. **Legacy private app** → not currently supported. There is no migration path; plan a rebuild.

Before any of these: CLI **8.4.0 or newer** (`hs --version`). Old CLIs fail these flows in undocumented ways, so rule that out first.

The phrase to internalize: **one-way doors**. After `hs app migrate`, project-defined features stop being editable in the app management UI and the old developer APIs. The escape hatch is narrower than people assume — Build #1 is a *configuration baseline* you can redeploy, not an un-migrate button.

## The documented failure catalog

**"Build failed or timed out. Inspect the failure to update your build and retry the migration."** When app cards are involved, the documented cause is a missing `crm.objects.custom.read` scope on the legacy app. The fix sequence matters: add the scope to the *legacy* app, rebuild it on its current version (e.g. 2023.2), and only then retry the migration. Note the contained blast radius — your customer-facing app keeps working while the migration build fails; the scary status on the project details page refers to the new project only.

**Serverless environment variables silently absent.** `hs project migrate` does **not** carry over variables defined under `environment` in `serverless.json`. Nothing fails at migrate time — your functions just run without their config later. Re-create each variable with `hs secret add` immediately after migrating.

**"Build failed: invalid configuration."** Schema problems in the generated `*-hsmeta.json` files. Run `hs project validate` to get the actual field errors, and check that every function `uid` is unique across the project.

**Features detaching after a later build.** That's UID churn, not the migration. A feature's `uid` is its identity; change it in any subsequent build and the platform sees a new feature. Set UIDs deliberately during migration and treat them as immutable afterward.

**CMS projects with serverless functions.** The CMS 2025.2 migration explicitly does not handle serverless functions — that path is manual (and on the app platform, remember [2025.2 can't express functions at all](/docs/answers/hubspot-serverless-functions-2025-2)).

## Diagnose before you migrate, not after

The pattern across every failure above: the information you needed existed *before* the one-way command ran. That's the gap HS-X's migration tooling closes. Point it at your legacy project and it classifies every feature — what converts automatically, what needs human review, and why — without touching your app:

```bash
hs-x migrate inspect ./my-legacy-app   # per-feature classification, no side effects
hs-x migrate report ./my-legacy-app    # readiness summary + concrete next steps
```

`inspect` reads the project tree directly (`hsproject.json`, `serverless.json`, feature `*-hsmeta.json` files) and tells you up front about the things this page exists for — review-required features, serverless config that won't carry, cards that need replacement before the deadline. HS-X's broader migration model is built around the same principle: generate the new 2026.03 app as a **separate testbed**, verify it against a real portal, and only touch the app your users have installed once everything is proven.

## If you're stuck right now

1. `hs --version` → upgrade to ≥ 8.4.0 if older.
2. Confirm which of the four starting states you're in (the wrong-command failure mode).
3. `hs project validate` for config errors; fix `*-hsmeta.json` fields and duplicate UIDs.
4. For "Build failed or timed out" with cards: add `crm.objects.custom.read` to the legacy app, rebuild legacy, retry.
5. After any successful migrate: re-add serverless secrets (`hs secret add`) before first deploy.
6. Keep Build #1 untouched as your revert baseline.

**Where next**

- [Which platform version should I be on?](/docs/answers/which-hubspot-platform-version)
- [Where did serverless functions go in 2025.2?](/docs/answers/hubspot-serverless-functions-2025-2)
- [Migrating legacy CRM cards to UI extensions](/docs/answers/legacy-crm-cards-to-ui-extensions)

