Why is hs project migrate failing?
TL;DR — Identify your starting point first: 2025.2 apps bump platformVersion and re-upload; 2023.1, 2023.2, and 2025.1 project apps run hs project migrate; non-project legacy public apps run hs app migrate; legacy private apps can't migrate. The documented failures: a missing crm.objects.custom.read scope, serverless environment variables that never migrate, invalid project config, and UID churn. CLI 8.4.0 or newer throughout.
Which migration command should I be running?
“Migrating to 2026.03” is not one operation. Per HubSpot's migration guide, there are three distinct paths, and each tool refuses or misbehaves when pointed at the wrong starting state:
- Already on 2025.2 → there is no migrate step. Edit
platformVersionfrom2025.2to2026.03inhsproject.json, thenhs project upload. If you ran a migrate command here and got nothing useful, this is why. - 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. - Legacy public app, no project →
hs app migrate(you may findhs project migrate-appin older tutorials — same operation, current command ishs 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. - Legacy private app → not currently supported. There is no migration path; plan a rebuild.
One thing none of the three commands does: convert a legacy CRM card. The card is not project source, so hs project migrate and hs app migrate neither convert nor refuse it; it keeps serving on the app until HubSpot's October 31, 2026 sunset, and replacing it is a separate step. HubSpot's migration flowchart tells project apps to convert legacy cards to app cards before running hs project migrate, so do the replacement first where you can. Note also that 2023.x and 2025.1 are now closed to uploads, so a project on either cannot ship a change until it migrates.
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.
What are the documented failures?
“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 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).
How do I see what breaks before I migrate?
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:
hs-x migrate inspect ./my-legacy-app # per-feature classification, no side effects
hs-x migrate report ./my-legacy-app # readiness summary + concrete next stepsinspect 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, and, when you point it at a migration.json pulled from the live app with hs-x migrate pull, legacy CRM 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.
How do I run the one-way step through HS-X?
Once the testbed is proven, hs-x migrate cutover is the wrapper around the commands above. Bare, it prints a dry-run plan of what cutover changes on the original app and which open decisions gate it; --execute performs it, after you type the project name to confirm (--yes skips that prompt in automation).
hs-x migrate cutover ./my-legacy-app # dry run: ordered plan, blocking vs advisory decisions
hs-x migrate cutover ./my-legacy-app --execute # one-way; needs @hubspot/cli installed and hs accounts authIt maps onto HubSpot's paths the same way this page does. A 2025.2 source gets platformVersion bumped to 2026.03 in place and re-uploaded with hs project upload; if no successful build comes back, the bump is rolled back. A 2023.2 or 2025.1 source hands off to HubSpot's interactive hs project migrate, which needs a real terminal for its feature prompts; afterwards the CLI reminds you to re-add serverless secrets with hs secret add. Non-project legacy apps are refused; run hs app migrate yourself.
The failures it can return:
HSX_E_CUTOVER_EXECUTE_GATED: a finding whose code ends in.missingor.empty, acomponent.unsupported.*finding, nohsproject.jsonat the path (amigration.jsonalone is not enough), or a source that is not a 2023.2, 2025.1, or 2025.2 project. Fix the source and re-run the dry run.HSX_E_CUTOVER_CONFIRM_REQUIRED: not an interactive terminal and no--yes; for 2023.2 and 2025.1 sources, no terminal at all, because HubSpot's prompts cannot be answered by--yes.HSX_E_CUTOVER_CONFIRM_MISMATCH: the typed name did not match the project name. Nothing changed.HSX_E_CUTOVER_EXECUTE_FAILED: HubSpot's CLI is missing or unauthenticated,hs project migrateexited non-zero, or the in-place upload did not reach a successful build. The tree is left as HubSpot found it.
What do I do if I'm stuck right now?
hs --version→ upgrade to 8.4.0 or newer if older.- Confirm which of the four starting states you're in (the wrong-command failure mode).
hs project validatefor config errors; fix*-hsmeta.jsonfields and duplicate UIDs.- For “Build failed or timed out” with cards: add
crm.objects.custom.readto the legacy app, rebuild legacy, retry. - After any successful migrate: re-add serverless secrets (
hs secret add) before first deploy. - Keep Build #1 untouched as your revert baseline (
hs project deploy --buildId=1redeploys it).
Last updated: August 27, 2026. Sourced from HubSpot's migrate to the latest platform version guide and the migration warning in its create an app card guide. Refreshed whenever HubSpot changes a migration command or documents a new failure.