Migrate an app
One command reads your legacy HubSpot project, classifies every feature, and generates a complete 2026.03 app you can test without risk. The generated project is a duplicate built for proving the migration. Your original app keeps running, untouched, until you decide to cut over.
TL;DR — Run hs-x migrate run . in your legacy project. It detects the platform version, classifies every feature into "ported automatically" or "needs your decision," and generates a net-new 2026.03 project with typed capabilities replacing your serverless functions, webhooks, and workflow actions. Test the result with hs-x dev. The original app is never modified.
A duplicate you can afford to break
Every migration tool faces the same trust problem: the command is easy to run and expensive to regret. HubSpot's own hs project migrate converts your app in place, and several of its steps are one-way. That is the right final move and the wrong first move.
HS-X separates the two. hs-x migrate run produces a net-new project, a duplicate of your app expressed in 2026.03 terms. It has its own directory, and when you deploy it, it becomes its own app with its own app ID. You can build it, run it in hs-x dev, deploy it, install it on a test portal, and throw it away. Your original app, with its installs, credentials, and marketplace listing, does not know any of this is happening.
Cutover, the step that touches the original, comes later and only with your explicit confirmation. The testbed is how you arrive at that confirmation with evidence instead of hope.
Run it
From the root of the legacy project (the directory with hsproject.json):
hs-x migrate run . --out ./quote-tracker-hsxHere is a real run against a 2025.1 public app with two serverless functions, a deal webhook, and a workflow action:
[ok] Generated migrated HS-X project (the dupe) 6 files ./quote-tracker-hsx
Ported automatically:
- Serverless function quote-status can be ported to a Worker-backed capability.
- Serverless function quote-status uses 1 secret(s); remap them to Worker secrets on deploy.
- Serverless function refresh-cache can be ported to a Worker-backed capability.
- 1 webhook subscription(s) can be ported to an HS-X trigger capability.
- Workflow action flag-stale (0 input field(s)) can be ported to an HS-X tool capability.
Decisions before cutover (the dupe is a testbed; these do not block it):
- [app.distribution.marketplace] This app distributes via the HubSpot Marketplace; cutover
affects the public listing and installed customers.
- [webhooks.target-url.forwarding] Events currently deliver to https://quotes.example.com/hooks;
confirm whether the migrated Worker should keep forwarding there or replace it.
[ok] Validation
Next steps:
cd quote-tracker-hsx && bun install
hs-x dev # test every migrated capability locally
hs-x deploy # upload the dupe as a NET-NEW app when readyIf you want the analysis without generating anything, hs-x migrate inspect . prints the same classification with no side effects, and hs-x migrate report . adds a readiness summary.
What the generator carries
The generated project keeps your app's real identity. Name, description, distribution mode, auth type, and scopes come from your legacy app.json or app-hsmeta.json and land in a typed defineApp block:
export default defineApp({
name: "Quote Tracker",
description: "Tracks quote status on deals",
distribution: "marketplace",
auth: "oauth",
platformVersion: "2026.03",
scopes: ["crm.objects.deals.read", "crm.objects.deals.write"],
});Features become typed capabilities, one worker file per feature family. A legacy serverless function turns into a tool whose handler is deliberately a stub:
worker.tool("quote-status", {
label: "Migrated: quote-status",
async handler() {
// TODO(migration): port the body of "quote-status.js" here.
// The legacy handler signature was `async (context) => result`; HS-X
// passes { input, enrolledObject } and returns ok(...)/err(...).
return ok({ ok: true });
},
});This is an honest boundary. Structure, wiring, identity, and registration port automatically; your business logic does not, because silently transformed logic is how migrations break in production. The TODO comment documents both signatures so each port is mechanical. Functions that declared secrets get a comment naming them, and hs-x injects the values as Worker secrets on deploy.
Webhook subscriptions become trigger capabilities. Workflow actions become tools with their input fields carried over. Each generated file states what it replaced and where the original lived.
The findings only you can answer
Decision findings print with a bracketed code and never block the testbed. The current set:
Marketplace distribution
Your app is listed on the HubSpot Marketplace. The testbed is unaffected, but cutover touches a public listing and live customers, so plan it with the legacy public app guide and the marketplace listing guide. For card swaps on listed apps, HubSpot auto-hides new app cards until you delete the rollout feature flags; the legacy CRM cards answer walks that sequence.
Webhook target forwarding
Your legacy app delivers webhook events to an external URL. After migration, events arrive at your Worker's trigger capability instead. Decide whether the Worker should keep forwarding events to the old endpoint (your backend keeps working unchanged) or replace it (the Worker becomes the backend). Forwarding is the safe first state; replacement is usually the destination.
Action URL forwarding
Same choice, for workflow action executions. The legacy action POSTs to your server; the migrated tool runs in your Worker. Keep the Worker forwarding to the old actionUrl while you verify behavior in hs-x dev, then move the logic into the handler and drop the forward.
Test, deploy, and the road to cutover
cd quote-tracker-hsx && bun install
hs-x dev # exercise every migrated capability against your portal
hs-x deploy # uploads the testbed as a NET-NEW appWork through the TODO ports one capability at a time, testing each in hs-x dev as you go. When the testbed behaves identically to the original, you are ready for cutover planning: the in-place migration of your real app, which keeps its app ID, installs, and OAuth grants. The decision tree and failure catalog for that step live in the hs project migrate troubleshooting answer, and the version background lives in which platform version should I be on.
