Skip to content

[nest] Fix NestJS Vercel build output - #2988

Merged
VaguelySerious merged 8 commits into
mainfrom
peter/nest-vercel-support
Jul 20, 2026
Merged

[nest] Fix NestJS Vercel build output#2988
VaguelySerious merged 8 commits into
mainfrom
peter/nest-vercel-support

Conversation

@VaguelySerious

@VaguelySeriousVaguelySerious commented Jul 17, 2026

Copy link
Copy Markdown
Member

Closes#1472 and #878

NestJS currently produces no .vercel/output, so nothing emits a queue-consumer function with experimentalTriggers, and Vercel Queue never dispatches the run.

Every other server framework (Express/Fastify/Hono) sidesteps this by being a Nitro app whose Vercel preset emits the consumer. NestJS has its own compiler/DI and can't ride that path, so it needs to emit the Build Output itself.

Approach

Keep nest build (SWC compiles decorators + emitDecoratorMetadata natively). After it, workflow-nest build --vercel (also auto-detected via the VERCEL env var) writes a complete .vercel/output:

  • Consumer — the combined flow.func (+ webhook/[token].func, static manifest, routing) is produced by the shared VercelBuildOutputAPIBuilder, i.e. the exact code path Nitro/Next/etc. use, so it's registered with the __wkf_workflow_*experimentalTriggers and VQS discovers it. Single combined trigger — matches current arch.
  • App — the NestJS app (from the SWC-compiled dist/) is bundled as a catch-all function, with the workflow world statically injected (createWorkflowWorldTargetEsbuildPlugin) so start() works inside it.
  • Routing — merged into config.json: workflow routes + filesystem win, then fall through to the app.

WorkflowModule now lazy-loads the build toolchain (@workflow/builders, esbuild, SWC) so importing it no longer drags the compiler into the runtime serverless bundle. The runtime entry (workflow/nest) no longer re-exports the builders; they live at workflow/nest/builder and workflow/nest/vercel-builder.

This deliberately avoids the earlier #1472 hacks (base64-embedding bundles into source, /tmp writes, globalThis injection, separate step/flow triggers) — each now has a clean shared equivalent.

Testing

  1. Tarballed the SDK, scaffolded a throwaway NestJS app consuming the tarballs, deployed prebuilt to Vercel (team peter-w).
  2. POST /api/startwrun_…; the run completed (both steps + a sleep ran):
    { "status": "completed",
    "returnValue": { "hello": "Hello, vercel!", "bye": "Goodbye, vercel.", "completedAt": "" } }
    i.e. it no longer stays pending.
  • App-function bundle contains no build tooling (esbuild/SWC/@workflow/builders) and boots under VERCEL=1.
  • flow.func/.vc-config.json carries the __wkf_workflow_* queue trigger.
  • @workflow/nest unit tests pass (37).

Status / notes

  • Draft: NestJS is still labelled experimental; this makes Vercel deploys actually work and documents the flow. No E2E Vercel lane for Nest is wired yet — worth adding before calling it supported.
  • The Nest-app bundling uses an esbuild externals allowlist for optional Nest peers; @vercel/nft-style tracing is a more robust future option if the allowlist proves brittle.

Deployed NestJS workflows previously stayed `pending` forever: nothing emitted
a queue-consumer function for Vercel Queue to discover, so runs were never
dispatched. This adds a Vercel Build Output path for NestJS.
`workflow-nest build --vercel` (also triggered when the VERCEL env var is set)
now writes `.vercel/output`:
- the combined workflow queue-consumer `flow.func` is produced by the shared
`VercelBuildOutputAPIBuilder` — the same code path Nitro/Next/etc. use — so
it is registered with `experimentalTriggers` and discovered by VQS;
- the NestJS app is bundled (from the SWC-compiled `dist/`) as a catch-all
function with the workflow world statically injected;
- routing is merged into `config.json` (workflow routes + filesystem win, then
fall through to the app).
`WorkflowModule` now lazy-loads the build toolchain (@workflow/builders,
esbuild, SWC) so importing it no longer pulls the compiler into the runtime
serverless bundle. The builders remain available via the
`workflow/nest/builder` and `workflow/nest/vercel-builder` subpaths.
Verified end-to-end: deployed a NestJS app to Vercel and a triggered workflow
ran to completion (previously stuck pending).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@changeset-bot

changeset-botBot commented Jul 17, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 6e6f9ab

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 16 packages
NameType
@workflow/nestMajor
workflowPatch
@workflow/world-testingPatch
@workflow/corePatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
@workflow/astroPatch
@workflow/nuxtPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercelBot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

The benchmark run for 6e6f9ab failed. See the run logs for details.

