* docs: apply Vercel technical writing standards
Audit the complete documentation corpus, package READMEs, skills, and
source TSDoc/comments against the vercel-technical-writing skill and
style-rules.md. Normalize sentence-case headings without changing
published anchors, remove prose em dashes and filler wording, improve
active voice and self-contained phrasing, standardize product/brand
capitalization, American English, list punctuation, units, and code
fence languages, and preserve exact runtime strings/table placeholders.
All executable code is unchanged. Modified skills have their metadata
versions bumped.
* docs: extend writing audit to repository Markdown
Apply the same technical-writing rules to design documents, compiler
specifications, workbench guides, package changelogs, and the remaining
tracked Markdown outside the deployed docs corpus. Preserve historical
meaning, commands, output literals, table placeholders, and heading
anchors.
* docs: exclude generated package changelogs from audit
* Fix Biome lint violations and add Biome CI check
Biome was not configured to respect .gitignore, so ~92% of the 13,355
reported diagnostics came from gitignored build artifacts. Enable VCS
integration (useIgnoreFile), apply safe auto-fixes across the repo, fix
the remaining mechanical errors by hand, downgrade judgment-call a11y /
dangerouslySetInnerHTML rules to warnings, and add a 'biome ci' job to
the Lint workflow so violations block PRs going forward.
* Use an empty changeset (no behavior change, no release needed)
The DCE usage collector skipped the entire variable name pattern when
visiting a `VarDeclarator` (to avoid marking the binding name as "used").
But default-value initializers inside destructuring patterns live in that
pattern — e.g. the `TTL` in `const { ttl = TTL } = options;` — so those
references were invisible to the collector. A module-scope `const`
referenced only through such a default was treated as unused and stripped,
while the surviving code kept reading it, producing a runtime
`ReferenceError` when the default fired.
Traverse the default-value initializer expressions (and computed keys)
within destructuring patterns while still not marking the binding names
themselves, so the referenced declaration is preserved. Function-parameter
defaults were already covered (params are visited in full).
Fixes#2396.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* [swc-plugin] Preserve imports referenced by hoisted nested steps
Dead-code elimination ran before nested step functions were hoisted out of workflow bodies, so imports referenced only by hoisted step bodies were incorrectly stripped from the step bundle, causing a ReferenceError at runtime. Move DCE to run after hoisting in visit_mut_program.
* [swc-plugin] Namespace nested step IDs under non-exported workflow functions
Anonymous steps nested inside callback properties of a non-exported workflow function were registered with an unnamespaced step ID in step mode while the workflow-mode proxy looked them up under the workflow function name, causing a runtime 'step not found' failure. Set current_workflow_function_name in visit_mut_fn_decl for non-exported workflow functions to match the behavior in visit_mut_export_decl. Also clarify the fixture comment to distinguish step-mode and workflow-mode behavior per reviewer feedback.
* [swc-plugin] Namespace nested step IDs across all workflow declaration shapes
Extends the previous fix to cover all three non-exported workflow declaration forms (async function decl, const arrow, const fn-expr) by visiting the workflow body with workflow context before replacing it, and corrects the __internal_workflows manifest comment to report the same prefixed step IDs that are registered at runtime and looked up by the workflow-mode WORKFLOW_USE_STEP proxy. Adds a dedicated regression fixture covering all three shapes.
* [swc-plugin] Capture lexical `this` for nested arrow step functions
When a nested arrow `"use step"` references the enclosing function/method's
`this`, plumb that `this` through the workflow runtime so the step body
sees the correct receiver.
- Workflow mode wraps the step proxy with `.bind(this)`, so invoking the
proxy captures the caller's `this` as `thisVal` on the queue item.
- Step mode hoists the body as a regular `function` (not an arrow) so the
runtime's `stepFn.apply(thisVal, args)` rebinds `this` inside the
hoisted body.
Detection only fires for arrows, since arrows inherit `this` lexically.
Nested non-arrow functions/methods/getters/setters introduce their own
`this`, so the detector stops at those boundaries.
The runtime already supported `thisVal` for instance-method steps; this
PR is purely a compiler change to feed the existing pipeline.
Caveat: capture works at runtime only when the captured value is
serializable across the workflow->step boundary (i.e. the enclosing
class implements `WORKFLOW_SERIALIZE`/`WORKFLOW_DESERIALIZE`).
Refs vercel/workflow#1865
* Address PR review: preserve step proxy metadata + tighter `this` detection
- core: Override `.bind` on step proxies so the bound function retains
`stepId` and `__closureVarsFn`. Without this, a bound proxy that flows
through workflow serialization (e.g. as a step argument) would be
treated as a non-serializable plain function by `getStepFunctionReducer`.
- swc-plugin: Detector now also walks `arrow.params` so `this` references
in default values / destructuring initializers (e.g. `(x = this.foo) =>
...`) trigger the `.bind(this)` path.
- swc-plugin: Class bodies inside the arrow body are now treated as
`this`-binding boundaries — `this` inside class field initializers,
methods, etc. is bound to the class instance, not the outer arrow. The
detector still walks `extends` clauses and computed property keys
because those are evaluated in the surrounding scope.
- spec.md: Sharpen the note about `this` in step bodies — it's
syntactically allowed but only meaningful for instance-method steps and
lexical-`this` arrow steps; other shapes compile but `this` will be
whatever the caller of the step proxy passes.
- Add `lexical-this-detector-edge-cases` fixture covering both the
default-param positive case and the inner-class false-positive guard.
- Strengthen the runtime test to assert `stepId` / `__closureVarsFn`
survive `.bind(...)`.
* [swc-plugin] Fix `arguments` closure-var capture; drop dead `this`/`arguments` checks
- Add `arguments` to `is_global_identifier` so it's not captured as a
closure variable. Previously a nested `function`-form step like
function step() { 'use step'; return arguments[0]; }
was hoisted with `const { arguments } = ...` (a strict-mode syntax
error) and the body's `arguments[0]` resolved against the destructured
binding instead of the function's intrinsic `arguments` object.
- Remove dead `ForbiddenExpression` checks for `this` and `arguments` in
`visit_mut_this_expr` / `visit_mut_ident`. The `'use step'` /
`'use workflow'` directives are stripped during the module-level
traversal before children are visited, so `in_step_function` /
`in_workflow_function` are never observed as true here in practice.
The existing `step-with-this-arguments-super` fixture explicitly
documents that all three identifiers are allowed in step bodies.
- Tighten the spec note about `arguments` accordingly: it works in
`function`-form steps (reflecting positional args) but is not captured
for arrow-form steps; use `...args` for that case.
- Add `nested-step-arguments` fixture pinning down the new behavior.
* refactor(swc-plugin): remove client transform mode, merge into step mode
Remove the `client` transform mode from the SWC compiler plugin. The
`client` and `step` modes were nearly identical — both preserved step
function bodies, replaced workflow bodies with throw stubs, and emitted
the same JSON manifest. Step mode now absorbs all client-mode behaviors:
- Dead code elimination (previously only workflow + client)
- Hoisted variable references for object property steps
- All integrations use mode: 'step' instead of 'client'
BREAKING CHANGE: The `client` value for the SWC plugin `mode` option is
no longer accepted. Use `step` instead.
* fix(nitro): force-inline workflow packages in dev mode for serde classId registration
In dev mode, Nitro's Rollup externalizes npm packages like @workflow/core,
so the SWC transform plugin never processes files like run.js. This means
serde classes (e.g. Run) never get the classId registration IIFE, causing
serialization failures when step functions return Run instances.
Uses a Rollup resolveId hook to force workflow SDK packages to be bundled
(non-external) while leaving all other dependencies external. This is more
targeted than noExternals=true which bundles everything and causes TDZ
errors from circular imports in packages like vue-bundle-renderer/h3.
The Nitro module now also ignores .nitro/workflow/** in watchOptions so
writing generated workflow bundles does not retrigger Nitro's own dev
bundle rebuild loop.
Also wraps dev:reload workflow rebuilds and makes LocalBuilder.build()
atomic (writes to temp files, renames on success) to avoid partial output
state during HMR.
For Nuxt, also configures Vite's ssr.noExternal to bundle workflow
packages in the SSR context.
* fix(nitro,nuxt): address review feedback on dev-mode classId fix
- nitro builders: use crypto.randomUUID() for temp file suffix instead of
Date.now() to avoid collisions under rapid/concurrent build() calls,
and serialize concurrent build() calls through an internal queue so
two overlapping dev rebuilds cannot clobber each other's temp outputs.
- nitro index: use fileURLToPath() to convert file:// URLs to filesystem
paths, which correctly handles Windows paths (file:///C:/... -> C:\...)
and percent-decoding, instead of relying on new URL(...).pathname.
- nuxt module: normalize vite.ssr.noExternal to an array (preserving any
existing string/RegExp/array entry) before appending workflow package
matchers, so the force-bundle behavior is not a no-op when noExternal
is already set to a non-array value.
* fix(swc-plugin): preserve step function names in stack traces after bundler minification
Add Object.defineProperty(fn, 'name', { value, configurable: true }) to the
IIFE step registration so the original function name survives bundler name
mangling (e.g. Turbopack renaming errorStepFn to eD). V8 uses the .name
property for stack traces, so this ensures 'at errorStepFn (...)' appears
instead of 'at eD (...)'.
* Apply suggestion from @Copilot
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Nathan Rajlich <n@n8.io>
---------
Signed-off-by: Nathan Rajlich <n@n8.io>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* [docs] Rename workflowdevkit references to workflowsdk
* [docs] Rename useworkflow.dev to workflow-sdk.dev
* [chore] Add changeset for domain rename
* [docs] Revert sitemap rewrite to useworkflow.dev (crawled-sitemap not yet available for new domain)
* feat(swc): dead code eliminate unreferenced private class members in workflow mode
After stripping 'use step' methods from a class body in workflow mode,
eliminate private members (both JS native #field/#method and TypeScript
private field/private method) that are no longer referenced by any
remaining public member.
The algorithm is iterative: references are seeded from public members,
then expanded through surviving private members until a fixed point,
enabling cascading elimination (e.g. a private field only referenced by
a private method that is itself unreferenced).
* test(swc): add fixture for JS native private member DCE (#field, #method)
* fix(swc): address review feedback on private member DCE
- Namespace JS native private names with # prefix to avoid collisions
with TS private members of the same name
- Track TS private member accesses on non-this receivers (e.g. a.x in
static methods) by maintaining a set of known TS-private names
- Use visit_children_with for full traversal including computed member
expressions
- Extract retain logic into shared retain_referenced_private_members()
helper used by both visit_mut_class_decl and visit_mut_class_expr
* Followup fixes for sync step function support
Address review feedback from #1633 (merged):
- Remove dangling incomplete comment from validate_async_function removal
- Restore export validation for file-level 'use step' files: allow
sync/async function exports, reject non-function exports (constants,
classes, re-exports) which can pull Node-only code into bundles
- Fix InvalidExport error message: 'Only functions can be exported'
for step files vs 'Only async functions can be exported' for workflow
- Update spec.md error table and supported function forms to document
sync step support
- Add sync-step-class test fixture (sync static methods, sync function
expressions with var/let)
- Add sync-workflow error test (sync workflow still errors)
* Tighten step file export validation, fix spec completeness
- Reject uninitialized var exports (export let x) in step files
- Reject local named exports (export { value }) in step files since
we cannot statically verify the binding is a function
- Add test coverage for both new rejection cases
- Add sync variants for let/var arrow functions in spec.md
* Add test fixtures for remaining export validation edge cases
- Add re-export with specifiers to invalid-exports (export { x } from)
- Add error fixture for default class export in step files
- Add fixture for sync default function export (should pass)
- Total: 220 tests (165 fixture + 33 error + 22 unit)
Lift the async function restriction from "use step" in both the SWC
compiler plugin and the TypeScript language service plugin. This enables
using "use step" as a mechanism to strip Node.js-dependent code from
the workflow VM bundle without requiring the function to be async.
The async restriction is preserved for "use workflow" functions.
- SWC plugin: removed async guards from all step function code paths,
updated should_transform_function, updated InvalidExport validation
- TypeScript plugin: removed error 9002 for sync step functions
- Added sync-step fixture test, cleaned up error test fixtures
- Updated spec.md error documentation
The SWC compiler plugin no longer generates import statements. All step
function registrations and closure variable access are now self-contained
inline IIFEs with zero module dependencies
* Add detect mode to SWC plugin for false positive directive filtering
Add a new 'detect' mode to the SWC workflow plugin that walks the AST
to find directives and serde patterns and emits the manifest, but does
not transform any code. The discover-entries plugin now uses a two-phase
approach: fast regexp pre-scan to filter out most files, then SWC detect
mode on candidates to validate at the AST level. This eliminates false
positives where directive-like strings appear inside template literals
or other non-code contexts. The mode:false syntax-only transform is also
removed since esbuild handles TypeScript natively.
* Keep SWC syntax transform in discover phase for decorator support
esbuild does not support legacy decorators or emitDecoratorMetadata,
so all files still need the SWC syntax transform (TS→JS) during
discovery. For regexp-matched files the 'detect' mode handles both
the syntax transform and manifest in a single pass; for all other
files the existing mode:false call is used.
* Use Set for discoveredWorkflows/Steps/SerdeFiles
Eliminates the manual .includes() dedup check and prevents duplicate
entries structurally.
* Update DeferredDiscoveredEntries to use Set<string>
* Add @workflow/next to changeset
* Support getter functions with "use step" directive
Add SWC compiler plugin support for JavaScript getters marked with
"use step", enabling patterns like `await obj.prop` where the getter
triggers a step function invocation.
- Handle Prop::Getter in object literals and MethodKind::Getter in classes
- Emit Object.getOwnPropertyDescriptor registration in step mode
- Emit hoisted proxy + Object.defineProperty in workflow mode
- Emit error for getters with "use workflow"
- Fix @vercel/workflow -> @workflow/serde imports in existing fixtures
- Update spec.md with getter transformation documentation
* Add changeset for getter step support
* Add e2e test for getter step functions
* Add static getter support, sanitize hoisted var identifiers
Address PR review feedback:
- Support static getters with "use step" using ClassName (not .prototype)
- Add sanitize_ident_part() to produce valid JS identifiers from getter
names that may contain special characters (e.g. string literal keys)
- Add static-getter-step test fixture
- Update spec.md with static getter transformation documentation
* Remove duplicate getter workflow error in visit_mut_prop_or_spread
The previous pre-release versions (4.x.y-beta.N) caused two issues:
- semver.inc('4.0.0-beta.N', 'major') returns 4.0.0, not 5.0.0
- Pre-release numbers carried over (beta.61 -> beta.62 instead of beta.0)
Setting all versions to 4.0.0 (non-pre-release) ensures a clean major
bump to 5.0.0-beta.0. Also removes @workflow/swc-playground-wasm from
the changeset and pre.json since it is a private package.
* fix(builders): override sideEffects:false for discovered workflow/step/serde entries
When node_modules packages include "sideEffects": false in their
package.json, esbuild drops bare imports from the virtual-entry.js
file. This is incorrect because the SWC compiler transform injects
side-effectful registration code (workflow IDs, step IDs, class
serialization) into these modules.
Fix: return the resolved path alongside sideEffects: true from the
onResolve handler so esbuild uses the plugin's resolution result
instead of re-reading the package.json.
* refactor(builders): normalize sideEffectEntries with realpaths for symlink compatibility
Extract withRealpaths() helper and use it for both normalizedEntriesToBundle
and sideEffectEntries at all three bundle sites. This ensures the
sideEffects override works correctly under pnpm/workspace symlinked
layouts where enhanced-resolve may return realpaths that differ from
the original discovered file paths.
* perf(builders): skip enhanced-resolve for transitive imports when only sideEffectEntries is set
When entriesToBundle is not set (workflow/client bundles), only top-level
import statements need the sideEffects override — transitive imports
from deep within the bundle are not bare imports and don't need resolution.
Skip enhanced-resolve for non-import-statement kinds to reduce overhead.
* fix(swc-plugin): use binding name for class expression method registrations
When a pre-bundled package (e.g. via tsup) contains class expressions
like `var Foo = class _Foo { ... }`, the internal name `_Foo` is only
scoped inside the class body. The SWC plugin was incorrectly using the
internal name for method step registrations and class serialization
registrations emitted at module scope, causing ReferenceError at runtime.
Fix: always use the binding name (registration_name) for
current_class_name in visit_mut_class_expr, consistent with the existing
handling for anonymous class expressions. This ensures:
- registerStepFunction calls reference the binding name (Foo)
- Only one class registration IIFE is emitted (not duplicates for both
Foo and _Foo)
- Step IDs use the binding name in their qualified path
* refactor(swc-plugin): rename internal_class_name to tracked_class_name for clarity
The variable no longer represents the internal class expression identifier
after being reassigned to the binding name. Rename to tracked_class_name
and eliminate the intermediate registration_name variable to make the
intent clearer and reduce confusion for future readers.
* fix(swc-plugin): rewrite anonymous export default class to const declaration
When an anonymous class with serde/step methods is exported as a default
export (`export default class { ... }`), the generated registration code
(registerStepFunction, class registry IIFE) would reference a nonexistent
variable at module scope, causing a ReferenceError at runtime.
Fix: detect anonymous default class exports in visit_mut_export_default_decl
and visit_mut_export_default_expr, generate a unique binding name
(__defaultClass), and defer a rewrite in visit_mut_module_items that
transforms the export into:
const __defaultClass = class __defaultClass { ... };
export default __defaultClass;
Named default class exports (export default class Foo { ... }) are
handled by setting current_class_binding_name so the transformer
uses the existing class name for registration code.
* refactor(swc-plugin): rename __defaultClass to __DefaultClass for class naming convention
* fix(swc-plugin): fix panic for step-only anonymous default class exports
Address review feedback:
- Remove expect() that panicked when anonymous default class had step
methods but no serde methods (ident was not re-inserted). Keep
const_name in a local variable instead of relying on class_expr.ident.
- Add debug_assert for single default class export invariant.
- Update spec.md and test fixture inputs to use new this() instead of
referencing the generated binding name.
- Add step-only anonymous default class fixture to cover the bug path.
* refactor(swc-plugin): address review feedback for export default class handling
- Remove dead Expr::Class handler in visit_mut_export_default_expr
(SWC wraps parenthesized form in Expr::Paren, so it never fires)
- Extract class_needs_binding_rewrite() helper, eliminating duplicated
detection logic and unnecessary body clones
- Add debug_assert for mutual exclusivity of default_workflow_exports
and default_class_exports
- Clarify spec.md on self-name behavior difference between serde and
step-only classes
* fix(builders): override sideEffects:false for discovered workflow/step/serde entries
When node_modules packages include "sideEffects": false in their
package.json, esbuild drops bare imports from the virtual-entry.js
file. This is incorrect because the SWC compiler transform injects
side-effectful registration code (workflow IDs, step IDs, class
serialization) into these modules.
Fix: return the resolved path alongside sideEffects: true from the
onResolve handler so esbuild uses the plugin's resolution result
instead of re-reading the package.json.
* refactor(builders): normalize sideEffectEntries with realpaths for symlink compatibility
Extract withRealpaths() helper and use it for both normalizedEntriesToBundle
and sideEffectEntries at all three bundle sites. This ensures the
sideEffects override works correctly under pnpm/workspace symlinked
layouts where enhanced-resolve may return realpaths that differ from
the original discovered file paths.
* perf(builders): skip enhanced-resolve for transitive imports when only sideEffectEntries is set
When entriesToBundle is not set (workflow/client bundles), only top-level
import statements need the sideEffects override — transitive imports
from deep within the bundle are not bare imports and don't need resolution.
Skip enhanced-resolve for non-import-statement kinds to reduce overhead.
* fix(swc-plugin): use binding name for class expression method registrations
When a pre-bundled package (e.g. via tsup) contains class expressions
like `var Foo = class _Foo { ... }`, the internal name `_Foo` is only
scoped inside the class body. The SWC plugin was incorrectly using the
internal name for method step registrations and class serialization
registrations emitted at module scope, causing ReferenceError at runtime.
Fix: always use the binding name (registration_name) for
current_class_name in visit_mut_class_expr, consistent with the existing
handling for anonymous class expressions. This ensures:
- registerStepFunction calls reference the binding name (Foo)
- Only one class registration IIFE is emitted (not duplicates for both
Foo and _Foo)
- Step IDs use the binding name in their qualified path
* refactor(swc-plugin): rename internal_class_name to tracked_class_name for clarity
The variable no longer represents the internal class expression identifier
after being reassigned to the binding name. Rename to tracked_class_name
and eliminate the intermediate registration_name variable to make the
intent clearer and reduce confusion for future readers.
* Inline class serialization registration to fix 3rd-party package support (#1480)
* Inline class serialization registration to fix 3rd-party package support
The SWC plugin previously generated:
import { registerSerializationClass } from "workflow/internal/class-serialization";
registerSerializationClass("class//...", ClassName);
This broke for 3rd-party packages (e.g. @vercel/sandbox) that define
serializable classes but don't depend on the 'workflow' package. The
bare 'workflow' specifier is unresolvable from within node_modules of
a package that doesn't list it as a dependency.
Now the plugin generates a self-contained IIFE that uses
Symbol.for('workflow-class-registry') on globalThis directly, with
zero module dependencies:
(function(__wf_cls, __wf_id) {
var __wf_sym = Symbol.for("workflow-class-registry"),
__wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map());
__wf_reg.set(__wf_id, __wf_cls);
Object.defineProperty(__wf_cls, "classId", { ... });
})(ClassName, "class//...");
This is fully compatible with the existing deserialization side in
@workflow/core which reads from the same globalThis registry.
* Address review feedback: fix comment and update docstring
- Fix inaccurate IIFE comment in lib.rs: the second arg is the
generated class ID string, not the literal "classId"
- Update registerSerializationClass docstring to reflect that the
SWC plugin now inlines equivalent logic rather than importing it
* Update CJS require fixture outputs for inline class serialization
The original PR #1480 was merged but reverted because it didn't include
updated fixtures for the CJS require patterns added by PR #1144
(custom-serialization-require-destructured and
custom-serialization-require-namespace). These fixtures still had the
old 'import { registerSerializationClass }' pattern instead of the
new inline IIFE.
* feat: export semantic error types and add API reference documentation
Add missing error exports (HookNotFoundError, EntityConflictError,
RunExpiredError, TooEarlyError, ThrottleError, RunNotSupportedError,
WorkflowWorldError) to workflow/internal/errors. Create new error
classes for world-level semantics. Tighten TSDoc comments on all
error classes. Add API reference docs for all error types.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: use @setup declarations, workflow/errors import, and errors/ doc section
- Replace @skip-typecheck with proper `declare` + `// @setup` lines
so code samples are typechecked but setup lines hidden from readers
- Add `workflow/errors` export to package.json (public API, replaces
`workflow/internal/errors` in docs)
- Add `workflow/errors` path mapping in docs-typecheck type-checker
- Add HookConflictError to re-export list
- Move all error docs under api-reference/workflow/errors/ subdirectory
- Update all internal cross-references and links
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: move error docs to top-level workflow-errors section
- Move semantic error docs to api-reference/workflow-errors/ (matching
the workflow/errors import path, like workflow-api for workflow/api)
- Keep FatalError and RetryableError in api-reference/workflow/ since
they're imported from workflow, not workflow/errors
- Fix all cross-reference links
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: update HTTP debug logger JSDoc to clarify scope
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: make TooEarlyError.retryAfter a number (seconds) matching WorkflowWorldError
TooEarlyError.retryAfter is now seconds (number) instead of a Date,
consistent with ThrottleError and WorkflowWorldError. The conversion
from seconds to Date is done at the consumer site (step-handler) rather
than at construction time.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address review feedback on docs accuracy
- WorkflowWorldError docs: add status, code, url, retryAfter properties
to TSDoc; clarify that .is() only matches direct instances (not
subclasses); use instanceof in catch-all example
- TooEarlyError/ThrottleError docs: mark retryAfter as optional (?)
to match actual type definitions
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Inline class serialization registration to fix 3rd-party package support
The SWC plugin previously generated:
import { registerSerializationClass } from "workflow/internal/class-serialization";
registerSerializationClass("class//...", ClassName);
This broke for 3rd-party packages (e.g. @vercel/sandbox) that define
serializable classes but don't depend on the 'workflow' package. The
bare 'workflow' specifier is unresolvable from within node_modules of
a package that doesn't list it as a dependency.
Now the plugin generates a self-contained IIFE that uses
Symbol.for('workflow-class-registry') on globalThis directly, with
zero module dependencies:
(function(__wf_cls, __wf_id) {
var __wf_sym = Symbol.for("workflow-class-registry"),
__wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map());
__wf_reg.set(__wf_id, __wf_cls);
Object.defineProperty(__wf_cls, "classId", { ... });
})(ClassName, "class//...");
This is fully compatible with the existing deserialization side in
@workflow/core which reads from the same globalThis registry.
* Address review feedback: fix comment and update docstring
- Fix inaccurate IIFE comment in lib.rs: the second arg is the
generated class ID string, not the literal "classId"
- Update registerSerializationClass docstring to reflect that the
SWC plugin now inlines equivalent logic rather than importing it
* Add CommonJS require() support for class serialization detection in SWC plugin
The SWC compiler plugin now detects classes with custom serialization methods (`WORKFLOW_SERIALIZE` / `WORKFLOW_DESERIALIZE`) when symbols are obtained via CommonJS `require()` calls, in addition to the existing ESM import and `Symbol.for()` patterns.
This handles code that has been pre-compiled from ESM to CommonJS by tools like TypeScript (tsc), esbuild, or tsup, which transform:
```ts
import { WORKFLOW_SERIALIZE } from "@workflow/serde"
```
into either:
```ts
const serde_1 = require("@workflow/serde") // namespace require
const { WORKFLOW_SERIALIZE } = require(...) // destructured require
```
Both patterns are now recognized during the identifier collection phase, and classes using them are properly registered with registerSerializationClass().
* Address review: tighten is_require_call and extract track_serialization_bindings helper
- Tighten is_require_call to require exactly one string literal argument,
rejecting dynamic requires and multi-arg calls.
- Extract track_serialization_bindings helper to deduplicate the require
tracking logic between Decl::Var and ExportDecl(Decl::Var) branches.
* Add support for calling `start()` directly inside workflow functions
Enable `start()` to work in workflow context by routing through an
internal step (`__workflow_start`), reusing existing step infrastructure
with no new event types or server changes needed.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Address PR review feedback
- Use typeof check instead of truthiness for WORKFLOW_START symbol
- Validate start() options in workflow context (reject unsupported options like world)
- Set maxRetries=0 on __workflow_start step to prevent orphaned child runs
- Add unit tests for createStart factory (6 tests)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Make Run serializable in workflow context with step-backed methods
- Add Run serialization via __serializable marker + custom Run reducer/reviver
in the serialization module (avoids SWC plugin injecting class-serialization imports)
- Create WorkflowRun class factory (packages/core/src/workflow/run.ts) with
step-backed methods: cancel(), status, returnValue, workflowName, createdAt,
startedAt, completedAt, exists
- Register 8 built-in steps (__run_cancel, __run_status, etc.) in step-handler
- Update __workflow_start to return full Run object (serialized → WorkflowRun in VM)
- Update createStart to pass through step result directly
- Update docs to reflect full Run support in workflow context
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix start() in workflow VM by delegating from api-workflow stub
The workflow VM loads api-workflow.ts (via the "workflow" export condition)
which stubs all runtime functions. The start stub needs to check for the
injected WORKFLOW_START symbol and delegate to it, otherwise start() throws
"doesn't allow this runtime usage" in the workflow context.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Address PR review: fix stale WORKFLOW_SERIALIZE comments and register Run in host registry
- Update comments in step-handler.ts and start.ts to reference the actual
serialization mechanism (Run reducer with __serializable marker) instead
of the stale WORKFLOW_SERIALIZE reference
- Register Run class in the host's class registry from step-handler.ts so
the Run reviver can deserialize Run/WorkflowRun instances in step context
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add docs for recursive/repeating workflows and deploymentId: "latest"
- Document using start() for self-chaining workflows to avoid large event logs
- Add examples for batch processing and cron-like repeating patterns
- Document deploymentId: "latest" option with type safety warning
- Update skill file with same patterns
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Return full Run object from startFromWorkflow e2e workflow
Update the e2e workflow to return the childRun object directly instead of
just childRun.runId, exercising Run serialization across the workflow boundary.
Update e2e test assertions to match.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add recursive fibonacci e2e test for start() in workflow
Demonstrates recursive workflow composition: fibonacciWorkflow starts
new instances of itself via start() + Promise.all to compute fib(6)=8,
fanning out across independent workflow runs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Move Run method steps to builtins with "use step" directives
Refactor: instead of manually registering Run method steps via
registerStepFunction in step-handler.ts, define them as proper "use step"
functions in builtins.ts with __builtin_ prefix. This leverages the
existing SWC plugin infrastructure — functions starting with "__builtin"
get stable bare-name step IDs.
- Add __builtin_run_{cancel,status,return_value,...} to both builtins files
- Use dynamic import() for getRun inside step bodies to avoid pulling
Node.js modules into the workflow bundle
- Remove manual registerStepFunction calls from step-handler.ts
- Update WorkflowRun step references to __builtin_run_* names
- Fix step name display in web observability: fall back to raw name
instead of "?" for built-in steps that don't follow step//module//fn format
- Add fibonacciWorkflow default args for nextjs-turbopack workbench UI
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Render Run objects as clickable links in web observability UI
- Add RunRef type and Run reviver to observabilityRevivers so serialized
Run objects are hydrated as RunRef instead of showing raw Uint8Array
- Add RunRefInline component (purple badge with run ID) that navigates
to the target run on click, matching the StreamRef pattern
- Thread onRunClick callback through the component chain:
WorkflowTraceViewer → EntityDetailPanel → AttributePanel → DataInspector
- Wire up navigation in the web app's run-detail-view
- Add startFromWorkflow default args for workbench UI
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Throw error instead of silent fallback when Run class not in registry
Address PR review: the Run reviver now throws if the class isn't found
in the registry, instead of silently returning a plain { runId } object
that would break the assumption of getting a valid Run instance.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix e2e failures: allow retries on Run getter steps, fix docs code samples
- Remove maxRetries=0 from read-only Run getter steps (status, returnValue,
workflowName, etc.) — these are safe to retry and need retries when the
child workflow hasn't completed within the step timeout. Only cancel
keeps maxRetries=0.
- Fix docs code samples: use correct import path (workflow/api not workflow),
add declare statements for helper functions used in examples.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Use standard step//module//function naming for built-in steps
Update the SWC plugin's __builtin_ special case to generate proper
step//@workflow/core//{name} IDs instead of bare function names. This
makes parseStepName work correctly for built-in steps, showing:
- StepName: "Run#returnValue" (not "__builtin_run_return_value")
- ModuleSpecifier: "@workflow/core" (not the raw function name)
Convention: __builtin_Run_cancel → step//@workflow/core//Run#cancel
(uppercase prefix + underscore → instance method # notation)
- Move __workflow_start to builtins.ts as __builtin_start
- Rename __builtin_run_* to __builtin_Run_* for proper # notation
- Update WorkflowRun step refs to use full step// IDs
- Remove manual registerStepFunction from step-handler.ts
- Update SWC spec.md with new naming examples
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Remove SWC __builtin special case, use standard step naming for builtins
Remove the SWC plugin's __builtin_ special case so built-in steps get
standard step//{module}@{version}//{fn} IDs like any other step. This
makes parseStepName work correctly, showing proper StepName and
ModuleSpecifier in observability.
The VM reconstructs the same IDs via builtinStepId() which uses the
@workflow/core version to build: step//workflow/internal/builtins@{v}//{fn}
- Remove __builtin special case from SWC plugin (revert to original)
- Add builtinStepId() helper shared by workflow.ts, start.ts, run.ts
- Rename Run steps: __builtin_Run_cancel → Run_cancel, etc.
- Rename start step: __builtin_start → start
- Move start step from manual registerStepFunction to builtins.ts
- Keep __builtin_response_* names unchanged (pre-existing)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Use static class methods for Run steps to get Run.method naming
Refactor Run method steps from standalone functions (Run_cancel) to
static methods on a Run class, so the SWC plugin generates step IDs
with the standard static method convention: Run.cancel, Run.returnValue,
Run.status, etc.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Address PR review: tests, docs warnings, skill fix
- Add TODO on Run.returnValue about polling blocking (replace with system
hooks once AbortSignal/AbortController PR lands)
- Add docs callout warning about returnValue holding workers alive
- Fix SKILL.md contradiction that said start() can't be used in workflows
- Enhance suspension test to assert step arguments are forwarded
- Add WorkflowRun unit tests: serializable marker, runId, registry, delegation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix response builtins: adopt this-serialization from PR #1413
The rebase onto main didn't fully adopt PR #1413's refactor of response
builtins to use `this` instead of explicit parameters. The old pattern
(resJson(this) wrappers) passed `this` as an argument, but the step
functions now expect `this` to be set via method call context.
Switch to Object.defineProperties on Request/Response prototypes,
matching main's approach. Also document WORKFLOW_PUBLIC_MANIFEST=1
for local e2e testing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Address docs review: returnValue polling is temporary, link to start() API ref
- Update returnValue warning to note this is a temporary implementation
that will be replaced with internal hooks
- Replace inline deploymentId: "latest" docs with link to the existing
start() API reference which already covers it comprehensively
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix e2e tests: replace collectedRunIds with trackRun API
PR #1426 replaced the manual collectedRunIds array with a trackRun()
helper. The start() wrapper already auto-tracks, so just remove the
manual push calls and add trackRun for the child run.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Signed-off-by: Pranay Prakash <pranay.gp@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix(swc-plugin): closure variable detection for `new` expressions and module-level declarations
Fix two SWC compiler plugin bugs related to closure variable detection:
1. Add Expr::New handling to ClosureVariableCollector so `new Class(...args)`
properly captures both the callee and arguments as closure variables.
2. Exclude module-level declarations (functions, variables, classes) from
closure variable detection, preventing over-capturing of identifiers
that are already available in all bundles. This also allows DCE to
properly remove step-only helpers and their imports from the workflow
bundle.
Fixes#1365
* fix(swc-plugin): handle additional expression/statement types in closure variable collector
Expand closure variable detection to cover more AST patterns:
Expressions: Seq (comma), Yield, OptChain, Prop::Shorthand,
computed property keys, Prop::Assign defaults, Class (skip bodies)
Statements: Throw, Try/Catch/Finally, Switch, ForIn, ForOf,
DoWhile, Labeled
Also fix existing Prop::Shorthand bug where object shorthand
properties like { url } were not being collected as closure vars.
Extend test fixture with cases for all newly handled patterns.
Fix spec.md wording per review feedback.
* fix(swc-plugin): preserve original step function bodies in enclosing functions
In step mode, nested step functions were replaced with bare references
to the hoisted copy (e.g., `return hoisted$fn;`). This broke direct
calls because the hoisted copy uses `__private_getClosureVars()` which
only works in workflow context.
Now the original function body is preserved inline with just the
directive stripped, so JavaScript's normal closure semantics work for
direct calls. The hoisted copy with `__private_getClosureVars()` is
still registered for workflow-driven execution.
Fixes#1369
* fix(swc-plugin): restore metadata tracking for object property steps in step mode
The previous commit accidentally removed the
object_property_workflow_conversions tracking from the step mode path,
causing __internal_workflows metadata to be stripped from step bundle
output for object property step functions.
* fix(swc-plugin): detect closure variables inside nested function/method bodies
The closure variable collector was skipping nested function expressions,
arrow functions, and method bodies entirely. This meant closure
variables used deep inside inner functions (e.g., a variable used inside
a ReadableStream's start() method) were not captured.
Now the collector walks into nested function/arrow/method/getter/setter
bodies while adding their parameters to the local var set, so only
truly free variables from the outer step scope are captured.
Also add ReadableStream, WritableStream, TransformStream, and other
common Web API globals to the known globals list.
* update changeset to include Bug 4
* fix(swc-plugin): handle TypeScript expression wrappers and class bodies in closure detection
After comparing with Next.js's SWC plugin closure detection approach,
identified and fixed remaining gaps:
- TypeScript expression wrappers (as, satisfies, !, type assertions,
const assertions, instantiation expressions) now traverse to the
inner expression instead of being silently skipped
- Class expressions and declarations now walk their body members
(methods, properties, constructors, static blocks) to detect
closure variables used inside them
- Document all remaining safe-to-skip Expr variants (This, Lit,
SuperProp, MetaProp, PrivateName, Invalid, JSX)
* test: add fixture cases for TypeScript wrappers and class body closure detection
* test: add TypeScript fixture for closure detection through TS expression wrappers
Add a proper input.ts fixture that tests closure variable detection
through real TypeScript syntax: `as`, `satisfies`, `!` (non-null),
angle-bracket type assertions, `as const`, and generic function calls.
Update test harness to support input.ts files by adding swc_ecma_parser
dev-dependency and auto-detecting TypeScript syntax from file extension.
Remove the incorrectly placed TypeScript-related test cases from the JS
fixture (they were using plain JS syntax, not actual TS wrappers).
* first pass
* fix failing test
* Update changeset to fix SWC compiler issue
Fix bug where the SWC compiler bug prunes step-only imports in the client-mode transformation
Signed-off-by: Nathan Rajlich <n@n8.io>
* DCO Remediation Commit for nathancolosimo <nathancolosimo@gmail.com>
I, nathancolosimo <nathancolosimo@gmail.com>, hereby add my Signed-off-by to this commit: 150d175e7d
I, nathancolosimo <nathancolosimo@gmail.com>, hereby add my Signed-off-by to this commit: 429747fcc3
Signed-off-by: nathancolosimo <nathancolosimo@gmail.com>
---------
Signed-off-by: Nathan Rajlich <n@n8.io>
Signed-off-by: nathancolosimo <nathancolosimo@gmail.com>
Co-authored-by: Nathan Rajlich <n@n8.io>
When upstream bundlers like tsup/esbuild transform class declarations into anonymous class expressions (e.g. `var Foo = class { ... }`), the SWC plugin now re-inserts the binding name into the class expression (e.g. `var Foo = class Foo { ... }`). This preserves the class `.name` property through subsequent bundling passes, preventing runtime failures in downstream bundlers like Nitro's Rollup bundler that rely on the class expression name for serialization class registration.
Fixed a bug where step functions nested multiple levels deep in an object weren't being properly transformed.
### What changed?
- Enhanced the SWC plugin to recursively process nested objects to find step functions
- Added support for step functions defined inside deeply nested object properties, including function call arguments
- Implemented proper path handling for nested step IDs, using compound paths (e.g., `vade/tools/VercelRequest/execute`)
- Fixed variable name generation by replacing slashes with `$` to create valid JavaScript identifiers
- Updated documentation with examples of nested object property step functions
- Added test cases for deeply nested step functions and agent tool steps
### How to test?
Test with code that has step functions nested multiple levels deep in objects:
```javascript
export const config = createConfig({
level1: {
level2: {
level3: {
myStep: async (input) => {
"use step";
return input * 2;
},
},
},
},
});
```
Or with agent tools:
```javascript
export const vade = agent({
tools: {
VercelRequest: {
execute: async (input, ctx) => {
"use step";
return 1 + 1;
},
},
},
});
```
### Why make this change?
This fix ensures that step functions can be properly identified and transformed regardless of how deeply they are nested in object structures. This is particularly important for complex configurations like agent tools where step functions might be several levels deep in the object hierarchy.
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