## Summary
When `WORKFLOW_PUBLIC_MANIFEST=1` is set, each framework builder exposes the workflow manifest at `/.well-known/workflow/v1/manifest.json` via the most appropriate mechanism for the framework:
- **Next.js**: Copies manifest to `public/.well-known/workflow/v1/manifest.json` (served as a static file)
- **SvelteKit**: Copies manifest to `static/.well-known/workflow/v1/manifest.json` (served as a static file)
- **Vercel Build Output API** (example workbench): Copies manifest to `.vercel/output/static/.well-known/workflow/v1/manifest.json` (served as a static file)
- **Nitro** (vite, hono, express, fastify, nuxt, nitro-v2, nitro-v3): Registers a virtual handler that reads and serves the manifest JSON
- **Astro**: Generates a `manifest.json.js` page route that returns the manifest JSON
- **NestJS**: Adds a `@Get('manifest.json')` endpoint on the `WorkflowController` (gated by the env var at runtime)
### Other changes
- `BaseBuilder.createManifest()` now returns the manifest JSON string so framework builders can use it
- Added `shouldExposePublicManifest` getter to `BaseBuilder`
- Fixed Astro `LocalBuilder` which was missing the `createManifest()` call that all other framework builders have
- Removed unused `public/` directory and static file copy from the example workbench build script
- Added `WORKFLOW_PUBLIC_MANIFEST` to `turbo.json` build task env so Vercel builds can access it
- Added generated manifest paths to `.gitignore`
Fixed a bug in module specifier resolution and added support for package subpath exports in workflow IDs.
### What changed?
- Fixed a caching bug in the module specifier resolution system that could cause incorrect IDs
- Added support for subpath exports in package IDs (e.g., `workflow/internal/builtins@4.0.0`)
- Improved module resolution by passing absolute file paths to the SWC transform
- Enhanced manifest merging to properly combine results from both workflow and step bundles
- Updated builders to return and merge manifests from both workflow and step bundles
### How to test?
1. Build a project that uses subpath exports in packages
2. Verify that workflow IDs correctly include the subpath (e.g., `workflow/internal/builtins@4.0.0`)
3. Test with a project that has multiple builds to ensure module specifier caching works correctly
### Why make this change?
This change addresses an issue where the module specifier cache could return incorrect results, leading to inconsistent workflow IDs. It also adds support for packages with multiple entry points through subpath exports, ensuring that steps with the same name in different subpaths don't collide. This improves the reliability of cross-bundle references and makes the system more robust when working with complex package structures.
## Summary
This PR changes how the SWC compiler generates IDs for workflows, steps, and classes. Instead of using raw file paths, IDs are now based on **Node.js module specifiers** when the file belongs to a package (either in `node_modules` or a workspace package).
## Motivation
Previously, IDs were generated using file paths like `step//src/jobs/order.ts//fetchData`. This caused several issues:
1. **Package exports conditions**: When a package uses conditional exports (e.g., `"workflow"` vs `"default"` conditions in `package.json`), the same import specifier can resolve to different files. Using file paths meant IDs could differ based on which export condition was used.
2. **Cross-bundle consistency**: Classes serialized in one bundle couldn't be deserialized in another if the file paths differed.
3. **Version tracking**: No way to include package versions in IDs for cache invalidation.
## Changes
### New ID Format
IDs now use the format `{type}//{modulePath}//{identifier}` where `modulePath` is either:
- A **module specifier** like `point@0.0.1` or `@myorg/shared@1.2.3` for package files
- A **relative path** prefixed with `./` like `./src/jobs/order` for local app files
Examples:
- `step//workflow@4.0.1-beta.50//fetch` (SDK step)
- `step//./workflows/order//processOrder` (local step)
- `class//point@0.0.1//Point` (package class)
- `class//./src/models/User//User` (local class)
### New Module Specifier Resolution
Added `packages/builders/src/module-specifier.ts` which:
- Detects if a file is in `node_modules` or a workspace package
- Finds the nearest `package.json` and extracts name/version
- Returns the module specifier for the SWC plugin to use
### SWC Plugin Changes
- Added `moduleSpecifier` option to plugin config
- Updated `naming.rs` to support both module specifiers and relative paths
- Added `get_module_path()` helper that uses specifier when available, falls back to `./filename` format
### Special Cases
- **Builtin functions** (`__builtin_*`): Continue to use just the function name as the ID for stable, version-independent lookup from the workflow VM runtime.
## Testing
- Updated all 125+ SWC plugin test fixtures to use new ID format
- Added tests for module specifier resolution
- Added tests for Windows path normalization in naming
## Breaking Changes
This is technically a breaking change for any persisted workflow runs that reference the old ID format. However, since IDs are internal implementation details and not user-facing, this should not affect end users.
## Files Changed
- `packages/builders/src/module-specifier.ts` - **NEW**: Module specifier resolution logic
- `packages/builders/src/apply-swc-transform.ts` - Pass module specifier to SWC plugin
- `packages/builders/src/base-builder.ts` - Use `getImportPath` for virtual entry imports
- `packages/swc-plugin-workflow/transform/src/lib.rs` - Accept and use module specifier
- `packages/swc-plugin-workflow/transform/src/naming.rs` - New ID formatting with module paths
- `packages/swc-plugin-workflow/spec.md` - Updated documentation
- `packages/core/e2e/e2e.test.ts` - Updated test assertions for new ID format
This PR ensures that all classes with custom serialization are automatically included in all bundle contexts (step, workflow, client) to ensure proper serialization/deserialization when crossing execution boundaries:
- Classes defined in any context can now be properly serialized when passing data between:
- Client → Workflow (when starting workflows)
- Workflow → Step (when calling steps)
- Step → Workflow (when returning step results)
- Workflow → Client (when returning workflow results)
- The build system now automatically discovers all files containing serializable classes and includes them in each bundle, regardless of where the class is originally defined.
- No manual configuration is required - cross-registration happens automatically during the build process.
Added automatic discovery for custom classes with workflow serialization, allowing serialization classes to be defined in separate files without requiring explicit directives.
### What changed?
- Added detection for files containing custom class serialization patterns:
- Files importing from `@workflow/serde`
- Files using `Symbol.for('workflow-serialize')` or `Symbol.for('workflow-deserialize')`
- Created shared utilities in `transform-utils.ts` for consistent pattern detection across all build tools
- Updated Next.js, Nitro, Rollup, and Vite plugins to use the new detection patterns
- Added exclusion logic to prevent re-processing of generated workflow files
- Added tests to verify the pattern detection works correctly
- Updated documentation in the SWC plugin spec to explain the new discovery mechanism
### How to test?
1. Create a class with custom serialization in a separate file:
```js
// models/point.js
export class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
static [Symbol.for('workflow-serialize')](instance) {
return { x: instance.x, y: instance.y };
}
static [Symbol.for('workflow-deserialize')](data) {
return new Point(data.x, data.y);
}
}
```
1. Import and use this class in a workflow or step file:
```js
'use workflow';
import { Point } from '../models/point';
export function myWorkflow() {
const point = new Point(10, 20);
return point;
}
```
1. Verify the class is properly serialized when passed between client and server
### Why make this change?
Previously, files containing custom serialization classes needed to include a `'use step'` directive to be discovered and transformed, even if they weren't actual step functions. This was unintuitive and could lead to confusion.
This change allows for a more natural code organization pattern where model classes with serialization can be defined in their own files without requiring directives. The build system will automatically discover and transform these files to ensure the serialization works correctly when the classes are used in workflows or steps.
* Fix bundling behavior for dynamic imports and server-only, client-only imports
* Generate changeset
* Filter tsconfig to only pull in code from within the current working directory(workflow dir)
* Rewrite server-only and client-only
* add additional pseudo directives
* upmerge
* fix failing unit test
* fix: add TypeScript decorator support to SWC transform
Enable decorator parsing in SWC configuration to support codebases using
TypeScript decorators (TypeORM, NestJS, class-validator, custom decorators).
Without this fix, projects importing files with decorators fail with:
"Unexpected token `@`. Expected identifier..." syntax errors.
Changes:
- Add `decorators: true` to parser options for TypeScript and ECMAScript
- Add `legacyDecorator: true` for TypeScript's experimentalDecorators
- Add `decoratorMetadata: true` for emitDecoratorMetadata support
* fix: conditionally enable decorators based on tsconfig compilerOptions
Read experimentalDecorators and emitDecoratorMetadata from tsconfig.json
to match Next.js behavior. Decorators are now only enabled when explicitly
configured in the project's tsconfig.
- Add getDecoratorOptionsForDirectory() to read tsconfig settings
- Update applySwcTransform and Next.js loader to use tsconfig options
- Handle JSONC format (comments, trailing commas) in tsconfig parsing
* fix: use json5 for parsing tsconfig in decorator options
Replace manual regex-based JSONC parsing with json5 library for more
robust handling of comments and trailing commas in tsconfig.json files.
* bump
* apply fix
* DCO Remediation Commit for JJ Kasper <jj@jjsweb.site>
I, JJ Kasper <jj@jjsweb.site>, hereby add my Signed-off-by to this commit: aa1dc36db7
I, JJ Kasper <jj@jjsweb.site>, hereby add my Signed-off-by to this commit: e25f1748c6
Signed-off-by: JJ Kasper <jj@jjsweb.site>
* DCO Remediation Commit for Rishabh <rishabh.pugalia@gmail.com>
I, Rishabh <rishabh.pugalia@gmail.com>, hereby add my Signed-off-by to this commit: e2f93048aa
I, Rishabh <rishabh.pugalia@gmail.com>, hereby add my Signed-off-by to this commit: 3faee5d3c2
I, Rishabh <rishabh.pugalia@gmail.com>, hereby add my Signed-off-by to this commit: 9dbbda6362
Signed-off-by: Rishabh <rishabh.pugalia@gmail.com>
---------
Signed-off-by: JJ Kasper <jj@jjsweb.site>
Signed-off-by: Rishabh <rishabh.pugalia@gmail.com>
Co-authored-by: JJ Kasper <jj@jjsweb.site>
* Refactor e2e tests for errors
* Improve step error tests to check workflow return value
Step error workflows now catch the error and return message/stack,
making assertions cleaner. Tests verify both:
- Workflow return value (caught error message)
- CLI step result (original stack with function names)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add assertion for stack trace in caught step error
With the fix in step.ts that propagates original stack traces,
we can now verify that caught step errors include function names
directly in the workflow return value (not just via CLI).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* logging
* Fix: When a step handler is re-invoked after max retries are exhausted, the step_failed event doesn't include a stack trace, breaking stack trace propagation for this edge case.
This commit fixes the issue reported at packages/core/src/runtime/step-handler.ts:127-146
## Stack trace loss in step_failed event when max retries are exceeded
**What fails:** Step handler does not include stack property in step_failed event when step is re-invoked after max retries exhausted, causing FatalError in workflow to lose original stack trace
**How to reproduce:**
1. Create a step that always fails with an error that includes a stack trace
2. Exhaust all retry attempts (e.g., maxRetries = 3, so 4 total attempts)
3. The step handler is re-invoked (attempt > maxRetries + 1)
4. This triggers the edge case at lines 127-146 in step-handler.ts
5. A step_failed event is created with fatal: true but without stack property
6. When step.ts processes this event (lines 103-104), the stack remains unset since event.eventData.stack is undefined
**Result:** FatalError created in workflow has default stack (from step.ts) instead of original error stack from step execution
**Expected:** Stack trace should be propagated consistently across all error paths. All other step_failed events with fatal: true include stack property (lines 311-313 and 351-353), but edge case was missing it.
**Root cause:** When step handler is re-invoked after max retries exhausted (attempt > maxRetries + 1), the code creates a step_failed event without including step.error?.stack, which contains the previous error information from the last failed attempt. Other code paths capture this correctly.
Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
* add withData
* Enable source maps for step bundles and validate in e2e tests
- Move sourcemap generation from intermediate workflow bundle to steps bundle
- Enhance step error tests to validate function names and source files in stack traces
- Remove isLocalDeployment() checks since source maps now work in dev mode
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add changeset for step bundle source maps
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add changeset for core package e2e test improvements
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Enable source maps in CI e2e tests
Add NODE_OPTIONS="--enable-source-maps" to all e2e test jobs to ensure
stack traces show original source file paths.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix step error source map checks for local prod builds
Add hasStepSourceMaps() helper that correctly identifies when source maps
are expected to work:
- Vercel prod: works (production builds have proper source maps)
- Local dev: works (DEV_TEST_CONFIG is set, uses step bundle with inline source maps)
- Local prod: doesn't work (nitro/bundler output doesn't preserve source maps)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add hasWorkflowSourceMaps() utility for vite-based framework exception
- Add isViteBasedFramework() helper to detect vite, sveltekit, astro apps
- Add hasWorkflowSourceMaps() to check if workflow errors have source maps
(known issue: vite-based frameworks in local deployments don't preserve them)
- Refactor e2e.test.ts to use the new utility instead of inline check
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* prevent sourcemap checks in nextjs and sveltekit (known offenders)
* improve source maps matrix check
* fix matrix again
* ugh more matrix ignoring
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Primary motivation here is to allow a `use workflow` function to be non-exported, meaning that it is called by `start()` in i.e. an API route within the same function:
- SWC plugin now emits `globalThis.__private_workflows.set(workflowId, fn)` directly after setting `workflowId`
- Non-exported workflow functions are now properly registered and can be invoked
- Removed runtime iteration over exports in the workflow bundle - registration happens at transform time
- Simplified virtual entry generation in base-builder
- Updated turbopack rules to include `*.mjs`, `*.mts`, `*.cjs`, `*.cts` in addition to existing extensions
- Fixed TypeScript detection for `.mts` and `.cts` files across all transform plugins
- Updated esbuild `resolveExtensions` to include `.mts` and `.cts`
- Updated the file watcher's `watchableExtensions` to include `.cts`