commit f549c8e · Fri, 17 Jul 2026 20:47:00 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioAvg (ms)P10 (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1446 (+14%)1750 🔴1892 🔴2518 🔴30
TTFShook + stream1540 (+16%)1896 🔴2120 🔴2596 🔴30
STSO1020 steps (1-20)285 (-4.5%)315 🔴391 🔴448 🔴19
STSO1020 steps (101-120)288 (-6.5%)308 🔴366 🔴370 🔴19
STSO1020 steps (1001-1020)557 (-11%)571 🔴642 🔴719 🔴19
WOstream1446 (+14%)17501892251830
WOhook + stream1540 (+16%)18962120259630
SLstream4237 (+324%)5649 🔴5697 🔴5817 🔴30
SLhook + stream3765 (+93%)5520 🔴5564 🔴6027 🔴30
📜 Previous results (1)

dad0793

Fri, 17 Jul 2026 20:03:00 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P10 (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1321 (+4.3%)1629 🔴1677 🔴1788 🔴30
TTFShook + stream1533 (+15%)1913 🔴1963 🔴2142 🔴30
STSO1020 steps (1-20)265 (-11%)295 🔴364 🔴423 🔴19
STSO1020 steps (101-120)280 (-9.4%)301 🔴355 🔴392 🔴19
STSO1020 steps (1001-1020)578 (-7.6%)614 🔴778 🔴837 🔴19
WOstream1321 (+4.3%)16291677178830
WOhook + stream1533 (+15%)19131963214230
SLstream4495 (+350%)5794 🔴5836 🔴5922 🔴30
SLhook + stream4020 (+106%)5584 🔴5627 🔴5721 🔴30

Avg deltas compare against the most recent benchmark run on main at the time of this run.

Metrics — TTFS: time to first step body (in-deployment start() → first step body, deployment clocks) · STSO: step-to-step overhead (gap between consecutive step bodies) · WO: workflow overhead (whole-run time outside step bodies, in-deployment anchored) · SL: stream latency (in-deployment write → read propagation, readAt - writtenAt)

Scenarios — stream: one step that streams chunks back to the client; no hooks, so the run stays in turbo mode · hook + stream: registers a hook before the same streaming step, which exits turbo mode · 1020 steps: 1020 trivial sequential steps; STSO is measured between consecutive steps in the given step ranges

🟢/🔴 mark percentiles within/above target. Targets (p75/p90/p99, ms) — TTFS 200/300/600 · SL 50/60/125 · STSO (1-20) 20/30/60 · STSO (101-120) 30/45/90 · STSO (1001-1020) 40/60/120

All metrics are measured from deployment-side timestamps only. Runs are triggered by an in-deployment route that stamps the anchor (clientStart) right before start(), so the CI runner’s request and its path through api.vercel.com sit outside every measured window. TTFS = in-deployment start() → first step body (turbo uses the in-process fast path, non-turbo the dispatch path), and includes the VQS dispatch hop plus any /flow cold start. STSO/WO are measured between step bodies on the deployment. SL is measured inside the workflow (parallel reader/writer steps), so it no longer includes the api.vercel.com read path.

Cold starts are kept in the numbers on purpose — they are part of real bursty-workload latency. The workbench deployment cold-starts the /flow invocation for a large fraction of runs, inflating P75+; the P10 column shows the warm-start floor for comparison.

@github-actions

github-actionsBot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production145302301683
✅ 💻 Local Development161702191836
✅ 📦 Local Production161702191836
✅ 🐘 Local Postgres161702191836
✅ 🪟 Windows15300153
✅ 📋 Other89401771071
✅ vercel-multi-region270027
Total7378010648442

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro126027
✅ example126027
✅ express126027
✅ fastify126027
✅ hono126027
✅ nextjs-turbopack15003
✅ nextjs-webpack15003
✅ nitro126027
✅ nuxt126027
✅ sveltekit14508
✅ vite126027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15300
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable128025
✅ e2e-local-dev-tanstack-start-128025
✅ e2e-local-postgres-nest-stable128025
✅ e2e-local-postgres-tanstack-start-128025
✅ e2e-local-prod-nest-stable128025
✅ e2e-local-prod-tanstack-start-128025
✅ e2e-vercel-prod-tanstack-start126027
✅ vercel-multi-region
AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

Comment threadpackages/nest/src/index.ts
…` meta-package, breaking the documented `import { NestLocalBuilder } from "workflow/nest"` and leaving no working alternative.
This commit fixes the issue reported at packages/nest/src/index.ts:6
## The bug
This PR changed `packages/nest/src/index.ts` from a value+type export:
```ts
export { type NestBuilderOptions, NestLocalBuilder } from './builder.js';
```
to a type-only export:
```ts
export type { NestBuilderOptions } from './builder.js';
```
The rationale is valid — `builder.ts` imports `@workflow/builders` (esbuild/SWC) at top level, and the runtime entry must stay free of build-time deps so a NestJS app can be bundled into a serverless function. The `NestLocalBuilder` value is instead intended to be reached via the `workflow/nest/builder` subpath (as the new comment in `index.ts` states).
The problem is that the escape hatch was only half-wired:
- `@workflow/nest`'s own `package.json` *does* expose a `./builder` subpath (`./dist/builder.js`).
- But the recommended consumer package is the **meta `workflow` package**, whose `package.json` only exposes `./nest` → `./dist/nest.js`, and `packages/workflow/src/nest.ts` is just `export * from '@workflow/nest'` (the runtime entry that no longer value-exports `NestLocalBuilder`). There was **no** `./nest/builder` subpath in the meta package.
### Concrete trigger
A user following the API-reference docs writes:
```ts
import { NestLocalBuilder } from "workflow/nest";
```
(`docs/content/docs/v5/api-reference/workflow-nest/nest-local-builder.mdx` line 15 and the v4 equivalent.) At runtime this resolves to `@workflow/nest`'s main entry, which now exports only the *type* `NestBuilderOptions` — so `NestLocalBuilder` is `undefined`, and `new NestLocalBuilder(...)` throws `TypeError: NestLocalBuilder is not a constructor`. There was also no `workflow/nest/builder` subpath to fall back to, so the value class was completely unreachable through the `workflow` meta package.
## The fix
1. Added a `./nest/builder` subpath to `packages/workflow/package.json` mapping to `./dist/nest-builder.js`.
2. Created `packages/workflow/src/nest-builder.ts` = `export * from '@workflow/nest/builder'`, mirroring the existing `src/nest.ts` pattern, so the value export is threaded through the meta package and compiled to `dist/nest-builder.js`.
3. Updated the v4 and v5 `nest-local-builder.mdx` docs to the working import path `import { NestLocalBuilder } from "workflow/nest/builder"`.
This restores reachability of `NestLocalBuilder` for meta-package consumers while preserving the PR's goal of keeping build-time deps out of the runtime entry.
Note: the sandbox has no installed `node_modules`, so a full `tsc` build could not be executed; the change is structurally identical to the already-working `./nest` subpath wiring.
Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Co-authored-by: VaguelySerious <mittgfu@gmail.com>
@VaguelySeriousVaguelySerious changed the title [nest] Support deploying NestJS workflows to Vercel[nest] Fix NestJS Vercel build outputJul 17, 2026
@VaguelySerious
VaguelySerious marked this pull request as ready for review July 17, 2026 19:46
@VaguelySerious
VaguelySerious requested review from a team and ijjk as code ownersJuly 17, 2026 19:46
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
'@swc/core/*',
'@swc/wasm',
'esbuild',
'*.node',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ai review: *.node is still always externalized, but this builder does not trace or copy external files into appFuncDir; the emitted function contains only index.js, package.json, and .vc-config.json. Any Nest app dependency with a native addon therefore keeps a runtime reference to a .node file that is absent from the deployed .func, causing MODULE_NOT_FOUND on cold start. Please trace/copy native artifacts (and their package layout) into the function, or fail the build with a clear unsupported-dependency error instead of producing a broken deployment.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed as a documented limitation rather than adding native-module tracing, which is out of scope for this exploratory integration (reliably tracing/copying .node artifacts also requires handling the runtime resolution these modules do via bindings/node-gyp-build).

*.node stays externalized so esbuild does not fail the build on a .node file it cannot bundle; I added a comment at that line making the intentional choice and its consequence explicit (b047a12), and called out the limitation in the changeset-linked README section ("Deploying to Vercel") plus the changeset. If native-addon support is needed we can follow up with @vercel/nft tracing.

Comment thread.changeset/nest-vercel-support.md Outdated
@@ -0,0 +1,5 @@
---
'@workflow/nest': minor

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ai review: This is marked as a minor change, but the PR removes the public NestLocalBuilder value export from the @workflow/nest and workflow/nest main entries. Existing documented imports stop working; adding a new /builder subpath provides a migration target but does not preserve compatibility. Because this bump type also governs stable backports, please either retain a compatible main-entry export or mark the change as major.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bumped @workflow/nest to major (b047a12). Retaining the main-entry NestLocalBuilder export is not an option — it would re-drag the build toolchain (@workflow/builders, esbuild, SWC) into the runtime bundle of every app that imports WorkflowModule, which is the bug this PR fixes — so the subpath (@workflow/nest/builder) is the migration target and the changeset now documents it as a breaking change.


Commands:
init Generate .swcrc configuration with the workflow plugin
build Build workflow bundles (and the Vercel Build Output when on Vercel)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ai review: The package README still documents only the init CLI command and has no Vercel deployment/build instructions. Repository standards require corresponding README updates when package functionality changes, and direct @workflow/nest consumers otherwise have no package-local documentation for this new command. Please update packages/nest/README.md with build, its options, the Vercel entry module, and the vercel-build flow.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated packages/nest/README.md (b047a12): added a "Deploying to Vercel" section (serverless entry module, skipBuild on Vercel, the vercel-build flow, native-addon limitation), documented the build command with an options table (--vercel, --dirs, --entry, --out-dir, --module), and noted the NestLocalBuilder/NestVercelBuilder subpaths in "How It Works".

…ive-addon limitation
- Changeset: bump @workflow/nest to major (removing NestLocalBuilder from the
main entry is a breaking change) and document the subpath migration + the
native-addon limitation.
- README: document the `build` command and its options, the Vercel entry
module and `vercel-build` flow, the NestVercelBuilder subpath, and the
native-addon limitation.
- vercel-builder: comment why `*.node` is externalized and that native addons
are not yet traced/copied into the deployed function.
- Minor wording: Workflow DevKit -> Workflow SDK.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment threadpackages/nest/README.md Outdated
Comment threadpackages/nest/README.md Outdated
```typescript
// _vercel/entry.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from '../src/app.module';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ai review: This entry bypasses the nest build output by importing ../src/app.module, so the Vercel builder feeds the Nest TypeScript graph back through esbuild. That contradicts the builder assumption that it bundles already-compiled JS, and esbuild does not emit the TypeScript emitDecoratorMetadata that Nest constructor injection relies on; apps with injected providers can therefore boot with missing DI metadata. Please import ../dist/app.module.js here, matching the checked-in workbench and the v5 guide, so the SWC-compiled decorator metadata is retained.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixed in 6e6f9ab. The entry example now imports the compiled ../dist/app.module.js (produced by nest build, whose SWC pass emits emitDecoratorMetadata) instead of raw ../src/app.module, and adds import 'reflect-metadata'. This matches the checked-in workbench/nest/_vercel/entry.ts and the getting-started docs, both of which already used dist/ + reflect-metadata.

…t + reflect-metadata
The entry snippet imported AppModule from raw `../src/app.module` and omitted
`import 'reflect-metadata'`. Importing raw TS routes the app back through esbuild
(which does not emit `emitDecoratorMetadata`), breaking NestJS constructor
injection; the entry must import the `nest build` output (`../dist/app.module.js`)
and register reflect-metadata — matching the workbench entry and getting-started
docs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actionsgithub-actionsBot mentioned this pull request Jul 20, 2026
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for 542138d (AI decision).

The new NestVercelBuilder depends on createWorkflowWorldTargetEsbuildPlugin from @workflow/builders (introduced in #2752), which does not exist on stable (verified via git grep on origin/stable), so the commit builds on main-only APIs and would not compile there. Additionally, the changeset marks @workflow/nest as a major breaking change (moving NestLocalBuilder off the package root), which is intended for the next major release rather than the stable v4 line.

To override, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

542138dc0b37f492fbf51b75e8ba4f57e291fc8f

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@VaguelySerious@karthikscale3
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
[nest] Fix NestJS Vercel build output by VaguelySerious · Pull Request #2988 · vercel/workflow · GitHub
Skip to content

[nest] Fix NestJS Vercel build output - #2988

Merged
VaguelySerious merged 8 commits into
mainfrom
peter/nest-vercel-support
Jul 20, 2026
Merged

[nest] Fix NestJS Vercel build output#2988
VaguelySerious merged 8 commits into
mainfrom
peter/nest-vercel-support

Conversation

@VaguelySerious

@VaguelySeriousVaguelySerious commented Jul 17, 2026

Copy link
Copy Markdown
Member

Closes#1472 and #878

NestJS currently produces no .vercel/output, so nothing emits a queue-consumer function with experimentalTriggers, and Vercel Queue never dispatches the run.

Every other server framework (Express/Fastify/Hono) sidesteps this by being a Nitro app whose Vercel preset emits the consumer. NestJS has its own compiler/DI and can't ride that path, so it needs to emit the Build Output itself.

Approach

Keep nest build (SWC compiles decorators + emitDecoratorMetadata natively). After it, workflow-nest build --vercel (also auto-detected via the VERCEL env var) writes a complete .vercel/output:

  • Consumer — the combined flow.func (+ webhook/[token].func, static manifest, routing) is produced by the shared VercelBuildOutputAPIBuilder, i.e. the exact code path Nitro/Next/etc. use, so it's registered with the __wkf_workflow_*experimentalTriggers and VQS discovers it. Single combined trigger — matches current arch.
  • App — the NestJS app (from the SWC-compiled dist/) is bundled as a catch-all function, with the workflow world statically injected (createWorkflowWorldTargetEsbuildPlugin) so start() works inside it.
  • Routing — merged into config.json: workflow routes + filesystem win, then fall through to the app.

WorkflowModule now lazy-loads the build toolchain (@workflow/builders, esbuild, SWC) so importing it no longer drags the compiler into the runtime serverless bundle. The runtime entry (workflow/nest) no longer re-exports the builders; they live at workflow/nest/builder and workflow/nest/vercel-builder.

This deliberately avoids the earlier #1472 hacks (base64-embedding bundles into source, /tmp writes, globalThis injection, separate step/flow triggers) — each now has a clean shared equivalent.

Testing

  1. Tarballed the SDK, scaffolded a throwaway NestJS app consuming the tarballs, deployed prebuilt to Vercel (team peter-w).
  2. POST /api/startwrun_…; the run completed (both steps + a sleep ran):
    { "status": "completed",
    "returnValue": { "hello": "Hello, vercel!", "bye": "Goodbye, vercel.", "completedAt": "" } }
    i.e. it no longer stays pending.
  • App-function bundle contains no build tooling (esbuild/SWC/@workflow/builders) and boots under VERCEL=1.
  • flow.func/.vc-config.json carries the __wkf_workflow_* queue trigger.
  • @workflow/nest unit tests pass (37).

Status / notes

  • Draft: NestJS is still labelled experimental; this makes Vercel deploys actually work and documents the flow. No E2E Vercel lane for Nest is wired yet — worth adding before calling it supported.
  • The Nest-app bundling uses an esbuild externals allowlist for optional Nest peers; @vercel/nft-style tracing is a more robust future option if the allowlist proves brittle.

Deployed NestJS workflows previously stayed `pending` forever: nothing emitted
a queue-consumer function for Vercel Queue to discover, so runs were never
dispatched. This adds a Vercel Build Output path for NestJS.
`workflow-nest build --vercel` (also triggered when the VERCEL env var is set)
now writes `.vercel/output`:
- the combined workflow queue-consumer `flow.func` is produced by the shared
`VercelBuildOutputAPIBuilder` — the same code path Nitro/Next/etc. use — so
it is registered with `experimentalTriggers` and discovered by VQS;
- the NestJS app is bundled (from the SWC-compiled `dist/`) as a catch-all
function with the workflow world statically injected;
- routing is merged into `config.json` (workflow routes + filesystem win, then
fall through to the app).
`WorkflowModule` now lazy-loads the build toolchain (@workflow/builders,
esbuild, SWC) so importing it no longer pulls the compiler into the runtime
serverless bundle. The builders remain available via the
`workflow/nest/builder` and `workflow/nest/vercel-builder` subpaths.
Verified end-to-end: deployed a NestJS app to Vercel and a triggered workflow
ran to completion (previously stuck pending).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@changeset-bot

changeset-botBot commented Jul 17, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 6e6f9ab

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 16 packages
NameType
@workflow/nestMajor
workflowPatch
@workflow/world-testingPatch
@workflow/corePatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
@workflow/astroPatch
@workflow/nuxtPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercelBot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

The benchmark run for 6e6f9ab failed. See the run logs for details.

commit f549c8e · Fri, 17 Jul 2026 20:47:00 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioAvg (ms)P10 (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1446 (+14%)1750 🔴1892 🔴2518 🔴30
TTFShook + stream1540 (+16%)1896 🔴2120 🔴2596 🔴30
STSO1020 steps (1-20)285 (-4.5%)315 🔴391 🔴448 🔴19
STSO1020 steps (101-120)288 (-6.5%)308 🔴366 🔴370 🔴19
STSO1020 steps (1001-1020)557 (-11%)571 🔴642 🔴719 🔴19
WOstream1446 (+14%)17501892251830
WOhook + stream1540 (+16%)18962120259630
SLstream4237 (+324%)5649 🔴5697 🔴5817 🔴30
SLhook + stream3765 (+93%)5520 🔴5564 🔴6027 🔴30
📜 Previous results (1)

dad0793

Fri, 17 Jul 2026 20:03:00 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P10 (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1321 (+4.3%)1629 🔴1677 🔴1788 🔴30
TTFShook + stream1533 (+15%)1913 🔴1963 🔴2142 🔴30
STSO1020 steps (1-20)265 (-11%)295 🔴364 🔴423 🔴19
STSO1020 steps (101-120)280 (-9.4%)301 🔴355 🔴392 🔴19
STSO1020 steps (1001-1020)578 (-7.6%)614 🔴778 🔴837 🔴19
WOstream1321 (+4.3%)16291677178830
WOhook + stream1533 (+15%)19131963214230
SLstream4495 (+350%)5794 🔴5836 🔴5922 🔴30
SLhook + stream4020 (+106%)5584 🔴5627 🔴5721 🔴30

Avg deltas compare against the most recent benchmark run on main at the time of this run.

Metrics — TTFS: time to first step body (in-deployment start() → first step body, deployment clocks) · STSO: step-to-step overhead (gap between consecutive step bodies) · WO: workflow overhead (whole-run time outside step bodies, in-deployment anchored) · SL: stream latency (in-deployment write → read propagation, readAt - writtenAt)

Scenarios — stream: one step that streams chunks back to the client; no hooks, so the run stays in turbo mode · hook + stream: registers a hook before the same streaming step, which exits turbo mode · 1020 steps: 1020 trivial sequential steps; STSO is measured between consecutive steps in the given step ranges

🟢/🔴 mark percentiles within/above target. Targets (p75/p90/p99, ms) — TTFS 200/300/600 · SL 50/60/125 · STSO (1-20) 20/30/60 · STSO (101-120) 30/45/90 · STSO (1001-1020) 40/60/120

All metrics are measured from deployment-side timestamps only. Runs are triggered by an in-deployment route that stamps the anchor (clientStart) right before start(), so the CI runner’s request and its path through api.vercel.com sit outside every measured window. TTFS = in-deployment start() → first step body (turbo uses the in-process fast path, non-turbo the dispatch path), and includes the VQS dispatch hop plus any /flow cold start. STSO/WO are measured between step bodies on the deployment. SL is measured inside the workflow (parallel reader/writer steps), so it no longer includes the api.vercel.com read path.

Cold starts are kept in the numbers on purpose — they are part of real bursty-workload latency. The workbench deployment cold-starts the /flow invocation for a large fraction of runs, inflating P75+; the P10 column shows the warm-start floor for comparison.

@github-actions

github-actionsBot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production145302301683
✅ 💻 Local Development161702191836
✅ 📦 Local Production161702191836
✅ 🐘 Local Postgres161702191836
✅ 🪟 Windows15300153
✅ 📋 Other89401771071
✅ vercel-multi-region270027
Total7378010648442

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro126027
✅ example126027
✅ express126027
✅ fastify126027
✅ hono126027
✅ nextjs-turbopack15003
✅ nextjs-webpack15003
✅ nitro126027
✅ nuxt126027
✅ sveltekit14508
✅ vite126027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15300
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable128025
✅ e2e-local-dev-tanstack-start-128025
✅ e2e-local-postgres-nest-stable128025
✅ e2e-local-postgres-tanstack-start-128025
✅ e2e-local-prod-nest-stable128025
✅ e2e-local-prod-tanstack-start-128025
✅ e2e-vercel-prod-tanstack-start126027
✅ vercel-multi-region
AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

Comment threadpackages/nest/src/index.ts
…` meta-package, breaking the documented `import { NestLocalBuilder } from "workflow/nest"` and leaving no working alternative.
This commit fixes the issue reported at packages/nest/src/index.ts:6
## The bug
This PR changed `packages/nest/src/index.ts` from a value+type export:
```ts
export { type NestBuilderOptions, NestLocalBuilder } from './builder.js';
```
to a type-only export:
```ts
export type { NestBuilderOptions } from './builder.js';
```
The rationale is valid — `builder.ts` imports `@workflow/builders` (esbuild/SWC) at top level, and the runtime entry must stay free of build-time deps so a NestJS app can be bundled into a serverless function. The `NestLocalBuilder` value is instead intended to be reached via the `workflow/nest/builder` subpath (as the new comment in `index.ts` states).
The problem is that the escape hatch was only half-wired:
- `@workflow/nest`'s own `package.json` *does* expose a `./builder` subpath (`./dist/builder.js`).
- But the recommended consumer package is the **meta `workflow` package**, whose `package.json` only exposes `./nest` → `./dist/nest.js`, and `packages/workflow/src/nest.ts` is just `export * from '@workflow/nest'` (the runtime entry that no longer value-exports `NestLocalBuilder`). There was **no** `./nest/builder` subpath in the meta package.
### Concrete trigger
A user following the API-reference docs writes:
```ts
import { NestLocalBuilder } from "workflow/nest";
```
(`docs/content/docs/v5/api-reference/workflow-nest/nest-local-builder.mdx` line 15 and the v4 equivalent.) At runtime this resolves to `@workflow/nest`'s main entry, which now exports only the *type* `NestBuilderOptions` — so `NestLocalBuilder` is `undefined`, and `new NestLocalBuilder(...)` throws `TypeError: NestLocalBuilder is not a constructor`. There was also no `workflow/nest/builder` subpath to fall back to, so the value class was completely unreachable through the `workflow` meta package.
## The fix
1. Added a `./nest/builder` subpath to `packages/workflow/package.json` mapping to `./dist/nest-builder.js`.
2. Created `packages/workflow/src/nest-builder.ts` = `export * from '@workflow/nest/builder'`, mirroring the existing `src/nest.ts` pattern, so the value export is threaded through the meta package and compiled to `dist/nest-builder.js`.
3. Updated the v4 and v5 `nest-local-builder.mdx` docs to the working import path `import { NestLocalBuilder } from "workflow/nest/builder"`.
This restores reachability of `NestLocalBuilder` for meta-package consumers while preserving the PR's goal of keeping build-time deps out of the runtime entry.
Note: the sandbox has no installed `node_modules`, so a full `tsc` build could not be executed; the change is structurally identical to the already-working `./nest` subpath wiring.
Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Co-authored-by: VaguelySerious <mittgfu@gmail.com>
@VaguelySeriousVaguelySerious changed the title [nest] Support deploying NestJS workflows to Vercel[nest] Fix NestJS Vercel build outputJul 17, 2026
@VaguelySerious
VaguelySerious marked this pull request as ready for review July 17, 2026 19:46
@VaguelySerious
VaguelySerious requested review from a team and ijjk as code ownersJuly 17, 2026 19:46
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
'@swc/core/*',
'@swc/wasm',
'esbuild',
'*.node',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ai review: *.node is still always externalized, but this builder does not trace or copy external files into appFuncDir; the emitted function contains only index.js, package.json, and .vc-config.json. Any Nest app dependency with a native addon therefore keeps a runtime reference to a .node file that is absent from the deployed .func, causing MODULE_NOT_FOUND on cold start. Please trace/copy native artifacts (and their package layout) into the function, or fail the build with a clear unsupported-dependency error instead of producing a broken deployment.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed as a documented limitation rather than adding native-module tracing, which is out of scope for this exploratory integration (reliably tracing/copying .node artifacts also requires handling the runtime resolution these modules do via bindings/node-gyp-build).

*.node stays externalized so esbuild does not fail the build on a .node file it cannot bundle; I added a comment at that line making the intentional choice and its consequence explicit (b047a12), and called out the limitation in the changeset-linked README section ("Deploying to Vercel") plus the changeset. If native-addon support is needed we can follow up with @vercel/nft tracing.

Comment thread.changeset/nest-vercel-support.md Outdated
@@ -0,0 +1,5 @@
---
'@workflow/nest': minor

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ai review: This is marked as a minor change, but the PR removes the public NestLocalBuilder value export from the @workflow/nest and workflow/nest main entries. Existing documented imports stop working; adding a new /builder subpath provides a migration target but does not preserve compatibility. Because this bump type also governs stable backports, please either retain a compatible main-entry export or mark the change as major.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bumped @workflow/nest to major (b047a12). Retaining the main-entry NestLocalBuilder export is not an option — it would re-drag the build toolchain (@workflow/builders, esbuild, SWC) into the runtime bundle of every app that imports WorkflowModule, which is the bug this PR fixes — so the subpath (@workflow/nest/builder) is the migration target and the changeset now documents it as a breaking change.


Commands:
init Generate .swcrc configuration with the workflow plugin
build Build workflow bundles (and the Vercel Build Output when on Vercel)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ai review: The package README still documents only the init CLI command and has no Vercel deployment/build instructions. Repository standards require corresponding README updates when package functionality changes, and direct @workflow/nest consumers otherwise have no package-local documentation for this new command. Please update packages/nest/README.md with build, its options, the Vercel entry module, and the vercel-build flow.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated packages/nest/README.md (b047a12): added a "Deploying to Vercel" section (serverless entry module, skipBuild on Vercel, the vercel-build flow, native-addon limitation), documented the build command with an options table (--vercel, --dirs, --entry, --out-dir, --module), and noted the NestLocalBuilder/NestVercelBuilder subpaths in "How It Works".

…ive-addon limitation
- Changeset: bump @workflow/nest to major (removing NestLocalBuilder from the
main entry is a breaking change) and document the subpath migration + the
native-addon limitation.
- README: document the `build` command and its options, the Vercel entry
module and `vercel-build` flow, the NestVercelBuilder subpath, and the
native-addon limitation.
- vercel-builder: comment why `*.node` is externalized and that native addons
are not yet traced/copied into the deployed function.
- Minor wording: Workflow DevKit -> Workflow SDK.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment threadpackages/nest/README.md Outdated
Comment threadpackages/nest/README.md Outdated
```typescript
// _vercel/entry.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from '../src/app.module';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ai review: This entry bypasses the nest build output by importing ../src/app.module, so the Vercel builder feeds the Nest TypeScript graph back through esbuild. That contradicts the builder assumption that it bundles already-compiled JS, and esbuild does not emit the TypeScript emitDecoratorMetadata that Nest constructor injection relies on; apps with injected providers can therefore boot with missing DI metadata. Please import ../dist/app.module.js here, matching the checked-in workbench and the v5 guide, so the SWC-compiled decorator metadata is retained.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixed in 6e6f9ab. The entry example now imports the compiled ../dist/app.module.js (produced by nest build, whose SWC pass emits emitDecoratorMetadata) instead of raw ../src/app.module, and adds import 'reflect-metadata'. This matches the checked-in workbench/nest/_vercel/entry.ts and the getting-started docs, both of which already used dist/ + reflect-metadata.

…t + reflect-metadata
The entry snippet imported AppModule from raw `../src/app.module` and omitted
`import 'reflect-metadata'`. Importing raw TS routes the app back through esbuild
(which does not emit `emitDecoratorMetadata`), breaking NestJS constructor
injection; the entry must import the `nest build` output (`../dist/app.module.js`)
and register reflect-metadata — matching the workbench entry and getting-started
docs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actionsgithub-actionsBot mentioned this pull request Jul 20, 2026
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for 542138d (AI decision).

The new NestVercelBuilder depends on createWorkflowWorldTargetEsbuildPlugin from @workflow/builders (introduced in #2752), which does not exist on stable (verified via git grep on origin/stable), so the commit builds on main-only APIs and would not compile there. Additionally, the changeset marks @workflow/nest as a major breaking change (moving NestLocalBuilder off the package root), which is intended for the next major release rather than the stable v4 line.

To override, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

542138dc0b37f492fbf51b75e8ba4f57e291fc8f

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@VaguelySerious@karthikscale3
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' [nest] Fix NestJS Vercel build output by VaguelySerious · Pull Request #2988 · vercel/workflow · GitHub
Skip to content

[nest] Fix NestJS Vercel build output - #2988

Merged
VaguelySerious merged 8 commits into
mainfrom
peter/nest-vercel-support
Jul 20, 2026
Merged

[nest] Fix NestJS Vercel build output#2988
VaguelySerious merged 8 commits into
mainfrom
peter/nest-vercel-support

Conversation

@VaguelySerious

@VaguelySeriousVaguelySerious commented Jul 17, 2026

Copy link
Copy Markdown
Member

Closes#1472 and #878

NestJS currently produces no .vercel/output, so nothing emits a queue-consumer function with experimentalTriggers, and Vercel Queue never dispatches the run.

Every other server framework (Express/Fastify/Hono) sidesteps this by being a Nitro app whose Vercel preset emits the consumer. NestJS has its own compiler/DI and can't ride that path, so it needs to emit the Build Output itself.

Approach

Keep nest build (SWC compiles decorators + emitDecoratorMetadata natively). After it, workflow-nest build --vercel (also auto-detected via the VERCEL env var) writes a complete .vercel/output:

  • Consumer — the combined flow.func (+ webhook/[token].func, static manifest, routing) is produced by the shared VercelBuildOutputAPIBuilder, i.e. the exact code path Nitro/Next/etc. use, so it's registered with the __wkf_workflow_*experimentalTriggers and VQS discovers it. Single combined trigger — matches current arch.
  • App — the NestJS app (from the SWC-compiled dist/) is bundled as a catch-all function, with the workflow world statically injected (createWorkflowWorldTargetEsbuildPlugin) so start() works inside it.
  • Routing — merged into config.json: workflow routes + filesystem win, then fall through to the app.

WorkflowModule now lazy-loads the build toolchain (@workflow/builders, esbuild, SWC) so importing it no longer drags the compiler into the runtime serverless bundle. The runtime entry (workflow/nest) no longer re-exports the builders; they live at workflow/nest/builder and workflow/nest/vercel-builder.

This deliberately avoids the earlier #1472 hacks (base64-embedding bundles into source, /tmp writes, globalThis injection, separate step/flow triggers) — each now has a clean shared equivalent.

Testing

  1. Tarballed the SDK, scaffolded a throwaway NestJS app consuming the tarballs, deployed prebuilt to Vercel (team peter-w).
  2. POST /api/startwrun_…; the run completed (both steps + a sleep ran):
    { "status": "completed",
    "returnValue": { "hello": "Hello, vercel!", "bye": "Goodbye, vercel.", "completedAt": "" } }
    i.e. it no longer stays pending.
  • App-function bundle contains no build tooling (esbuild/SWC/@workflow/builders) and boots under VERCEL=1.
  • flow.func/.vc-config.json carries the __wkf_workflow_* queue trigger.
  • @workflow/nest unit tests pass (37).

Status / notes

  • Draft: NestJS is still labelled experimental; this makes Vercel deploys actually work and documents the flow. No E2E Vercel lane for Nest is wired yet — worth adding before calling it supported.
  • The Nest-app bundling uses an esbuild externals allowlist for optional Nest peers; @vercel/nft-style tracing is a more robust future option if the allowlist proves brittle.

Deployed NestJS workflows previously stayed `pending` forever: nothing emitted
a queue-consumer function for Vercel Queue to discover, so runs were never
dispatched. This adds a Vercel Build Output path for NestJS.
`workflow-nest build --vercel` (also triggered when the VERCEL env var is set)
now writes `.vercel/output`:
- the combined workflow queue-consumer `flow.func` is produced by the shared
`VercelBuildOutputAPIBuilder` — the same code path Nitro/Next/etc. use — so
it is registered with `experimentalTriggers` and discovered by VQS;
- the NestJS app is bundled (from the SWC-compiled `dist/`) as a catch-all
function with the workflow world statically injected;
- routing is merged into `config.json` (workflow routes + filesystem win, then
fall through to the app).
`WorkflowModule` now lazy-loads the build toolchain (@workflow/builders,
esbuild, SWC) so importing it no longer pulls the compiler into the runtime
serverless bundle. The builders remain available via the
`workflow/nest/builder` and `workflow/nest/vercel-builder` subpaths.
Verified end-to-end: deployed a NestJS app to Vercel and a triggered workflow
ran to completion (previously stuck pending).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@changeset-bot

changeset-botBot commented Jul 17, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 6e6f9ab

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 16 packages
NameType
@workflow/nestMajor
workflowPatch
@workflow/world-testingPatch
@workflow/corePatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
@workflow/astroPatch
@workflow/nuxtPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercelBot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

The benchmark run for 6e6f9ab failed. See the run logs for details.

commit f549c8e · Fri, 17 Jul 2026 20:47:00 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioAvg (ms)P10 (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1446 (+14%)1750 🔴1892 🔴2518 🔴30
TTFShook + stream1540 (+16%)1896 🔴2120 🔴2596 🔴30
STSO1020 steps (1-20)285 (-4.5%)315 🔴391 🔴448 🔴19
STSO1020 steps (101-120)288 (-6.5%)308 🔴366 🔴370 🔴19
STSO1020 steps (1001-1020)557 (-11%)571 🔴642 🔴719 🔴19
WOstream1446 (+14%)17501892251830
WOhook + stream1540 (+16%)18962120259630
SLstream4237 (+324%)5649 🔴5697 🔴5817 🔴30
SLhook + stream3765 (+93%)5520 🔴5564 🔴6027 🔴30
📜 Previous results (1)

dad0793

Fri, 17 Jul 2026 20:03:00 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P10 (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1321 (+4.3%)1629 🔴1677 🔴1788 🔴30
TTFShook + stream1533 (+15%)1913 🔴1963 🔴2142 🔴30
STSO1020 steps (1-20)265 (-11%)295 🔴364 🔴423 🔴19
STSO1020 steps (101-120)280 (-9.4%)301 🔴355 🔴392 🔴19
STSO1020 steps (1001-1020)578 (-7.6%)614 🔴778 🔴837 🔴19
WOstream1321 (+4.3%)16291677178830
WOhook + stream1533 (+15%)19131963214230
SLstream4495 (+350%)5794 🔴5836 🔴5922 🔴30
SLhook + stream4020 (+106%)5584 🔴5627 🔴5721 🔴30

Avg deltas compare against the most recent benchmark run on main at the time of this run.

Metrics — TTFS: time to first step body (in-deployment start() → first step body, deployment clocks) · STSO: step-to-step overhead (gap between consecutive step bodies) · WO: workflow overhead (whole-run time outside step bodies, in-deployment anchored) · SL: stream latency (in-deployment write → read propagation, readAt - writtenAt)

Scenarios — stream: one step that streams chunks back to the client; no hooks, so the run stays in turbo mode · hook + stream: registers a hook before the same streaming step, which exits turbo mode · 1020 steps: 1020 trivial sequential steps; STSO is measured between consecutive steps in the given step ranges

🟢/🔴 mark percentiles within/above target. Targets (p75/p90/p99, ms) — TTFS 200/300/600 · SL 50/60/125 · STSO (1-20) 20/30/60 · STSO (101-120) 30/45/90 · STSO (1001-1020) 40/60/120

All metrics are measured from deployment-side timestamps only. Runs are triggered by an in-deployment route that stamps the anchor (clientStart) right before start(), so the CI runner’s request and its path through api.vercel.com sit outside every measured window. TTFS = in-deployment start() → first step body (turbo uses the in-process fast path, non-turbo the dispatch path), and includes the VQS dispatch hop plus any /flow cold start. STSO/WO are measured between step bodies on the deployment. SL is measured inside the workflow (parallel reader/writer steps), so it no longer includes the api.vercel.com read path.

Cold starts are kept in the numbers on purpose — they are part of real bursty-workload latency. The workbench deployment cold-starts the /flow invocation for a large fraction of runs, inflating P75+; the P10 column shows the warm-start floor for comparison.

@github-actions

github-actionsBot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production145302301683
✅ 💻 Local Development161702191836
✅ 📦 Local Production161702191836
✅ 🐘 Local Postgres161702191836
✅ 🪟 Windows15300153
✅ 📋 Other89401771071
✅ vercel-multi-region270027
Total7378010648442

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro126027
✅ example126027
✅ express126027
✅ fastify126027
✅ hono126027
✅ nextjs-turbopack15003
✅ nextjs-webpack15003
✅ nitro126027
✅ nuxt126027
✅ sveltekit14508
✅ vite126027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15300
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable128025
✅ e2e-local-dev-tanstack-start-128025
✅ e2e-local-postgres-nest-stable128025
✅ e2e-local-postgres-tanstack-start-128025
✅ e2e-local-prod-nest-stable128025
✅ e2e-local-prod-tanstack-start-128025
✅ e2e-vercel-prod-tanstack-start126027
✅ vercel-multi-region
AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

Comment threadpackages/nest/src/index.ts
…` meta-package, breaking the documented `import { NestLocalBuilder } from "workflow/nest"` and leaving no working alternative.
This commit fixes the issue reported at packages/nest/src/index.ts:6
## The bug
This PR changed `packages/nest/src/index.ts` from a value+type export:
```ts
export { type NestBuilderOptions, NestLocalBuilder } from './builder.js';
```
to a type-only export:
```ts
export type { NestBuilderOptions } from './builder.js';
```
The rationale is valid — `builder.ts` imports `@workflow/builders` (esbuild/SWC) at top level, and the runtime entry must stay free of build-time deps so a NestJS app can be bundled into a serverless function. The `NestLocalBuilder` value is instead intended to be reached via the `workflow/nest/builder` subpath (as the new comment in `index.ts` states).
The problem is that the escape hatch was only half-wired:
- `@workflow/nest`'s own `package.json` *does* expose a `./builder` subpath (`./dist/builder.js`).
- But the recommended consumer package is the **meta `workflow` package**, whose `package.json` only exposes `./nest` → `./dist/nest.js`, and `packages/workflow/src/nest.ts` is just `export * from '@workflow/nest'` (the runtime entry that no longer value-exports `NestLocalBuilder`). There was **no** `./nest/builder` subpath in the meta package.
### Concrete trigger
A user following the API-reference docs writes:
```ts
import { NestLocalBuilder } from "workflow/nest";
```
(`docs/content/docs/v5/api-reference/workflow-nest/nest-local-builder.mdx` line 15 and the v4 equivalent.) At runtime this resolves to `@workflow/nest`'s main entry, which now exports only the *type* `NestBuilderOptions` — so `NestLocalBuilder` is `undefined`, and `new NestLocalBuilder(...)` throws `TypeError: NestLocalBuilder is not a constructor`. There was also no `workflow/nest/builder` subpath to fall back to, so the value class was completely unreachable through the `workflow` meta package.
## The fix
1. Added a `./nest/builder` subpath to `packages/workflow/package.json` mapping to `./dist/nest-builder.js`.
2. Created `packages/workflow/src/nest-builder.ts` = `export * from '@workflow/nest/builder'`, mirroring the existing `src/nest.ts` pattern, so the value export is threaded through the meta package and compiled to `dist/nest-builder.js`.
3. Updated the v4 and v5 `nest-local-builder.mdx` docs to the working import path `import { NestLocalBuilder } from "workflow/nest/builder"`.
This restores reachability of `NestLocalBuilder` for meta-package consumers while preserving the PR's goal of keeping build-time deps out of the runtime entry.
Note: the sandbox has no installed `node_modules`, so a full `tsc` build could not be executed; the change is structurally identical to the already-working `./nest` subpath wiring.
Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Co-authored-by: VaguelySerious <mittgfu@gmail.com>
@VaguelySeriousVaguelySerious changed the title [nest] Support deploying NestJS workflows to Vercel[nest] Fix NestJS Vercel build outputJul 17, 2026
@VaguelySerious
VaguelySerious marked this pull request as ready for review July 17, 2026 19:46
@VaguelySerious
VaguelySerious requested review from a team and ijjk as code ownersJuly 17, 2026 19:46
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
'@swc/core/*',
'@swc/wasm',
'esbuild',
'*.node',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ai review: *.node is still always externalized, but this builder does not trace or copy external files into appFuncDir; the emitted function contains only index.js, package.json, and .vc-config.json. Any Nest app dependency with a native addon therefore keeps a runtime reference to a .node file that is absent from the deployed .func, causing MODULE_NOT_FOUND on cold start. Please trace/copy native artifacts (and their package layout) into the function, or fail the build with a clear unsupported-dependency error instead of producing a broken deployment.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed as a documented limitation rather than adding native-module tracing, which is out of scope for this exploratory integration (reliably tracing/copying .node artifacts also requires handling the runtime resolution these modules do via bindings/node-gyp-build).

*.node stays externalized so esbuild does not fail the build on a .node file it cannot bundle; I added a comment at that line making the intentional choice and its consequence explicit (b047a12), and called out the limitation in the changeset-linked README section ("Deploying to Vercel") plus the changeset. If native-addon support is needed we can follow up with @vercel/nft tracing.

Comment thread.changeset/nest-vercel-support.md Outdated
@@ -0,0 +1,5 @@
---
'@workflow/nest': minor

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ai review: This is marked as a minor change, but the PR removes the public NestLocalBuilder value export from the @workflow/nest and workflow/nest main entries. Existing documented imports stop working; adding a new /builder subpath provides a migration target but does not preserve compatibility. Because this bump type also governs stable backports, please either retain a compatible main-entry export or mark the change as major.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bumped @workflow/nest to major (b047a12). Retaining the main-entry NestLocalBuilder export is not an option — it would re-drag the build toolchain (@workflow/builders, esbuild, SWC) into the runtime bundle of every app that imports WorkflowModule, which is the bug this PR fixes — so the subpath (@workflow/nest/builder) is the migration target and the changeset now documents it as a breaking change.


Commands:
init Generate .swcrc configuration with the workflow plugin
build Build workflow bundles (and the Vercel Build Output when on Vercel)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ai review: The package README still documents only the init CLI command and has no Vercel deployment/build instructions. Repository standards require corresponding README updates when package functionality changes, and direct @workflow/nest consumers otherwise have no package-local documentation for this new command. Please update packages/nest/README.md with build, its options, the Vercel entry module, and the vercel-build flow.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated packages/nest/README.md (b047a12): added a "Deploying to Vercel" section (serverless entry module, skipBuild on Vercel, the vercel-build flow, native-addon limitation), documented the build command with an options table (--vercel, --dirs, --entry, --out-dir, --module), and noted the NestLocalBuilder/NestVercelBuilder subpaths in "How It Works".

…ive-addon limitation
- Changeset: bump @workflow/nest to major (removing NestLocalBuilder from the
main entry is a breaking change) and document the subpath migration + the
native-addon limitation.
- README: document the `build` command and its options, the Vercel entry
module and `vercel-build` flow, the NestVercelBuilder subpath, and the
native-addon limitation.
- vercel-builder: comment why `*.node` is externalized and that native addons
are not yet traced/copied into the deployed function.
- Minor wording: Workflow DevKit -> Workflow SDK.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment threadpackages/nest/README.md Outdated
Comment threadpackages/nest/README.md Outdated
```typescript
// _vercel/entry.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from '../src/app.module';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ai review: This entry bypasses the nest build output by importing ../src/app.module, so the Vercel builder feeds the Nest TypeScript graph back through esbuild. That contradicts the builder assumption that it bundles already-compiled JS, and esbuild does not emit the TypeScript emitDecoratorMetadata that Nest constructor injection relies on; apps with injected providers can therefore boot with missing DI metadata. Please import ../dist/app.module.js here, matching the checked-in workbench and the v5 guide, so the SWC-compiled decorator metadata is retained.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixed in 6e6f9ab. The entry example now imports the compiled ../dist/app.module.js (produced by nest build, whose SWC pass emits emitDecoratorMetadata) instead of raw ../src/app.module, and adds import 'reflect-metadata'. This matches the checked-in workbench/nest/_vercel/entry.ts and the getting-started docs, both of which already used dist/ + reflect-metadata.

…t + reflect-metadata
The entry snippet imported AppModule from raw `../src/app.module` and omitted
`import 'reflect-metadata'`. Importing raw TS routes the app back through esbuild
(which does not emit `emitDecoratorMetadata`), breaking NestJS constructor
injection; the entry must import the `nest build` output (`../dist/app.module.js`)
and register reflect-metadata — matching the workbench entry and getting-started
docs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actionsgithub-actionsBot mentioned this pull request Jul 20, 2026
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for 542138d (AI decision).

The new NestVercelBuilder depends on createWorkflowWorldTargetEsbuildPlugin from @workflow/builders (introduced in #2752), which does not exist on stable (verified via git grep on origin/stable), so the commit builds on main-only APIs and would not compile there. Additionally, the changeset marks @workflow/nest as a major breaking change (moving NestLocalBuilder off the package root), which is intended for the next major release rather than the stable v4 line.

To override, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

542138dc0b37f492fbf51b75e8ba4f57e291fc8f

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@VaguelySerious@karthikscale3
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' [nest] Fix NestJS Vercel build output by VaguelySerious · Pull Request #2988 · vercel/workflow · GitHub
Skip to content

[nest] Fix NestJS Vercel build output - #2988

Merged
VaguelySerious merged 8 commits into
mainfrom
peter/nest-vercel-support
Jul 20, 2026
Merged

[nest] Fix NestJS Vercel build output#2988
VaguelySerious merged 8 commits into
mainfrom
peter/nest-vercel-support

Conversation

@VaguelySerious

@VaguelySeriousVaguelySerious commented Jul 17, 2026

Copy link
Copy Markdown
Member

Closes#1472 and #878

NestJS currently produces no .vercel/output, so nothing emits a queue-consumer function with experimentalTriggers, and Vercel Queue never dispatches the run.

Every other server framework (Express/Fastify/Hono) sidesteps this by being a Nitro app whose Vercel preset emits the consumer. NestJS has its own compiler/DI and can't ride that path, so it needs to emit the Build Output itself.

Approach

Keep nest build (SWC compiles decorators + emitDecoratorMetadata natively). After it, workflow-nest build --vercel (also auto-detected via the VERCEL env var) writes a complete .vercel/output:

  • Consumer — the combined flow.func (+ webhook/[token].func, static manifest, routing) is produced by the shared VercelBuildOutputAPIBuilder, i.e. the exact code path Nitro/Next/etc. use, so it's registered with the __wkf_workflow_*experimentalTriggers and VQS discovers it. Single combined trigger — matches current arch.
  • App — the NestJS app (from the SWC-compiled dist/) is bundled as a catch-all function, with the workflow world statically injected (createWorkflowWorldTargetEsbuildPlugin) so start() works inside it.
  • Routing — merged into config.json: workflow routes + filesystem win, then fall through to the app.

WorkflowModule now lazy-loads the build toolchain (@workflow/builders, esbuild, SWC) so importing it no longer drags the compiler into the runtime serverless bundle. The runtime entry (workflow/nest) no longer re-exports the builders; they live at workflow/nest/builder and workflow/nest/vercel-builder.

This deliberately avoids the earlier #1472 hacks (base64-embedding bundles into source, /tmp writes, globalThis injection, separate step/flow triggers) — each now has a clean shared equivalent.

Testing

  1. Tarballed the SDK, scaffolded a throwaway NestJS app consuming the tarballs, deployed prebuilt to Vercel (team peter-w).
  2. POST /api/startwrun_…; the run completed (both steps + a sleep ran):
    { "status": "completed",
    "returnValue": { "hello": "Hello, vercel!", "bye": "Goodbye, vercel.", "completedAt": "" } }
    i.e. it no longer stays pending.
  • App-function bundle contains no build tooling (esbuild/SWC/@workflow/builders) and boots under VERCEL=1.
  • flow.func/.vc-config.json carries the __wkf_workflow_* queue trigger.
  • @workflow/nest unit tests pass (37).

Status / notes

  • Draft: NestJS is still labelled experimental; this makes Vercel deploys actually work and documents the flow. No E2E Vercel lane for Nest is wired yet — worth adding before calling it supported.
  • The Nest-app bundling uses an esbuild externals allowlist for optional Nest peers; @vercel/nft-style tracing is a more robust future option if the allowlist proves brittle.

Deployed NestJS workflows previously stayed `pending` forever: nothing emitted
a queue-consumer function for Vercel Queue to discover, so runs were never
dispatched. This adds a Vercel Build Output path for NestJS.
`workflow-nest build --vercel` (also triggered when the VERCEL env var is set)
now writes `.vercel/output`:
- the combined workflow queue-consumer `flow.func` is produced by the shared
`VercelBuildOutputAPIBuilder` — the same code path Nitro/Next/etc. use — so
it is registered with `experimentalTriggers` and discovered by VQS;
- the NestJS app is bundled (from the SWC-compiled `dist/`) as a catch-all
function with the workflow world statically injected;
- routing is merged into `config.json` (workflow routes + filesystem win, then
fall through to the app).
`WorkflowModule` now lazy-loads the build toolchain (@workflow/builders,
esbuild, SWC) so importing it no longer pulls the compiler into the runtime
serverless bundle. The builders remain available via the
`workflow/nest/builder` and `workflow/nest/vercel-builder` subpaths.
Verified end-to-end: deployed a NestJS app to Vercel and a triggered workflow
ran to completion (previously stuck pending).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@changeset-bot

changeset-botBot commented Jul 17, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 6e6f9ab

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 16 packages
NameType
@workflow/nestMajor
workflowPatch
@workflow/world-testingPatch
@workflow/corePatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
@workflow/astroPatch
@workflow/nuxtPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercelBot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

The benchmark run for 6e6f9ab failed. See the run logs for details.

commit f549c8e · Fri, 17 Jul 2026 20:47:00 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioAvg (ms)P10 (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1446 (+14%)1750 🔴1892 🔴2518 🔴30
TTFShook + stream1540 (+16%)1896 🔴2120 🔴2596 🔴30
STSO1020 steps (1-20)285 (-4.5%)315 🔴391 🔴448 🔴19
STSO1020 steps (101-120)288 (-6.5%)308 🔴366 🔴370 🔴19
STSO1020 steps (1001-1020)557 (-11%)571 🔴642 🔴719 🔴19
WOstream1446 (+14%)17501892251830
WOhook + stream1540 (+16%)18962120259630
SLstream4237 (+324%)5649 🔴5697 🔴5817 🔴30
SLhook + stream3765 (+93%)5520 🔴5564 🔴6027 🔴30
📜 Previous results (1)

dad0793

Fri, 17 Jul 2026 20:03:00 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P10 (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1321 (+4.3%)1629 🔴1677 🔴1788 🔴30
TTFShook + stream1533 (+15%)1913 🔴1963 🔴2142 🔴30
STSO1020 steps (1-20)265 (-11%)295 🔴364 🔴423 🔴19
STSO1020 steps (101-120)280 (-9.4%)301 🔴355 🔴392 🔴19
STSO1020 steps (1001-1020)578 (-7.6%)614 🔴778 🔴837 🔴19
WOstream1321 (+4.3%)16291677178830
WOhook + stream1533 (+15%)19131963214230
SLstream4495 (+350%)5794 🔴5836 🔴5922 🔴30
SLhook + stream4020 (+106%)5584 🔴5627 🔴5721 🔴30

Avg deltas compare against the most recent benchmark run on main at the time of this run.

Metrics — TTFS: time to first step body (in-deployment start() → first step body, deployment clocks) · STSO: step-to-step overhead (gap between consecutive step bodies) · WO: workflow overhead (whole-run time outside step bodies, in-deployment anchored) · SL: stream latency (in-deployment write → read propagation, readAt - writtenAt)

Scenarios — stream: one step that streams chunks back to the client; no hooks, so the run stays in turbo mode · hook + stream: registers a hook before the same streaming step, which exits turbo mode · 1020 steps: 1020 trivial sequential steps; STSO is measured between consecutive steps in the given step ranges

🟢/🔴 mark percentiles within/above target. Targets (p75/p90/p99, ms) — TTFS 200/300/600 · SL 50/60/125 · STSO (1-20) 20/30/60 · STSO (101-120) 30/45/90 · STSO (1001-1020) 40/60/120

All metrics are measured from deployment-side timestamps only. Runs are triggered by an in-deployment route that stamps the anchor (clientStart) right before start(), so the CI runner’s request and its path through api.vercel.com sit outside every measured window. TTFS = in-deployment start() → first step body (turbo uses the in-process fast path, non-turbo the dispatch path), and includes the VQS dispatch hop plus any /flow cold start. STSO/WO are measured between step bodies on the deployment. SL is measured inside the workflow (parallel reader/writer steps), so it no longer includes the api.vercel.com read path.

Cold starts are kept in the numbers on purpose — they are part of real bursty-workload latency. The workbench deployment cold-starts the /flow invocation for a large fraction of runs, inflating P75+; the P10 column shows the warm-start floor for comparison.

@github-actions

github-actionsBot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production145302301683
✅ 💻 Local Development161702191836
✅ 📦 Local Production161702191836
✅ 🐘 Local Postgres161702191836
✅ 🪟 Windows15300153
✅ 📋 Other89401771071
✅ vercel-multi-region270027
Total7378010648442

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro126027
✅ example126027
✅ express126027
✅ fastify126027
✅ hono126027
✅ nextjs-turbopack15003
✅ nextjs-webpack15003
✅ nitro126027
✅ nuxt126027
✅ sveltekit14508
✅ vite126027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15300
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable128025
✅ e2e-local-dev-tanstack-start-128025
✅ e2e-local-postgres-nest-stable128025
✅ e2e-local-postgres-tanstack-start-128025
✅ e2e-local-prod-nest-stable128025
✅ e2e-local-prod-tanstack-start-128025
✅ e2e-vercel-prod-tanstack-start126027
✅ vercel-multi-region
AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

Comment threadpackages/nest/src/index.ts
…` meta-package, breaking the documented `import { NestLocalBuilder } from "workflow/nest"` and leaving no working alternative.
This commit fixes the issue reported at packages/nest/src/index.ts:6
## The bug
This PR changed `packages/nest/src/index.ts` from a value+type export:
```ts
export { type NestBuilderOptions, NestLocalBuilder } from './builder.js';
```
to a type-only export:
```ts
export type { NestBuilderOptions } from './builder.js';
```
The rationale is valid — `builder.ts` imports `@workflow/builders` (esbuild/SWC) at top level, and the runtime entry must stay free of build-time deps so a NestJS app can be bundled into a serverless function. The `NestLocalBuilder` value is instead intended to be reached via the `workflow/nest/builder` subpath (as the new comment in `index.ts` states).
The problem is that the escape hatch was only half-wired:
- `@workflow/nest`'s own `package.json` *does* expose a `./builder` subpath (`./dist/builder.js`).
- But the recommended consumer package is the **meta `workflow` package**, whose `package.json` only exposes `./nest` → `./dist/nest.js`, and `packages/workflow/src/nest.ts` is just `export * from '@workflow/nest'` (the runtime entry that no longer value-exports `NestLocalBuilder`). There was **no** `./nest/builder` subpath in the meta package.
### Concrete trigger
A user following the API-reference docs writes:
```ts
import { NestLocalBuilder } from "workflow/nest";
```
(`docs/content/docs/v5/api-reference/workflow-nest/nest-local-builder.mdx` line 15 and the v4 equivalent.) At runtime this resolves to `@workflow/nest`'s main entry, which now exports only the *type* `NestBuilderOptions` — so `NestLocalBuilder` is `undefined`, and `new NestLocalBuilder(...)` throws `TypeError: NestLocalBuilder is not a constructor`. There was also no `workflow/nest/builder` subpath to fall back to, so the value class was completely unreachable through the `workflow` meta package.
## The fix
1. Added a `./nest/builder` subpath to `packages/workflow/package.json` mapping to `./dist/nest-builder.js`.
2. Created `packages/workflow/src/nest-builder.ts` = `export * from '@workflow/nest/builder'`, mirroring the existing `src/nest.ts` pattern, so the value export is threaded through the meta package and compiled to `dist/nest-builder.js`.
3. Updated the v4 and v5 `nest-local-builder.mdx` docs to the working import path `import { NestLocalBuilder } from "workflow/nest/builder"`.
This restores reachability of `NestLocalBuilder` for meta-package consumers while preserving the PR's goal of keeping build-time deps out of the runtime entry.
Note: the sandbox has no installed `node_modules`, so a full `tsc` build could not be executed; the change is structurally identical to the already-working `./nest` subpath wiring.
Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Co-authored-by: VaguelySerious <mittgfu@gmail.com>
@VaguelySeriousVaguelySerious changed the title [nest] Support deploying NestJS workflows to Vercel[nest] Fix NestJS Vercel build outputJul 17, 2026
@VaguelySerious
VaguelySerious marked this pull request as ready for review July 17, 2026 19:46
@VaguelySerious
VaguelySerious requested review from a team and ijjk as code ownersJuly 17, 2026 19:46
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
'@swc/core/*',
'@swc/wasm',
'esbuild',
'*.node',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ai review: *.node is still always externalized, but this builder does not trace or copy external files into appFuncDir; the emitted function contains only index.js, package.json, and .vc-config.json. Any Nest app dependency with a native addon therefore keeps a runtime reference to a .node file that is absent from the deployed .func, causing MODULE_NOT_FOUND on cold start. Please trace/copy native artifacts (and their package layout) into the function, or fail the build with a clear unsupported-dependency error instead of producing a broken deployment.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed as a documented limitation rather than adding native-module tracing, which is out of scope for this exploratory integration (reliably tracing/copying .node artifacts also requires handling the runtime resolution these modules do via bindings/node-gyp-build).

*.node stays externalized so esbuild does not fail the build on a .node file it cannot bundle; I added a comment at that line making the intentional choice and its consequence explicit (b047a12), and called out the limitation in the changeset-linked README section ("Deploying to Vercel") plus the changeset. If native-addon support is needed we can follow up with @vercel/nft tracing.

Comment thread.changeset/nest-vercel-support.md Outdated
@@ -0,0 +1,5 @@
---
'@workflow/nest': minor

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ai review: This is marked as a minor change, but the PR removes the public NestLocalBuilder value export from the @workflow/nest and workflow/nest main entries. Existing documented imports stop working; adding a new /builder subpath provides a migration target but does not preserve compatibility. Because this bump type also governs stable backports, please either retain a compatible main-entry export or mark the change as major.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bumped @workflow/nest to major (b047a12). Retaining the main-entry NestLocalBuilder export is not an option — it would re-drag the build toolchain (@workflow/builders, esbuild, SWC) into the runtime bundle of every app that imports WorkflowModule, which is the bug this PR fixes — so the subpath (@workflow/nest/builder) is the migration target and the changeset now documents it as a breaking change.


Commands:
init Generate .swcrc configuration with the workflow plugin
build Build workflow bundles (and the Vercel Build Output when on Vercel)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ai review: The package README still documents only the init CLI command and has no Vercel deployment/build instructions. Repository standards require corresponding README updates when package functionality changes, and direct @workflow/nest consumers otherwise have no package-local documentation for this new command. Please update packages/nest/README.md with build, its options, the Vercel entry module, and the vercel-build flow.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated packages/nest/README.md (b047a12): added a "Deploying to Vercel" section (serverless entry module, skipBuild on Vercel, the vercel-build flow, native-addon limitation), documented the build command with an options table (--vercel, --dirs, --entry, --out-dir, --module), and noted the NestLocalBuilder/NestVercelBuilder subpaths in "How It Works".

…ive-addon limitation
- Changeset: bump @workflow/nest to major (removing NestLocalBuilder from the
main entry is a breaking change) and document the subpath migration + the
native-addon limitation.
- README: document the `build` command and its options, the Vercel entry
module and `vercel-build` flow, the NestVercelBuilder subpath, and the
native-addon limitation.
- vercel-builder: comment why `*.node` is externalized and that native addons
are not yet traced/copied into the deployed function.
- Minor wording: Workflow DevKit -> Workflow SDK.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment threadpackages/nest/README.md Outdated
Comment threadpackages/nest/README.md Outdated
```typescript
// _vercel/entry.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from '../src/app.module';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ai review: This entry bypasses the nest build output by importing ../src/app.module, so the Vercel builder feeds the Nest TypeScript graph back through esbuild. That contradicts the builder assumption that it bundles already-compiled JS, and esbuild does not emit the TypeScript emitDecoratorMetadata that Nest constructor injection relies on; apps with injected providers can therefore boot with missing DI metadata. Please import ../dist/app.module.js here, matching the checked-in workbench and the v5 guide, so the SWC-compiled decorator metadata is retained.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixed in 6e6f9ab. The entry example now imports the compiled ../dist/app.module.js (produced by nest build, whose SWC pass emits emitDecoratorMetadata) instead of raw ../src/app.module, and adds import 'reflect-metadata'. This matches the checked-in workbench/nest/_vercel/entry.ts and the getting-started docs, both of which already used dist/ + reflect-metadata.

…t + reflect-metadata
The entry snippet imported AppModule from raw `../src/app.module` and omitted
`import 'reflect-metadata'`. Importing raw TS routes the app back through esbuild
(which does not emit `emitDecoratorMetadata`), breaking NestJS constructor
injection; the entry must import the `nest build` output (`../dist/app.module.js`)
and register reflect-metadata — matching the workbench entry and getting-started
docs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actionsgithub-actionsBot mentioned this pull request Jul 20, 2026
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for 542138d (AI decision).

The new NestVercelBuilder depends on createWorkflowWorldTargetEsbuildPlugin from @workflow/builders (introduced in #2752), which does not exist on stable (verified via git grep on origin/stable), so the commit builds on main-only APIs and would not compile there. Additionally, the changeset marks @workflow/nest as a major breaking change (moving NestLocalBuilder off the package root), which is intended for the next major release rather than the stable v4 line.

To override, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

542138dc0b37f492fbf51b75e8ba4f57e291fc8f

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@VaguelySerious@karthikscale3
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' [nest] Fix NestJS Vercel build output by VaguelySerious · Pull Request #2988 · vercel/workflow · GitHub
Skip to content

[nest] Fix NestJS Vercel build output - #2988

Merged
VaguelySerious merged 8 commits into
mainfrom
peter/nest-vercel-support
Jul 20, 2026
Merged

[nest] Fix NestJS Vercel build output#2988
VaguelySerious merged 8 commits into
mainfrom
peter/nest-vercel-support

Conversation

@VaguelySerious

@VaguelySeriousVaguelySerious commented Jul 17, 2026

Copy link
Copy Markdown
Member

Closes#1472 and #878

NestJS currently produces no .vercel/output, so nothing emits a queue-consumer function with experimentalTriggers, and Vercel Queue never dispatches the run.

Every other server framework (Express/Fastify/Hono) sidesteps this by being a Nitro app whose Vercel preset emits the consumer. NestJS has its own compiler/DI and can't ride that path, so it needs to emit the Build Output itself.

Approach

Keep nest build (SWC compiles decorators + emitDecoratorMetadata natively). After it, workflow-nest build --vercel (also auto-detected via the VERCEL env var) writes a complete .vercel/output:

  • Consumer — the combined flow.func (+ webhook/[token].func, static manifest, routing) is produced by the shared VercelBuildOutputAPIBuilder, i.e. the exact code path Nitro/Next/etc. use, so it's registered with the __wkf_workflow_*experimentalTriggers and VQS discovers it. Single combined trigger — matches current arch.
  • App — the NestJS app (from the SWC-compiled dist/) is bundled as a catch-all function, with the workflow world statically injected (createWorkflowWorldTargetEsbuildPlugin) so start() works inside it.
  • Routing — merged into config.json: workflow routes + filesystem win, then fall through to the app.

WorkflowModule now lazy-loads the build toolchain (@workflow/builders, esbuild, SWC) so importing it no longer drags the compiler into the runtime serverless bundle. The runtime entry (workflow/nest) no longer re-exports the builders; they live at workflow/nest/builder and workflow/nest/vercel-builder.

This deliberately avoids the earlier #1472 hacks (base64-embedding bundles into source, /tmp writes, globalThis injection, separate step/flow triggers) — each now has a clean shared equivalent.

Testing

  1. Tarballed the SDK, scaffolded a throwaway NestJS app consuming the tarballs, deployed prebuilt to Vercel (team peter-w).
  2. POST /api/startwrun_…; the run completed (both steps + a sleep ran):
    { "status": "completed",
    "returnValue": { "hello": "Hello, vercel!", "bye": "Goodbye, vercel.", "completedAt": "" } }
    i.e. it no longer stays pending.
  • App-function bundle contains no build tooling (esbuild/SWC/@workflow/builders) and boots under VERCEL=1.
  • flow.func/.vc-config.json carries the __wkf_workflow_* queue trigger.
  • @workflow/nest unit tests pass (37).

Status / notes

  • Draft: NestJS is still labelled experimental; this makes Vercel deploys actually work and documents the flow. No E2E Vercel lane for Nest is wired yet — worth adding before calling it supported.
  • The Nest-app bundling uses an esbuild externals allowlist for optional Nest peers; @vercel/nft-style tracing is a more robust future option if the allowlist proves brittle.

Deployed NestJS workflows previously stayed `pending` forever: nothing emitted
a queue-consumer function for Vercel Queue to discover, so runs were never
dispatched. This adds a Vercel Build Output path for NestJS.
`workflow-nest build --vercel` (also triggered when the VERCEL env var is set)
now writes `.vercel/output`:
- the combined workflow queue-consumer `flow.func` is produced by the shared
`VercelBuildOutputAPIBuilder` — the same code path Nitro/Next/etc. use — so
it is registered with `experimentalTriggers` and discovered by VQS;
- the NestJS app is bundled (from the SWC-compiled `dist/`) as a catch-all
function with the workflow world statically injected;
- routing is merged into `config.json` (workflow routes + filesystem win, then
fall through to the app).
`WorkflowModule` now lazy-loads the build toolchain (@workflow/builders,
esbuild, SWC) so importing it no longer pulls the compiler into the runtime
serverless bundle. The builders remain available via the
`workflow/nest/builder` and `workflow/nest/vercel-builder` subpaths.
Verified end-to-end: deployed a NestJS app to Vercel and a triggered workflow
ran to completion (previously stuck pending).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@changeset-bot

changeset-botBot commented Jul 17, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 6e6f9ab

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 16 packages
NameType
@workflow/nestMajor
workflowPatch
@workflow/world-testingPatch
@workflow/corePatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
@workflow/astroPatch
@workflow/nuxtPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercelBot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

The benchmark run for 6e6f9ab failed. See the run logs for details.

commit f549c8e · Fri, 17 Jul 2026 20:47:00 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioAvg (ms)P10 (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1446 (+14%)1750 🔴1892 🔴2518 🔴30
TTFShook + stream1540 (+16%)1896 🔴2120 🔴2596 🔴30
STSO1020 steps (1-20)285 (-4.5%)315 🔴391 🔴448 🔴19
STSO1020 steps (101-120)288 (-6.5%)308 🔴366 🔴370 🔴19
STSO1020 steps (1001-1020)557 (-11%)571 🔴642 🔴719 🔴19
WOstream1446 (+14%)17501892251830
WOhook + stream1540 (+16%)18962120259630
SLstream4237 (+324%)5649 🔴5697 🔴5817 🔴30
SLhook + stream3765 (+93%)5520 🔴5564 🔴6027 🔴30
📜 Previous results (1)

dad0793

Fri, 17 Jul 2026 20:03:00 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P10 (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1321 (+4.3%)1629 🔴1677 🔴1788 🔴30
TTFShook + stream1533 (+15%)1913 🔴1963 🔴2142 🔴30
STSO1020 steps (1-20)265 (-11%)295 🔴364 🔴423 🔴19
STSO1020 steps (101-120)280 (-9.4%)301 🔴355 🔴392 🔴19
STSO1020 steps (1001-1020)578 (-7.6%)614 🔴778 🔴837 🔴19
WOstream1321 (+4.3%)16291677178830
WOhook + stream1533 (+15%)19131963214230
SLstream4495 (+350%)5794 🔴5836 🔴5922 🔴30
SLhook + stream4020 (+106%)5584 🔴5627 🔴5721 🔴30

Avg deltas compare against the most recent benchmark run on main at the time of this run.

Metrics — TTFS: time to first step body (in-deployment start() → first step body, deployment clocks) · STSO: step-to-step overhead (gap between consecutive step bodies) · WO: workflow overhead (whole-run time outside step bodies, in-deployment anchored) · SL: stream latency (in-deployment write → read propagation, readAt - writtenAt)

Scenarios — stream: one step that streams chunks back to the client; no hooks, so the run stays in turbo mode · hook + stream: registers a hook before the same streaming step, which exits turbo mode · 1020 steps: 1020 trivial sequential steps; STSO is measured between consecutive steps in the given step ranges

🟢/🔴 mark percentiles within/above target. Targets (p75/p90/p99, ms) — TTFS 200/300/600 · SL 50/60/125 · STSO (1-20) 20/30/60 · STSO (101-120) 30/45/90 · STSO (1001-1020) 40/60/120

All metrics are measured from deployment-side timestamps only. Runs are triggered by an in-deployment route that stamps the anchor (clientStart) right before start(), so the CI runner’s request and its path through api.vercel.com sit outside every measured window. TTFS = in-deployment start() → first step body (turbo uses the in-process fast path, non-turbo the dispatch path), and includes the VQS dispatch hop plus any /flow cold start. STSO/WO are measured between step bodies on the deployment. SL is measured inside the workflow (parallel reader/writer steps), so it no longer includes the api.vercel.com read path.

Cold starts are kept in the numbers on purpose — they are part of real bursty-workload latency. The workbench deployment cold-starts the /flow invocation for a large fraction of runs, inflating P75+; the P10 column shows the warm-start floor for comparison.

@github-actions

github-actionsBot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production145302301683
✅ 💻 Local Development161702191836
✅ 📦 Local Production161702191836
✅ 🐘 Local Postgres161702191836
✅ 🪟 Windows15300153
✅ 📋 Other89401771071
✅ vercel-multi-region270027
Total7378010648442

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro126027
✅ example126027
✅ express126027
✅ fastify126027
✅ hono126027
✅ nextjs-turbopack15003
✅ nextjs-webpack15003
✅ nitro126027
✅ nuxt126027
✅ sveltekit14508
✅ vite126027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15300
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable128025
✅ e2e-local-dev-tanstack-start-128025
✅ e2e-local-postgres-nest-stable128025
✅ e2e-local-postgres-tanstack-start-128025
✅ e2e-local-prod-nest-stable128025
✅ e2e-local-prod-tanstack-start-128025
✅ e2e-vercel-prod-tanstack-start126027
✅ vercel-multi-region
AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

Comment threadpackages/nest/src/index.ts
…` meta-package, breaking the documented `import { NestLocalBuilder } from "workflow/nest"` and leaving no working alternative.
This commit fixes the issue reported at packages/nest/src/index.ts:6
## The bug
This PR changed `packages/nest/src/index.ts` from a value+type export:
```ts
export { type NestBuilderOptions, NestLocalBuilder } from './builder.js';
```
to a type-only export:
```ts
export type { NestBuilderOptions } from './builder.js';
```
The rationale is valid — `builder.ts` imports `@workflow/builders` (esbuild/SWC) at top level, and the runtime entry must stay free of build-time deps so a NestJS app can be bundled into a serverless function. The `NestLocalBuilder` value is instead intended to be reached via the `workflow/nest/builder` subpath (as the new comment in `index.ts` states).
The problem is that the escape hatch was only half-wired:
- `@workflow/nest`'s own `package.json` *does* expose a `./builder` subpath (`./dist/builder.js`).
- But the recommended consumer package is the **meta `workflow` package**, whose `package.json` only exposes `./nest` → `./dist/nest.js`, and `packages/workflow/src/nest.ts` is just `export * from '@workflow/nest'` (the runtime entry that no longer value-exports `NestLocalBuilder`). There was **no** `./nest/builder` subpath in the meta package.
### Concrete trigger
A user following the API-reference docs writes:
```ts
import { NestLocalBuilder } from "workflow/nest";
```
(`docs/content/docs/v5/api-reference/workflow-nest/nest-local-builder.mdx` line 15 and the v4 equivalent.) At runtime this resolves to `@workflow/nest`'s main entry, which now exports only the *type* `NestBuilderOptions` — so `NestLocalBuilder` is `undefined`, and `new NestLocalBuilder(...)` throws `TypeError: NestLocalBuilder is not a constructor`. There was also no `workflow/nest/builder` subpath to fall back to, so the value class was completely unreachable through the `workflow` meta package.
## The fix
1. Added a `./nest/builder` subpath to `packages/workflow/package.json` mapping to `./dist/nest-builder.js`.
2. Created `packages/workflow/src/nest-builder.ts` = `export * from '@workflow/nest/builder'`, mirroring the existing `src/nest.ts` pattern, so the value export is threaded through the meta package and compiled to `dist/nest-builder.js`.
3. Updated the v4 and v5 `nest-local-builder.mdx` docs to the working import path `import { NestLocalBuilder } from "workflow/nest/builder"`.
This restores reachability of `NestLocalBuilder` for meta-package consumers while preserving the PR's goal of keeping build-time deps out of the runtime entry.
Note: the sandbox has no installed `node_modules`, so a full `tsc` build could not be executed; the change is structurally identical to the already-working `./nest` subpath wiring.
Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Co-authored-by: VaguelySerious <mittgfu@gmail.com>
@VaguelySeriousVaguelySerious changed the title [nest] Support deploying NestJS workflows to Vercel[nest] Fix NestJS Vercel build outputJul 17, 2026
@VaguelySerious
VaguelySerious marked this pull request as ready for review July 17, 2026 19:46
@VaguelySerious
VaguelySerious requested review from a team and ijjk as code ownersJuly 17, 2026 19:46
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
'@swc/core/*',
'@swc/wasm',
'esbuild',
'*.node',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ai review: *.node is still always externalized, but this builder does not trace or copy external files into appFuncDir; the emitted function contains only index.js, package.json, and .vc-config.json. Any Nest app dependency with a native addon therefore keeps a runtime reference to a .node file that is absent from the deployed .func, causing MODULE_NOT_FOUND on cold start. Please trace/copy native artifacts (and their package layout) into the function, or fail the build with a clear unsupported-dependency error instead of producing a broken deployment.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed as a documented limitation rather than adding native-module tracing, which is out of scope for this exploratory integration (reliably tracing/copying .node artifacts also requires handling the runtime resolution these modules do via bindings/node-gyp-build).

*.node stays externalized so esbuild does not fail the build on a .node file it cannot bundle; I added a comment at that line making the intentional choice and its consequence explicit (b047a12), and called out the limitation in the changeset-linked README section ("Deploying to Vercel") plus the changeset. If native-addon support is needed we can follow up with @vercel/nft tracing.

Comment thread.changeset/nest-vercel-support.md Outdated
@@ -0,0 +1,5 @@
---
'@workflow/nest': minor

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ai review: This is marked as a minor change, but the PR removes the public NestLocalBuilder value export from the @workflow/nest and workflow/nest main entries. Existing documented imports stop working; adding a new /builder subpath provides a migration target but does not preserve compatibility. Because this bump type also governs stable backports, please either retain a compatible main-entry export or mark the change as major.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bumped @workflow/nest to major (b047a12). Retaining the main-entry NestLocalBuilder export is not an option — it would re-drag the build toolchain (@workflow/builders, esbuild, SWC) into the runtime bundle of every app that imports WorkflowModule, which is the bug this PR fixes — so the subpath (@workflow/nest/builder) is the migration target and the changeset now documents it as a breaking change.


Commands:
init Generate .swcrc configuration with the workflow plugin
build Build workflow bundles (and the Vercel Build Output when on Vercel)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ai review: The package README still documents only the init CLI command and has no Vercel deployment/build instructions. Repository standards require corresponding README updates when package functionality changes, and direct @workflow/nest consumers otherwise have no package-local documentation for this new command. Please update packages/nest/README.md with build, its options, the Vercel entry module, and the vercel-build flow.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated packages/nest/README.md (b047a12): added a "Deploying to Vercel" section (serverless entry module, skipBuild on Vercel, the vercel-build flow, native-addon limitation), documented the build command with an options table (--vercel, --dirs, --entry, --out-dir, --module), and noted the NestLocalBuilder/NestVercelBuilder subpaths in "How It Works".

…ive-addon limitation
- Changeset: bump @workflow/nest to major (removing NestLocalBuilder from the
main entry is a breaking change) and document the subpath migration + the
native-addon limitation.
- README: document the `build` command and its options, the Vercel entry
module and `vercel-build` flow, the NestVercelBuilder subpath, and the
native-addon limitation.
- vercel-builder: comment why `*.node` is externalized and that native addons
are not yet traced/copied into the deployed function.
- Minor wording: Workflow DevKit -> Workflow SDK.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment threadpackages/nest/README.md Outdated
Comment threadpackages/nest/README.md Outdated
```typescript
// _vercel/entry.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from '../src/app.module';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ai review: This entry bypasses the nest build output by importing ../src/app.module, so the Vercel builder feeds the Nest TypeScript graph back through esbuild. That contradicts the builder assumption that it bundles already-compiled JS, and esbuild does not emit the TypeScript emitDecoratorMetadata that Nest constructor injection relies on; apps with injected providers can therefore boot with missing DI metadata. Please import ../dist/app.module.js here, matching the checked-in workbench and the v5 guide, so the SWC-compiled decorator metadata is retained.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixed in 6e6f9ab. The entry example now imports the compiled ../dist/app.module.js (produced by nest build, whose SWC pass emits emitDecoratorMetadata) instead of raw ../src/app.module, and adds import 'reflect-metadata'. This matches the checked-in workbench/nest/_vercel/entry.ts and the getting-started docs, both of which already used dist/ + reflect-metadata.

…t + reflect-metadata
The entry snippet imported AppModule from raw `../src/app.module` and omitted
`import 'reflect-metadata'`. Importing raw TS routes the app back through esbuild
(which does not emit `emitDecoratorMetadata`), breaking NestJS constructor
injection; the entry must import the `nest build` output (`../dist/app.module.js`)
and register reflect-metadata — matching the workbench entry and getting-started
docs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actionsgithub-actionsBot mentioned this pull request Jul 20, 2026
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for 542138d (AI decision).

The new NestVercelBuilder depends on createWorkflowWorldTargetEsbuildPlugin from @workflow/builders (introduced in #2752), which does not exist on stable (verified via git grep on origin/stable), so the commit builds on main-only APIs and would not compile there. Additionally, the changeset marks @workflow/nest as a major breaking change (moving NestLocalBuilder off the package root), which is intended for the next major release rather than the stable v4 line.

To override, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

542138dc0b37f492fbf51b75e8ba4f57e291fc8f

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@VaguelySerious@karthikscale3
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' [nest] Fix NestJS Vercel build output by VaguelySerious · Pull Request #2988 · vercel/workflow · GitHub
Skip to content

[nest] Fix NestJS Vercel build output - #2988

Merged
VaguelySerious merged 8 commits into
mainfrom
peter/nest-vercel-support
Jul 20, 2026
Merged

[nest] Fix NestJS Vercel build output#2988
VaguelySerious merged 8 commits into
mainfrom
peter/nest-vercel-support

Conversation

@VaguelySerious

@VaguelySeriousVaguelySerious commented Jul 17, 2026

Copy link
Copy Markdown
Member

Closes#1472 and #878

NestJS currently produces no .vercel/output, so nothing emits a queue-consumer function with experimentalTriggers, and Vercel Queue never dispatches the run.

Every other server framework (Express/Fastify/Hono) sidesteps this by being a Nitro app whose Vercel preset emits the consumer. NestJS has its own compiler/DI and can't ride that path, so it needs to emit the Build Output itself.

Approach

Keep nest build (SWC compiles decorators + emitDecoratorMetadata natively). After it, workflow-nest build --vercel (also auto-detected via the VERCEL env var) writes a complete .vercel/output:

  • Consumer — the combined flow.func (+ webhook/[token].func, static manifest, routing) is produced by the shared VercelBuildOutputAPIBuilder, i.e. the exact code path Nitro/Next/etc. use, so it's registered with the __wkf_workflow_*experimentalTriggers and VQS discovers it. Single combined trigger — matches current arch.
  • App — the NestJS app (from the SWC-compiled dist/) is bundled as a catch-all function, with the workflow world statically injected (createWorkflowWorldTargetEsbuildPlugin) so start() works inside it.
  • Routing — merged into config.json: workflow routes + filesystem win, then fall through to the app.

WorkflowModule now lazy-loads the build toolchain (@workflow/builders, esbuild, SWC) so importing it no longer drags the compiler into the runtime serverless bundle. The runtime entry (workflow/nest) no longer re-exports the builders; they live at workflow/nest/builder and workflow/nest/vercel-builder.

This deliberately avoids the earlier #1472 hacks (base64-embedding bundles into source, /tmp writes, globalThis injection, separate step/flow triggers) — each now has a clean shared equivalent.

Testing

  1. Tarballed the SDK, scaffolded a throwaway NestJS app consuming the tarballs, deployed prebuilt to Vercel (team peter-w).
  2. POST /api/startwrun_…; the run completed (both steps + a sleep ran):
    { "status": "completed",
    "returnValue": { "hello": "Hello, vercel!", "bye": "Goodbye, vercel.", "completedAt": "" } }
    i.e. it no longer stays pending.
  • App-function bundle contains no build tooling (esbuild/SWC/@workflow/builders) and boots under VERCEL=1.
  • flow.func/.vc-config.json carries the __wkf_workflow_* queue trigger.
  • @workflow/nest unit tests pass (37).

Status / notes

  • Draft: NestJS is still labelled experimental; this makes Vercel deploys actually work and documents the flow. No E2E Vercel lane for Nest is wired yet — worth adding before calling it supported.
  • The Nest-app bundling uses an esbuild externals allowlist for optional Nest peers; @vercel/nft-style tracing is a more robust future option if the allowlist proves brittle.

Deployed NestJS workflows previously stayed `pending` forever: nothing emitted
a queue-consumer function for Vercel Queue to discover, so runs were never
dispatched. This adds a Vercel Build Output path for NestJS.
`workflow-nest build --vercel` (also triggered when the VERCEL env var is set)
now writes `.vercel/output`:
- the combined workflow queue-consumer `flow.func` is produced by the shared
`VercelBuildOutputAPIBuilder` — the same code path Nitro/Next/etc. use — so
it is registered with `experimentalTriggers` and discovered by VQS;
- the NestJS app is bundled (from the SWC-compiled `dist/`) as a catch-all
function with the workflow world statically injected;
- routing is merged into `config.json` (workflow routes + filesystem win, then
fall through to the app).
`WorkflowModule` now lazy-loads the build toolchain (@workflow/builders,
esbuild, SWC) so importing it no longer pulls the compiler into the runtime
serverless bundle. The builders remain available via the
`workflow/nest/builder` and `workflow/nest/vercel-builder` subpaths.
Verified end-to-end: deployed a NestJS app to Vercel and a triggered workflow
ran to completion (previously stuck pending).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@changeset-bot

changeset-botBot commented Jul 17, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 6e6f9ab

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 16 packages
NameType
@workflow/nestMajor
workflowPatch
@workflow/world-testingPatch
@workflow/corePatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
@workflow/astroPatch
@workflow/nuxtPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercelBot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

The benchmark run for 6e6f9ab failed. See the run logs for details.

commit f549c8e · Fri, 17 Jul 2026 20:47:00 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioAvg (ms)P10 (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1446 (+14%)1750 🔴1892 🔴2518 🔴30
TTFShook + stream1540 (+16%)1896 🔴2120 🔴2596 🔴30
STSO1020 steps (1-20)285 (-4.5%)315 🔴391 🔴448 🔴19
STSO1020 steps (101-120)288 (-6.5%)308 🔴366 🔴370 🔴19
STSO1020 steps (1001-1020)557 (-11%)571 🔴642 🔴719 🔴19
WOstream1446 (+14%)17501892251830
WOhook + stream1540 (+16%)18962120259630
SLstream4237 (+324%)5649 🔴5697 🔴5817 🔴30
SLhook + stream3765 (+93%)5520 🔴5564 🔴6027 🔴30
📜 Previous results (1)

dad0793

Fri, 17 Jul 2026 20:03:00 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P10 (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1321 (+4.3%)1629 🔴1677 🔴1788 🔴30
TTFShook + stream1533 (+15%)1913 🔴1963 🔴2142 🔴30
STSO1020 steps (1-20)265 (-11%)295 🔴364 🔴423 🔴19
STSO1020 steps (101-120)280 (-9.4%)301 🔴355 🔴392 🔴19
STSO1020 steps (1001-1020)578 (-7.6%)614 🔴778 🔴837 🔴19
WOstream1321 (+4.3%)16291677178830
WOhook + stream1533 (+15%)19131963214230
SLstream4495 (+350%)5794 🔴5836 🔴5922 🔴30
SLhook + stream4020 (+106%)5584 🔴5627 🔴5721 🔴30

Avg deltas compare against the most recent benchmark run on main at the time of this run.

Metrics — TTFS: time to first step body (in-deployment start() → first step body, deployment clocks) · STSO: step-to-step overhead (gap between consecutive step bodies) · WO: workflow overhead (whole-run time outside step bodies, in-deployment anchored) · SL: stream latency (in-deployment write → read propagation, readAt - writtenAt)

Scenarios — stream: one step that streams chunks back to the client; no hooks, so the run stays in turbo mode · hook + stream: registers a hook before the same streaming step, which exits turbo mode · 1020 steps: 1020 trivial sequential steps; STSO is measured between consecutive steps in the given step ranges

🟢/🔴 mark percentiles within/above target. Targets (p75/p90/p99, ms) — TTFS 200/300/600 · SL 50/60/125 · STSO (1-20) 20/30/60 · STSO (101-120) 30/45/90 · STSO (1001-1020) 40/60/120

All metrics are measured from deployment-side timestamps only. Runs are triggered by an in-deployment route that stamps the anchor (clientStart) right before start(), so the CI runner’s request and its path through api.vercel.com sit outside every measured window. TTFS = in-deployment start() → first step body (turbo uses the in-process fast path, non-turbo the dispatch path), and includes the VQS dispatch hop plus any /flow cold start. STSO/WO are measured between step bodies on the deployment. SL is measured inside the workflow (parallel reader/writer steps), so it no longer includes the api.vercel.com read path.

Cold starts are kept in the numbers on purpose — they are part of real bursty-workload latency. The workbench deployment cold-starts the /flow invocation for a large fraction of runs, inflating P75+; the P10 column shows the warm-start floor for comparison.

@github-actions

github-actionsBot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production145302301683
✅ 💻 Local Development161702191836
✅ 📦 Local Production161702191836
✅ 🐘 Local Postgres161702191836
✅ 🪟 Windows15300153
✅ 📋 Other89401771071
✅ vercel-multi-region270027
Total7378010648442

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro126027
✅ example126027
✅ express126027
✅ fastify126027
✅ hono126027
✅ nextjs-turbopack15003
✅ nextjs-webpack15003
✅ nitro126027
✅ nuxt126027
✅ sveltekit14508
✅ vite126027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15300
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable128025
✅ e2e-local-dev-tanstack-start-128025
✅ e2e-local-postgres-nest-stable128025
✅ e2e-local-postgres-tanstack-start-128025
✅ e2e-local-prod-nest-stable128025
✅ e2e-local-prod-tanstack-start-128025
✅ e2e-vercel-prod-tanstack-start126027
✅ vercel-multi-region
AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

Comment threadpackages/nest/src/index.ts
…` meta-package, breaking the documented `import { NestLocalBuilder } from "workflow/nest"` and leaving no working alternative.
This commit fixes the issue reported at packages/nest/src/index.ts:6
## The bug
This PR changed `packages/nest/src/index.ts` from a value+type export:
```ts
export { type NestBuilderOptions, NestLocalBuilder } from './builder.js';
```
to a type-only export:
```ts
export type { NestBuilderOptions } from './builder.js';
```
The rationale is valid — `builder.ts` imports `@workflow/builders` (esbuild/SWC) at top level, and the runtime entry must stay free of build-time deps so a NestJS app can be bundled into a serverless function. The `NestLocalBuilder` value is instead intended to be reached via the `workflow/nest/builder` subpath (as the new comment in `index.ts` states).
The problem is that the escape hatch was only half-wired:
- `@workflow/nest`'s own `package.json` *does* expose a `./builder` subpath (`./dist/builder.js`).
- But the recommended consumer package is the **meta `workflow` package**, whose `package.json` only exposes `./nest` → `./dist/nest.js`, and `packages/workflow/src/nest.ts` is just `export * from '@workflow/nest'` (the runtime entry that no longer value-exports `NestLocalBuilder`). There was **no** `./nest/builder` subpath in the meta package.
### Concrete trigger
A user following the API-reference docs writes:
```ts
import { NestLocalBuilder } from "workflow/nest";
```
(`docs/content/docs/v5/api-reference/workflow-nest/nest-local-builder.mdx` line 15 and the v4 equivalent.) At runtime this resolves to `@workflow/nest`'s main entry, which now exports only the *type* `NestBuilderOptions` — so `NestLocalBuilder` is `undefined`, and `new NestLocalBuilder(...)` throws `TypeError: NestLocalBuilder is not a constructor`. There was also no `workflow/nest/builder` subpath to fall back to, so the value class was completely unreachable through the `workflow` meta package.
## The fix
1. Added a `./nest/builder` subpath to `packages/workflow/package.json` mapping to `./dist/nest-builder.js`.
2. Created `packages/workflow/src/nest-builder.ts` = `export * from '@workflow/nest/builder'`, mirroring the existing `src/nest.ts` pattern, so the value export is threaded through the meta package and compiled to `dist/nest-builder.js`.
3. Updated the v4 and v5 `nest-local-builder.mdx` docs to the working import path `import { NestLocalBuilder } from "workflow/nest/builder"`.
This restores reachability of `NestLocalBuilder` for meta-package consumers while preserving the PR's goal of keeping build-time deps out of the runtime entry.
Note: the sandbox has no installed `node_modules`, so a full `tsc` build could not be executed; the change is structurally identical to the already-working `./nest` subpath wiring.
Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Co-authored-by: VaguelySerious <mittgfu@gmail.com>
@VaguelySeriousVaguelySerious changed the title [nest] Support deploying NestJS workflows to Vercel[nest] Fix NestJS Vercel build outputJul 17, 2026
@VaguelySerious
VaguelySerious marked this pull request as ready for review July 17, 2026 19:46
@VaguelySerious
VaguelySerious requested review from a team and ijjk as code ownersJuly 17, 2026 19:46
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
'@swc/core/*',
'@swc/wasm',
'esbuild',
'*.node',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ai review: *.node is still always externalized, but this builder does not trace or copy external files into appFuncDir; the emitted function contains only index.js, package.json, and .vc-config.json. Any Nest app dependency with a native addon therefore keeps a runtime reference to a .node file that is absent from the deployed .func, causing MODULE_NOT_FOUND on cold start. Please trace/copy native artifacts (and their package layout) into the function, or fail the build with a clear unsupported-dependency error instead of producing a broken deployment.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed as a documented limitation rather than adding native-module tracing, which is out of scope for this exploratory integration (reliably tracing/copying .node artifacts also requires handling the runtime resolution these modules do via bindings/node-gyp-build).

*.node stays externalized so esbuild does not fail the build on a .node file it cannot bundle; I added a comment at that line making the intentional choice and its consequence explicit (b047a12), and called out the limitation in the changeset-linked README section ("Deploying to Vercel") plus the changeset. If native-addon support is needed we can follow up with @vercel/nft tracing.

Comment thread.changeset/nest-vercel-support.md Outdated
@@ -0,0 +1,5 @@
---
'@workflow/nest': minor

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ai review: This is marked as a minor change, but the PR removes the public NestLocalBuilder value export from the @workflow/nest and workflow/nest main entries. Existing documented imports stop working; adding a new /builder subpath provides a migration target but does not preserve compatibility. Because this bump type also governs stable backports, please either retain a compatible main-entry export or mark the change as major.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bumped @workflow/nest to major (b047a12). Retaining the main-entry NestLocalBuilder export is not an option — it would re-drag the build toolchain (@workflow/builders, esbuild, SWC) into the runtime bundle of every app that imports WorkflowModule, which is the bug this PR fixes — so the subpath (@workflow/nest/builder) is the migration target and the changeset now documents it as a breaking change.


Commands:
init Generate .swcrc configuration with the workflow plugin
build Build workflow bundles (and the Vercel Build Output when on Vercel)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ai review: The package README still documents only the init CLI command and has no Vercel deployment/build instructions. Repository standards require corresponding README updates when package functionality changes, and direct @workflow/nest consumers otherwise have no package-local documentation for this new command. Please update packages/nest/README.md with build, its options, the Vercel entry module, and the vercel-build flow.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated packages/nest/README.md (b047a12): added a "Deploying to Vercel" section (serverless entry module, skipBuild on Vercel, the vercel-build flow, native-addon limitation), documented the build command with an options table (--vercel, --dirs, --entry, --out-dir, --module), and noted the NestLocalBuilder/NestVercelBuilder subpaths in "How It Works".

…ive-addon limitation
- Changeset: bump @workflow/nest to major (removing NestLocalBuilder from the
main entry is a breaking change) and document the subpath migration + the
native-addon limitation.
- README: document the `build` command and its options, the Vercel entry
module and `vercel-build` flow, the NestVercelBuilder subpath, and the
native-addon limitation.
- vercel-builder: comment why `*.node` is externalized and that native addons
are not yet traced/copied into the deployed function.
- Minor wording: Workflow DevKit -> Workflow SDK.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment threadpackages/nest/README.md Outdated
Comment threadpackages/nest/README.md Outdated
```typescript
// _vercel/entry.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from '../src/app.module';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ai review: This entry bypasses the nest build output by importing ../src/app.module, so the Vercel builder feeds the Nest TypeScript graph back through esbuild. That contradicts the builder assumption that it bundles already-compiled JS, and esbuild does not emit the TypeScript emitDecoratorMetadata that Nest constructor injection relies on; apps with injected providers can therefore boot with missing DI metadata. Please import ../dist/app.module.js here, matching the checked-in workbench and the v5 guide, so the SWC-compiled decorator metadata is retained.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixed in 6e6f9ab. The entry example now imports the compiled ../dist/app.module.js (produced by nest build, whose SWC pass emits emitDecoratorMetadata) instead of raw ../src/app.module, and adds import 'reflect-metadata'. This matches the checked-in workbench/nest/_vercel/entry.ts and the getting-started docs, both of which already used dist/ + reflect-metadata.

…t + reflect-metadata
The entry snippet imported AppModule from raw `../src/app.module` and omitted
`import 'reflect-metadata'`. Importing raw TS routes the app back through esbuild
(which does not emit `emitDecoratorMetadata`), breaking NestJS constructor
injection; the entry must import the `nest build` output (`../dist/app.module.js`)
and register reflect-metadata — matching the workbench entry and getting-started
docs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actionsgithub-actionsBot mentioned this pull request Jul 20, 2026
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for 542138d (AI decision).

The new NestVercelBuilder depends on createWorkflowWorldTargetEsbuildPlugin from @workflow/builders (introduced in #2752), which does not exist on stable (verified via git grep on origin/stable), so the commit builds on main-only APIs and would not compile there. Additionally, the changeset marks @workflow/nest as a major breaking change (moving NestLocalBuilder off the package root), which is intended for the next major release rather than the stable v4 line.

To override, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

542138dc0b37f492fbf51b75e8ba4f57e291fc8f

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@VaguelySerious@karthikscale3
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); [nest] Fix NestJS Vercel build output by VaguelySerious · Pull Request #2988 · vercel/workflow · GitHub
Skip to content

[nest] Fix NestJS Vercel build output - #2988

Merged
VaguelySerious merged 8 commits into
mainfrom
peter/nest-vercel-support
Jul 20, 2026
Merged

[nest] Fix NestJS Vercel build output#2988
VaguelySerious merged 8 commits into
mainfrom
peter/nest-vercel-support

Conversation

@VaguelySerious

@VaguelySeriousVaguelySerious commented Jul 17, 2026

Copy link
Copy Markdown
Member

Closes#1472 and #878

NestJS currently produces no .vercel/output, so nothing emits a queue-consumer function with experimentalTriggers, and Vercel Queue never dispatches the run.

Every other server framework (Express/Fastify/Hono) sidesteps this by being a Nitro app whose Vercel preset emits the consumer. NestJS has its own compiler/DI and can't ride that path, so it needs to emit the Build Output itself.

Approach

Keep nest build (SWC compiles decorators + emitDecoratorMetadata natively). After it, workflow-nest build --vercel (also auto-detected via the VERCEL env var) writes a complete .vercel/output:

  • Consumer — the combined flow.func (+ webhook/[token].func, static manifest, routing) is produced by the shared VercelBuildOutputAPIBuilder, i.e. the exact code path Nitro/Next/etc. use, so it's registered with the __wkf_workflow_*experimentalTriggers and VQS discovers it. Single combined trigger — matches current arch.
  • App — the NestJS app (from the SWC-compiled dist/) is bundled as a catch-all function, with the workflow world statically injected (createWorkflowWorldTargetEsbuildPlugin) so start() works inside it.
  • Routing — merged into config.json: workflow routes + filesystem win, then fall through to the app.

WorkflowModule now lazy-loads the build toolchain (@workflow/builders, esbuild, SWC) so importing it no longer drags the compiler into the runtime serverless bundle. The runtime entry (workflow/nest) no longer re-exports the builders; they live at workflow/nest/builder and workflow/nest/vercel-builder.

This deliberately avoids the earlier #1472 hacks (base64-embedding bundles into source, /tmp writes, globalThis injection, separate step/flow triggers) — each now has a clean shared equivalent.

Testing

  1. Tarballed the SDK, scaffolded a throwaway NestJS app consuming the tarballs, deployed prebuilt to Vercel (team peter-w).
  2. POST /api/startwrun_…; the run completed (both steps + a sleep ran):
    { "status": "completed",
    "returnValue": { "hello": "Hello, vercel!", "bye": "Goodbye, vercel.", "completedAt": "" } }
    i.e. it no longer stays pending.
  • App-function bundle contains no build tooling (esbuild/SWC/@workflow/builders) and boots under VERCEL=1.
  • flow.func/.vc-config.json carries the __wkf_workflow_* queue trigger.
  • @workflow/nest unit tests pass (37).

Status / notes

  • Draft: NestJS is still labelled experimental; this makes Vercel deploys actually work and documents the flow. No E2E Vercel lane for Nest is wired yet — worth adding before calling it supported.
  • The Nest-app bundling uses an esbuild externals allowlist for optional Nest peers; @vercel/nft-style tracing is a more robust future option if the allowlist proves brittle.

Deployed NestJS workflows previously stayed `pending` forever: nothing emitted
a queue-consumer function for Vercel Queue to discover, so runs were never
dispatched. This adds a Vercel Build Output path for NestJS.
`workflow-nest build --vercel` (also triggered when the VERCEL env var is set)
now writes `.vercel/output`:
- the combined workflow queue-consumer `flow.func` is produced by the shared
`VercelBuildOutputAPIBuilder` — the same code path Nitro/Next/etc. use — so
it is registered with `experimentalTriggers` and discovered by VQS;
- the NestJS app is bundled (from the SWC-compiled `dist/`) as a catch-all
function with the workflow world statically injected;
- routing is merged into `config.json` (workflow routes + filesystem win, then
fall through to the app).
`WorkflowModule` now lazy-loads the build toolchain (@workflow/builders,
esbuild, SWC) so importing it no longer pulls the compiler into the runtime
serverless bundle. The builders remain available via the
`workflow/nest/builder` and `workflow/nest/vercel-builder` subpaths.
Verified end-to-end: deployed a NestJS app to Vercel and a triggered workflow
ran to completion (previously stuck pending).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@changeset-bot

changeset-botBot commented Jul 17, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 6e6f9ab

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 16 packages
NameType
@workflow/nestMajor
workflowPatch
@workflow/world-testingPatch
@workflow/corePatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
@workflow/astroPatch
@workflow/nuxtPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercelBot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

The benchmark run for 6e6f9ab failed. See the run logs for details.

commit f549c8e · Fri, 17 Jul 2026 20:47:00 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioAvg (ms)P10 (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1446 (+14%)1750 🔴1892 🔴2518 🔴30
TTFShook + stream1540 (+16%)1896 🔴2120 🔴2596 🔴30
STSO1020 steps (1-20)285 (-4.5%)315 🔴391 🔴448 🔴19
STSO1020 steps (101-120)288 (-6.5%)308 🔴366 🔴370 🔴19
STSO1020 steps (1001-1020)557 (-11%)571 🔴642 🔴719 🔴19
WOstream1446 (+14%)17501892251830
WOhook + stream1540 (+16%)18962120259630
SLstream4237 (+324%)5649 🔴5697 🔴5817 🔴30
SLhook + stream3765 (+93%)5520 🔴5564 🔴6027 🔴30
📜 Previous results (1)

dad0793

Fri, 17 Jul 2026 20:03:00 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P10 (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1321 (+4.3%)1629 🔴1677 🔴1788 🔴30
TTFShook + stream1533 (+15%)1913 🔴1963 🔴2142 🔴30
STSO1020 steps (1-20)265 (-11%)295 🔴364 🔴423 🔴19
STSO1020 steps (101-120)280 (-9.4%)301 🔴355 🔴392 🔴19
STSO1020 steps (1001-1020)578 (-7.6%)614 🔴778 🔴837 🔴19
WOstream1321 (+4.3%)16291677178830
WOhook + stream1533 (+15%)19131963214230
SLstream4495 (+350%)5794 🔴5836 🔴5922 🔴30
SLhook + stream4020 (+106%)5584 🔴5627 🔴5721 🔴30

Avg deltas compare against the most recent benchmark run on main at the time of this run.

Metrics — TTFS: time to first step body (in-deployment start() → first step body, deployment clocks) · STSO: step-to-step overhead (gap between consecutive step bodies) · WO: workflow overhead (whole-run time outside step bodies, in-deployment anchored) · SL: stream latency (in-deployment write → read propagation, readAt - writtenAt)

Scenarios — stream: one step that streams chunks back to the client; no hooks, so the run stays in turbo mode · hook + stream: registers a hook before the same streaming step, which exits turbo mode · 1020 steps: 1020 trivial sequential steps; STSO is measured between consecutive steps in the given step ranges

🟢/🔴 mark percentiles within/above target. Targets (p75/p90/p99, ms) — TTFS 200/300/600 · SL 50/60/125 · STSO (1-20) 20/30/60 · STSO (101-120) 30/45/90 · STSO (1001-1020) 40/60/120

All metrics are measured from deployment-side timestamps only. Runs are triggered by an in-deployment route that stamps the anchor (clientStart) right before start(), so the CI runner’s request and its path through api.vercel.com sit outside every measured window. TTFS = in-deployment start() → first step body (turbo uses the in-process fast path, non-turbo the dispatch path), and includes the VQS dispatch hop plus any /flow cold start. STSO/WO are measured between step bodies on the deployment. SL is measured inside the workflow (parallel reader/writer steps), so it no longer includes the api.vercel.com read path.

Cold starts are kept in the numbers on purpose — they are part of real bursty-workload latency. The workbench deployment cold-starts the /flow invocation for a large fraction of runs, inflating P75+; the P10 column shows the warm-start floor for comparison.

@github-actions

github-actionsBot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production145302301683
✅ 💻 Local Development161702191836
✅ 📦 Local Production161702191836
✅ 🐘 Local Postgres161702191836
✅ 🪟 Windows15300153
✅ 📋 Other89401771071
✅ vercel-multi-region270027
Total7378010648442

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro126027
✅ example126027
✅ express126027
✅ fastify126027
✅ hono126027
✅ nextjs-turbopack15003
✅ nextjs-webpack15003
✅ nitro126027
✅ nuxt126027
✅ sveltekit14508
✅ vite126027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15300
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable128025
✅ e2e-local-dev-tanstack-start-128025
✅ e2e-local-postgres-nest-stable128025
✅ e2e-local-postgres-tanstack-start-128025
✅ e2e-local-prod-nest-stable128025
✅ e2e-local-prod-tanstack-start-128025
✅ e2e-vercel-prod-tanstack-start126027
✅ vercel-multi-region
AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

Comment threadpackages/nest/src/index.ts
…` meta-package, breaking the documented `import { NestLocalBuilder } from "workflow/nest"` and leaving no working alternative.
This commit fixes the issue reported at packages/nest/src/index.ts:6
## The bug
This PR changed `packages/nest/src/index.ts` from a value+type export:
```ts
export { type NestBuilderOptions, NestLocalBuilder } from './builder.js';
```
to a type-only export:
```ts
export type { NestBuilderOptions } from './builder.js';
```
The rationale is valid — `builder.ts` imports `@workflow/builders` (esbuild/SWC) at top level, and the runtime entry must stay free of build-time deps so a NestJS app can be bundled into a serverless function. The `NestLocalBuilder` value is instead intended to be reached via the `workflow/nest/builder` subpath (as the new comment in `index.ts` states).
The problem is that the escape hatch was only half-wired:
- `@workflow/nest`'s own `package.json` *does* expose a `./builder` subpath (`./dist/builder.js`).
- But the recommended consumer package is the **meta `workflow` package**, whose `package.json` only exposes `./nest` → `./dist/nest.js`, and `packages/workflow/src/nest.ts` is just `export * from '@workflow/nest'` (the runtime entry that no longer value-exports `NestLocalBuilder`). There was **no** `./nest/builder` subpath in the meta package.
### Concrete trigger
A user following the API-reference docs writes:
```ts
import { NestLocalBuilder } from "workflow/nest";
```
(`docs/content/docs/v5/api-reference/workflow-nest/nest-local-builder.mdx` line 15 and the v4 equivalent.) At runtime this resolves to `@workflow/nest`'s main entry, which now exports only the *type* `NestBuilderOptions` — so `NestLocalBuilder` is `undefined`, and `new NestLocalBuilder(...)` throws `TypeError: NestLocalBuilder is not a constructor`. There was also no `workflow/nest/builder` subpath to fall back to, so the value class was completely unreachable through the `workflow` meta package.
## The fix
1. Added a `./nest/builder` subpath to `packages/workflow/package.json` mapping to `./dist/nest-builder.js`.
2. Created `packages/workflow/src/nest-builder.ts` = `export * from '@workflow/nest/builder'`, mirroring the existing `src/nest.ts` pattern, so the value export is threaded through the meta package and compiled to `dist/nest-builder.js`.
3. Updated the v4 and v5 `nest-local-builder.mdx` docs to the working import path `import { NestLocalBuilder } from "workflow/nest/builder"`.
This restores reachability of `NestLocalBuilder` for meta-package consumers while preserving the PR's goal of keeping build-time deps out of the runtime entry.
Note: the sandbox has no installed `node_modules`, so a full `tsc` build could not be executed; the change is structurally identical to the already-working `./nest` subpath wiring.
Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Co-authored-by: VaguelySerious <mittgfu@gmail.com>
@VaguelySeriousVaguelySerious changed the title [nest] Support deploying NestJS workflows to Vercel[nest] Fix NestJS Vercel build outputJul 17, 2026
@VaguelySerious
VaguelySerious marked this pull request as ready for review July 17, 2026 19:46
@VaguelySerious
VaguelySerious requested review from a team and ijjk as code ownersJuly 17, 2026 19:46
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
'@swc/core/*',
'@swc/wasm',
'esbuild',
'*.node',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ai review: *.node is still always externalized, but this builder does not trace or copy external files into appFuncDir; the emitted function contains only index.js, package.json, and .vc-config.json. Any Nest app dependency with a native addon therefore keeps a runtime reference to a .node file that is absent from the deployed .func, causing MODULE_NOT_FOUND on cold start. Please trace/copy native artifacts (and their package layout) into the function, or fail the build with a clear unsupported-dependency error instead of producing a broken deployment.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed as a documented limitation rather than adding native-module tracing, which is out of scope for this exploratory integration (reliably tracing/copying .node artifacts also requires handling the runtime resolution these modules do via bindings/node-gyp-build).

*.node stays externalized so esbuild does not fail the build on a .node file it cannot bundle; I added a comment at that line making the intentional choice and its consequence explicit (b047a12), and called out the limitation in the changeset-linked README section ("Deploying to Vercel") plus the changeset. If native-addon support is needed we can follow up with @vercel/nft tracing.

Comment thread.changeset/nest-vercel-support.md Outdated
@@ -0,0 +1,5 @@
---
'@workflow/nest': minor

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ai review: This is marked as a minor change, but the PR removes the public NestLocalBuilder value export from the @workflow/nest and workflow/nest main entries. Existing documented imports stop working; adding a new /builder subpath provides a migration target but does not preserve compatibility. Because this bump type also governs stable backports, please either retain a compatible main-entry export or mark the change as major.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bumped @workflow/nest to major (b047a12). Retaining the main-entry NestLocalBuilder export is not an option — it would re-drag the build toolchain (@workflow/builders, esbuild, SWC) into the runtime bundle of every app that imports WorkflowModule, which is the bug this PR fixes — so the subpath (@workflow/nest/builder) is the migration target and the changeset now documents it as a breaking change.


Commands:
init Generate .swcrc configuration with the workflow plugin
build Build workflow bundles (and the Vercel Build Output when on Vercel)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ai review: The package README still documents only the init CLI command and has no Vercel deployment/build instructions. Repository standards require corresponding README updates when package functionality changes, and direct @workflow/nest consumers otherwise have no package-local documentation for this new command. Please update packages/nest/README.md with build, its options, the Vercel entry module, and the vercel-build flow.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated packages/nest/README.md (b047a12): added a "Deploying to Vercel" section (serverless entry module, skipBuild on Vercel, the vercel-build flow, native-addon limitation), documented the build command with an options table (--vercel, --dirs, --entry, --out-dir, --module), and noted the NestLocalBuilder/NestVercelBuilder subpaths in "How It Works".

…ive-addon limitation
- Changeset: bump @workflow/nest to major (removing NestLocalBuilder from the
main entry is a breaking change) and document the subpath migration + the
native-addon limitation.
- README: document the `build` command and its options, the Vercel entry
module and `vercel-build` flow, the NestVercelBuilder subpath, and the
native-addon limitation.
- vercel-builder: comment why `*.node` is externalized and that native addons
are not yet traced/copied into the deployed function.
- Minor wording: Workflow DevKit -> Workflow SDK.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment threadpackages/nest/README.md Outdated
Comment threadpackages/nest/README.md Outdated
```typescript
// _vercel/entry.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from '../src/app.module';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ai review: This entry bypasses the nest build output by importing ../src/app.module, so the Vercel builder feeds the Nest TypeScript graph back through esbuild. That contradicts the builder assumption that it bundles already-compiled JS, and esbuild does not emit the TypeScript emitDecoratorMetadata that Nest constructor injection relies on; apps with injected providers can therefore boot with missing DI metadata. Please import ../dist/app.module.js here, matching the checked-in workbench and the v5 guide, so the SWC-compiled decorator metadata is retained.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixed in 6e6f9ab. The entry example now imports the compiled ../dist/app.module.js (produced by nest build, whose SWC pass emits emitDecoratorMetadata) instead of raw ../src/app.module, and adds import 'reflect-metadata'. This matches the checked-in workbench/nest/_vercel/entry.ts and the getting-started docs, both of which already used dist/ + reflect-metadata.

…t + reflect-metadata
The entry snippet imported AppModule from raw `../src/app.module` and omitted
`import 'reflect-metadata'`. Importing raw TS routes the app back through esbuild
(which does not emit `emitDecoratorMetadata`), breaking NestJS constructor
injection; the entry must import the `nest build` output (`../dist/app.module.js`)
and register reflect-metadata — matching the workbench entry and getting-started
docs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actionsgithub-actionsBot mentioned this pull request Jul 20, 2026
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for 542138d (AI decision).

The new NestVercelBuilder depends on createWorkflowWorldTargetEsbuildPlugin from @workflow/builders (introduced in #2752), which does not exist on stable (verified via git grep on origin/stable), so the commit builds on main-only APIs and would not compile there. Additionally, the changeset marks @workflow/nest as a major breaking change (moving NestLocalBuilder off the package root), which is intended for the next major release rather than the stable v4 line.

To override, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

542138dc0b37f492fbf51b75e8ba4f57e291fc8f

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@VaguelySerious@karthikscale3