* fix(core): make step-argument serialization failures catchable in workflow code
A step whose arguments fail to serialize is now finalized by the
suspension handler as step_created + step_failed (mirroring a step-body
failure) instead of rejecting the whole suspension. The next replay —
forced in-process, since no step message is dispatched for the failed
step — rejects the step's promise with the SerializationError, so a
try/catch around the step call observes it. Uncaught, the error
propagates out of the workflow body and fails the run as a fatal
USER_ERROR immediately, instead of redelivering the orchestrator
message until max deliveries (49/48) as reported in production on v4.
* Serialize the step_failed error with the VM global; one-sentence changeset
Addresses review feedback: dehydrateStepError in
finalizeUnserializableStep now receives suspension.globalThis like every
other dehydration in this file. Error detection is realm-independent, so
the host-created SerializationError serializes identically, but VM-realm
values guest code threw into the cause chain are now detected by the
realm-sensitive reducers.
* Address review: QuickJS engine support, deferred-batch join, drain gate, placeholder marker, telemetry, docs
- QuickJS: dumpPendingOps now catches a step input's serialization
failure per-op, reframes it as a SerializationError with the same
framed message as dehydrateStepArguments, and surfaces it on the
pending op instead of failing the whole collection. The entrypoint's
dispatchPendingOps finalizes such steps as step_created (placeholder
input) + step_failed, excludes them from inline claims and queue
publishes, marks them handled, and raises the requeue signal so the
failure is observed even when the feed lags — mirroring the node:vm
engine, so both engines agree: catchable in workflow code, USER_ERROR
with the framed message when uncaught. Both step-argument e2e tests
now pass on WORKFLOW_VM=quickjs.
- runtime.ts: the failed-step replay path now joins
suspensionResult.deferredBatchWork before continuing, so a trailing
chunk commit or step-message publish rejection propagates instead of
being swallowed after ack; committed inline claims are documented as
deliberately handed to owned recovery.
- Terminal drain: finalization is gated on a stepDispatch target. The
drain caller has no replay to observe a finalization, so a completed
run no longer gains failed-step rows for an unawaited unserializable
step — the rethrown error is swallowed by the drain's catch,
preserving its pre-existing behavior.
- The placeholder input now carries a marker string ('[input
unavailable: step argument serialization failed]', shared via
runtime/unserializable-step.ts) so inspect/o11y don't render the
failed step as a genuine zero-argument call.
- New workflow.steps.failed_serialization span attribute on the
suspension span, so occurrence is measurable without log search.
- Docs: v5 serialization-failed error page documents where each
boundary's failure surfaces (catchable step failure vs run failure)
and the no-retry USER_ERROR semantics; foundations/errors-and-retries
gains a Serialization Failures section with the try/catch shape.
* Guard the finalization crash window; self-contained docs samples
- A crash or transient failure between finalization's two durable
writes leaves a lone placeholder step_created, and redelivery then
dispatches the step through normal crash recovery — previously
running user code with the placeholder arguments. The placeholder
now carries a structural flag on the input triple's top level (which
user code never controls, so no false positives), and the step
executor checks it after hydration: instead of running the body, it
throws the intended fatal SerializationError, completing the
interrupted finalization as step_failed. Applies to both engines
(they share the placeholder and the executor).
- Regression tests: executor fails a placeholder-input step without
running the body (and doesn't trip on a genuine argument equal to
the display marker); handleSuspension rejects for redelivery when
step_failed can't be written after step_created landed, leaving the
recoverable placeholder behind; mixed bad-step + large fan-out
returns the failure set alongside still-pending deferredBatchWork
whose rejection surfaces — the contract the runtime's failed-step
join (added previously) relies on.
- Docs: the two new code samples are now self-contained so the docs
code-sample typecheck passes.
Signed-off-by: Nathan Rajlich <n@n8.io>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* Propagate trace context to vercel-workflow.com in workbench instrumentation
@vercel/otel only propagates W3C trace context to Vercel deployment URLs
by default, so outgoing requests to the workflow-server
(vercel-workflow.com) got a client span with no `traceparent` header —
breaking the APM trace link to workflow-server's spans. Add
`instrumentationConfig.fetch.propagateContextUrls` for the workflow-server
domain in every workbench that uses @vercel/otel: example,
nextjs-turbopack, nextjs-webpack, and sveltekit. The Next.js and SvelteKit
apps already declared @vercel/otel but weren't registering it at all; they
now do.
* Also propagate trace context to the Vercel Queue Service (vercel-queue.com)
The workflow-server queue path (@vercel/queue) sends to regional
vercel-queue.com subdomains (e.g. iad1.vercel-queue.com) when not using the
queues proxy, which were missing a `traceparent` header for the same reason
as vercel-workflow.com. Add `/vercel-queue\.com/` to propagateContextUrls in
all four workbench instrumentation configs.
---------
Signed-off-by: Pranay Prakash <pranay.gp@gmail.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Peter Wielander <mittgfu@gmail.com>
hono <4.12.25 is vulnerable to CVE-2026-54290 (GHSA-88fw-hqm2-52qc):
the CORS middleware reflects any request Origin with
Access-Control-Allow-Credentials: true when credentials are enabled and
origin is left at the default wildcard, exposing cookie-authenticated
endpoints to arbitrary origins.
- packages/world-testing: hono 4.12.21 -> 4.12.25 (the flagged manifest)
- workbench/hono: ^4.12.8 -> ^4.12.25, clearing the also-vulnerable
4.12.9 from the lockfile
Neither app uses hono's CORS middleware, so neither was exploitable, but
the bump clears the vulnerable code from the dependency tree. Only the
core Hono class is imported in world-testing; build and typecheck pass.
Signed-off-by: Pranay Prakash <pranay.gp@gmail.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Add a `--url` flag to `inspect`/`web` that prints a run's observability
dashboard deep link to stdout and exits — no browser, no local server —
so scripts and agents can share a link instead of opening a UI.
Fix the Vercel dashboard URL to the current
`…/workflows/runs/<id>?environment=<env>` route (drop the legacy
`/observability` segment) and respect `--env`. Apply the same route fix
to the e2e helpers, CI aggregation scripts, and the nextjs-turbopack
workbench. Document deep-linking in the workflow skill and observability
docs.
Signed-off-by: Pranay Prakash <pranay.gp@gmail.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Astro <6.4.6 is vulnerable to CVE-2026-54299 (GHSA-2pvr-wf23-7pc7, host
header SSRF in prerendered error page fetch). The fix only exists in the
6.x line — there is no 5.x backport — so this bumps:
- workbench/astro: astro ^6.4.6, @astrojs/node 10.1.4, @astrojs/vercel ^10.0.8
- packages/astro: astro devDependency 6.4.6 (typecheck only, not shipped)
Removes both vulnerable astro@5.16.3 and astro@5.18.0 from the lockfile.
Verified the example app builds under both the node and vercel adapters.
Signed-off-by: Pranay Prakash <pranay.gp@gmail.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* test: e2e coverage for run-idempotency conflict-handling strategies (#2387)
* test: e2e coverage for run-idempotency conflict-handling strategies
Covers the patterns documented in foundations/idempotency:
- claim-only hook mutex: token claimed and held with no payload data,
duplicate identifies the owner, token released after completion
- adopt the owner's result via conflict.returnValue
- signal the owner: duplicate forwards its payload via resumeHook
- supersede: duplicate cancels the owner and reclaims the token
- route-side resume-or-start retry pattern reaching the started run
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test: fix adopt-owner-result race — gate owner completion on observed conflict
On slow runtimes the duplicate's first invocation could land after the
owner completed and released the token, making the duplicate a fresh
owner that waits forever for a payload (90s timeout across CI matrices).
Poll the duplicate's event log for hook_conflict before resuming the
owner, and widen the test timeout for the added gate budget.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* review: assert superseded owner's returnValue rejection; empty changeset
- Await run1.returnValue and assert WorkflowRunCancelledError so the
cancellation is verified end-to-end and no rejection leaks from the
supersede test.
- Test-only PR: use an empty changeset.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* ci: retrigger preview deployments (turbopack deployment for 2e9d000 wedged in esbuild hang)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* ci: bust poisoned turbo cache entry for nextjs-turbopack build
The 2e9d000 deployment's next build crashed in an esbuild hang but its
task (70724907c9dd3a29) was recorded into the turbo remote cache anyway,
so every subsequent build with the same input hash replays the broken
artifact (missing routes-manifest). Change a build input to force a
fresh execution.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Pranay Prakash <pranay.gp@gmail.com>
* fix(backport): adapt conflict-handling tests to stable's getConflict/getRun API
The backport of #2387 used APIs that only exist on `main`, breaking the
nextjs-turbopack/webpack builds and the e2e suite on `stable`:
- `hookAdoptOwnerResultWorkflow`/`hookSupersedeOwnerWorkflow` read
`conflict.returnValue`/`conflict.cancel()`, but on `stable`
`getConflict()` resolves with `{ runId }`. Resolve the owning run via
`getRun(conflict.runId)` inside a step (the documented stable pattern)
to await its result / cancel it.
- Import `resumeHook` from `workflow/api` in 99_e2e.ts (was used by
`forwardPayloadToOwner` but never imported).
- Convert the backported `waitForHook(token, { runId })` call sites to
`waitForHookState(token, predicate)`; `waitForHook` does not exist on
`stable` (#2405 standardized on `waitForHookState`).
Both workbench builds and `biome check` pass locally.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(backport): import HookNotFoundError in e2e test
The backported conflict tests call `HookNotFoundError.is()` in
`hookClaimOnlyMutexWorkflow` (token-release wait) and the resume-or-start
route test, but the import was never carried into the stable test file —
causing a runtime `ReferenceError: HookNotFoundError is not defined`.
Import it from `@workflow/errors` (matches `main`).
Verified locally against nextjs-turbopack: the two previously-failing
tests plus the adopt/signal/supersede rewrites all pass (5/5).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Signed-off-by: Pranay Prakash <pranay.gp@gmail.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Pranay Prakash <pranay.gp@gmail.com>
* feat: replace hook.hasConflict with hook.getConflict (Promise<Run | null>)
hasConflict's boolean didn't expose WHICH run owns the token, so the
duplicate run couldn't act on the conflict. getConflict resolves with
null once registration commits, or with a Run handle for the conflicting
run — letting the workflow return/log the owner's runId, inspect its
status, await its result, or cancel it and continue, all in code.
The workflow-mode create-hook module exposes the bundle's compiled Run
class (durable step-proxy methods) on a well-known symbol so the host-
side hook consumer can construct the conflicting run inside the VM.
Contexts without the class (plain unit tests) fall back to a { runId }
object, which is also the documented v4 shape (no native Run
serialization in v4).
* fix: never resolve getConflict with a non-Run fallback shape
getConflict's contract is Promise<Run | null>. In the degenerate cases
where a real Run cannot be constructed — a hook_conflict event persisted
by an old world without conflictingRunId, or a context that never loaded
the workflow-mode create-hook module — reject with HookConflictError
instead of resolving with a { runId }-shaped impostor.
Test harnesses now register the Run class on the (VM) globalThis like
real bundles do.
* refactor: make getConflict a method — hook.getConflict()
A property getter that triggers registration/suspension reads as passive
state; a method makes the side effect explicit. Update implementation,
types, tests, e2e workflows, docs, and changeset.
* review: guard Run class registration, fix anchors, clarify changeset
- Only register WORKFLOW_RUN_CLASS when the workflow runtime is present
(WORKFLOW_CREATE_HOOK installed on globalThis), so host imports of the
workflow-mode module neither mutate the host global nor expose the
non-step-proxy host Run.
- Drop #run-idempotency link fragments — that section lands in the
stacked docs PR (#2011), which restores the anchored links.
- Note in docs that getConflict() rejects with HookConflictError for
legacy hook_conflict events lacking the owner's run ID.
- Changeset now calls out the hasConflict -> getConflict() replacement.
* refactor: resolve the conflicting Run through the serialization class registry
Replace the bespoke WORKFLOW_RUN_CLASS global with the registry the
serialization pipeline already uses to revive Run instances:
- The SWC plugin already auto-registers the workflow bundle's compiled
Run in globalThis[workflow-class-registry], but under a path-derived
classId the host cannot know statically. The workflow-mode create-hook
module now aliases it under a stable id (class//workflow//Run) via a
new aliasSerializationClass() helper (a plain registry entry —
registerSerializationClass cannot be reused since the plugin's IIFE
already defined the non-configurable classId property).
- createConflictingRun() looks the class up with
getSerializationClass(RUN_CLASS_ID, ctx.globalThis) and constructs
through its WORKFLOW_DESERIALIZE hook, exactly as the Instance reviver
would for a serialized Run crossing from a step into the workflow.
- Because the registry is keyed per-global, no environment guard is
needed: a stray host-side import registers the host Run on the host
registry, which is the correct class for that context. The
WORKFLOW_CREATE_HOOK guard, the ??=, and the WORKFLOW_RUN_CLASS symbol
are all deleted.
Verified: 1156 core unit tests; compiled workbench bundle contains the
stable alias alongside the plugin's path-derived registration with zero
WORKFLOW_RUN_CLASS references; all 5 hookGetConflict e2e tests pass
against a local nextjs-turbopack dev server, including conflict
resolution reading conflict.status through a durable step.
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Nathan Rajlich <n@n8.io>
* feat: add hook ready promise
* test: cover hook ready continuation scheduling
* feat: replace hook.ready with hook.hasConflict (Promise<boolean>)
- hook.hasConflict resolves true when the token is owned by another
active hook, false once registration is committed — no throw, so
workflows can branch on conflicts early. Awaiting it suspends the
workflow to commit the hook registration (createHook alone does not).
- Chain the already-created fast-path through promiseQueue so
resolution order matches event-log order (review feedback).
- Skip inline step execution when a suspension has an awaited hook
creation so the hasConflict continuation can advance independently
of step execution (review feedback).
- Update unit tests, e2e tests, workbench workflows, and v4/v5 docs.
* docs: fix inconsistent hasConflict bullet in create-webhook reference
State both resolution values explicitly (true = token already owned,
false = registered) instead of a parenthetical that only described the
false case.
* docs: require docs preview links in PR descriptions for docs changes
* docs: restore SWC Plugin heading in AGENTS.md
---------
Signed-off-by: Pranay Prakash <pranay.gp@gmail.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Nathan Rajlich <n@n8.io>
* test(e2e): cover WritableStream passed as start() argument
Adds an e2e workflow + test where a parent workflow gets a WritableStream
via getWritable(), forwards it through start() to a child workflow, and
the child step writes raw bytes to it. Asserts the external reader on
the parent's stream observes the exact bytes the child wrote.
* fix(core): avoid double-framing when WritableStream is forwarded via start()
When a workflow's getWritable() handle is passed across start() to a
child workflow, the parent step's reviver wraps it in a serialize
transform that pipes into a workflow server stream. Until now,
getExternalReducers.WritableStream then installed a second serialize
transform on top of that — so every chunk the child step wrote got
devalue-framed twice but only deframed once on the reader side, and
external consumers saw the inner frame instead of the original bytes.
Fix: tag every user-visible writable that's already backed by a
workflow server stream with its (runId, name). When the external
reducer recognizes those tags during dehydration, it bridges bytes
straight from the new child-side server stream to the original server
stream instead of piping through the user's writable. That leaves the
producer-side serialize transform (installed once by the child's step
reviver) as the only framing layer in the chain.
* fix(core): forward (runId, name) when a tagged WritableStream crosses start()
Replaces the previous in-process bridge with first-class writable
forwarding at the descriptor level. When a parent workflow's
getWritable() handle is passed as an argument to a child workflow,
the dehydrated descriptor now carries the original (runId, name).
The child run's step-side reviver opens the writable against the
parent's server stream directly and resolves the parent run's
encryption key (encrypt-only) via getEncryptionKeyForRun.
This removes the architectural limitation that the bridge could
only stay alive for the duration of the parent step process — on
Vercel that capped forwarding at ~15 minutes regardless of the
child run's lifetime, dropping any writes the child made after the
parent step process exited.
importKey() now accepts a usages parameter, defaulting to
['encrypt', 'decrypt']. The cross-run forwarding path imports with
['encrypt'] only so a compromised child run cannot decrypt any
existing data on the parent's stream — only contribute new writes.
* test: rename writable-forwarded workflows and cover step-context getWritable()
Addresses PR review:
- Rename writableForwardedToChildChildWorkflow → writableForwardedChildWorkflow
(drops the duplicated 'Child' segment).
- Split writableForwardedToChildWorkflow into two variants covered by a
test.each: writableForwardedFromWorkflowWorkflow (workflow-context
getWritable, the original test) and writableForwardedFromStepWorkflow
(step-context getWritable passed directly into start() from the same
step that called getWritable()).
- Terser changeset description.
Signed-off-by: Nathan Rajlich <n@n8.io>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Nathan Rajlich <n@n8.io>
Turborepo replays nextjs-turbopack:build from cache without restoring the
Vercel diagnostics manifest (.vercel/output/diagnostics/workflows-manifest.json),
which causes the Vercel deployment to fail post-build. Add .vercel/output/**
to the workbench's Turbo outputs so it is persisted and replayed. Applies to
both nextjs-turbopack and nextjs-webpack (whose turbo.json is a symlink).
Signed-off-by: Nathan Rajlich <n@n8.io>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* [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.
Signed-off-by: Nathan Rajlich <n@n8.io>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Nathan Rajlich <n@n8.io>
* Bump vite (#1827)
* test: enable step source-map assertions for vite local dev (#1862)
* test: enable step source-map assertions for vite local dev
Vite ^7.3.2 (bumped in #1827) preserves step bundle source maps in
dev mode, so stack traces now contain original file paths. Update
hasStepSourceMaps() so vite returns true in local dev and stays false
only in local prod, fixing the consistently failing 'basic step error'
and 'cross-file step error' e2e tests.
* chore: drop body from empty changeset
* address review: drop redundant vite local-prod guard
The default `!DEV_TEST_CONFIG` fall-through already returns false for
vite local prod, so the vite-specific guard is dead code. Just remove
the vite block entirely now that vite local dev matches the default
'has source maps' behavior.
---------
Co-authored-by: Karthik Kalyan <105607645+karthikscale3@users.noreply.github.com>
* move swc-playground-wasm into workbench/swc-playground
Move the Rust/WASM source from packages/swc-playground-wasm into
workbench/swc-playground/wasm/ so it is no longer built as part of
the packages/* turbo filter in CI. The WASM build now runs as part
of the playground's own prebuild/dev scripts, avoiding Rust toolchain
failures in unrelated CI jobs.
* fix: ensure default rustup toolchain in wasm build
The Vercel build environment has rustup installed but no default
toolchain configured. Add a check for this and install stable
if needed. Also check for VERCEL env var in addition to CI.
* fix: ensure cargo bin dir is in PATH on Vercel
After cargo install wasm-pack, the binary lives in $CARGO_HOME/bin
which may not be in PATH on the Vercel build environment. Always
add the cargo bin directory to PATH after ensuring the toolchain.
Co-authored-by: Nathan Rajlich <n@n8.io>
* Rename 'Workflow Development Kit' / 'DevKit' to 'Workflow SDK' across docs, code, and config
Follow-up to cdf90d5a38 (#1541)
* Fix missing </h1> closing tag and add article 'the' before 'Workflow SDK' in docs
* Move SWC playground transform from server action to client-side WASM
Create a new swc-playground-wasm crate that bundles swc_ecma_parser,
swc_ecma_codegen, and the swc_workflow transform visitor into a single
WASM binary via wasm-bindgen/wasm-pack. This runs the code
transformation entirely in the browser, eliminating the server action
round-trip and serverless function cold starts on every keystroke.
- New packages/swc-playground-wasm Rust crate targeting wasm32-unknown-unknown
- Exposes transform() and transformAll() functions via wasm-bindgen
- Client loads WASM + JS glue from public/wasm/ as static assets
- Removed @swc/core and @workflow/swc-plugin server-side dependencies
- Removed serverExternalPackages/outputFileTracingIncludes Next.js config
- Added WASM loading indicator and error state in the UI
- Reduced transform debounce from 500ms to 300ms (no network latency)
* Add missing extends key to swc-playground-wasm turbo.json
* Fix build: ensure rustup is available for wasm32-unknown-unknown target
The Vercel build environment has a system Rust without rustup, so
the wasm32-unknown-unknown target can't be managed. Now the build
script checks for rustup specifically (not just cargo) and installs
it when missing, matching the pattern in swc-plugin-workflow/build.js.
* Add workflow type definitions to Monaco editor for intellisense
Auto-generate type declarations from built .d.ts files of workflow,
@workflow/core, @workflow/errors, @workflow/world, and @workflow/utils
packages. Register them with Monaco's TypeScript language service via
addExtraLib() so imports like 'workflow' resolve with full types,
eliminating red squiggles and enabling autocomplete/hover info.
* Fix Monaco type resolution: register root index.d.ts for each package
Monaco's NodeJs module resolution looks for index.d.ts at the package
root, not just in dist/. Register the main entry .d.ts content at both
paths (dist/ and root) so bare imports resolve with full type info.
Also remove the 2307 diagnostic suppression so non-existent imports
correctly show errors.
* Fix Monaco tooltip overflow by enabling fixedOverflowWidgets
* Fix hydration mismatch, Monaco type resolution paths, and WASM init warning
- Fix hydration mismatch: gate Reset button disabled state on isHydrated
so server and client render consistently during hydration
- Fix Monaco types: use bare node_modules/ paths instead of file:///
URIs for addExtraLib, which is what Monaco's NodeJs resolver expects
- Remove virtual package.json entries (Monaco doesn't use them)
- Fix wasm-bindgen init deprecation: pass object { module_or_path }
instead of bare string argument
* Fix Monaco module resolution: set model URI to file:/// and match addExtraLib paths
Monaco's TypeScript NodeJs resolver needs the editor model and the
addExtraLib entries to share the same URI scheme. Set the input editor
model path to file:///src/input.tsx and register all type declarations
at file:///node_modules/... so resolution of bare imports like
'workflow' correctly finds the virtual node_modules.
Also register a root index.d.ts for packages like @workflow/world that
lack an explicit 'types' field in their package.json exports.
* Use declare module ambient declarations for reliable Monaco type resolution
Replace the virtual node_modules filesystem approach with declare module
ambient declarations. This is the standard approach used by TypeScript
Playground and StackBlitz — it works regardless of Monaco's internal URI
scheme and module resolution quirks.
The generation script now:
- Registers all .d.ts files at file:///node_modules/<pkg>/dist/... paths
- Generates a global ambient declarations file with declare module blocks
that map bare import specifiers to their .d.ts entry points
- Includes @workflow/serde and workflow sub-exports (api, errors, observability)
- Supports configurable sub-export mappings per package
* Inline types into declare module blocks for full Monaco type support
Replace the export-from-file approach with fully inlined declare module
blocks. The script now reads each .d.ts entry point, recursively inlines
all relative imports, strips external import statements (resolved via
other declare module blocks), and produces a single ambient declarations
string.
This correctly handles:
- unique symbol exports (@workflow/serde)
- cross-package re-exports (workflow re-exporting from @workflow/core)
- JSDoc comments preserved for hover documentation
- Sub-path exports (workflow/api, workflow/errors, workflow/observability)
- Added @workflow/serde package
* Add workspace packages as dependencies so Turbo builds their types
The generate-monaco-types script reads .d.ts files from the built
dist/ directories of workflow, @workflow/core, @workflow/errors, etc.
On Vercel, Turbo only builds explicit dependencies — without these
workspace references, the packages were never built and the dist/
directories didn't exist, resulting in 0 type modules generated.
* Add @types/node declarations to Monaco editor
Register all @types/node .d.ts files via addExtraLib so Node.js
built-in modules (crypto, fs, path, etc.) are available in the
playground editor with full type information.
* Collect @types/node .d.ts files recursively to include subpath modules
The previous non-recursive scan missed subdirectory files like
fs/promises.d.ts, stream/web.d.ts, dns/promises.d.ts, etc., causing
'Cannot find module node:fs/promises' errors.
* feat: enhance error handling for missing workflow functions
Slack-Thread: https://vercel.slack.com/archives/C09G3EQAL84/p1773856370214769?thread_ts=1773856370.214769&cid=C09G3EQAL84
Co-authored-by: Pranay Prakash <1797812+pranaygp@users.noreply.github.com>
* fix: update step not found handling to match FatalError pattern
Move step function validation after step_started and call step_failed directly if not found.
Co-authored-by: Pranay Prakash <1797812+pranaygp@users.noreply.github.com>
* changes
Signed-off-by: Pranay Prakash <pranay.gp@gmail.com>
* feat: add StepNotRegisteredError and WorkflowNotRegisteredError semantic errors
Introduce dedicated error types for when step/workflow functions are not
registered in the current deployment, replacing generic WorkflowRuntimeError.
These are infrastructure errors (not user code errors) with proper error
slugs, docs pages, and a new FUNCTION_NOT_REGISTERED error code.
Step not found fails the step (like FatalError) so the workflow can handle
it gracefully. Workflow not found fails the run.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address PR review comments
- Remove FUNCTION_NOT_REGISTERED error code, use RUNTIME_ERROR instead
- Use .is() instead of instanceof for WorkflowRuntimeError check in runtime.ts
- Remove non-working example from WorkflowNotRegisteredError docs (custom
errors not serialized yet)
- Update all references from FUNCTION_NOT_REGISTERED to RUNTIME_ERROR
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add e2e tests for step/workflow not registered errors and fix docs typecheck
E2E tests:
- WorkflowNotRegisteredError: start a run with a fake workflowId, verify
the run fails with RUNTIME_ERROR
- StepNotRegisteredError (caught): workflow catches the step failure,
verify workflow completes and step is marked failed
- StepNotRegisteredError (uncaught): verify the run fails when workflow
doesn't catch the error
Step not registered is tested by manually invoking useStep with a
non-existent step ID in the workflow VM — this is the same pattern the
SWC transform generates for real step calls.
Also fix docs typecheck by using declare/\@setup pattern instead of
\@skip-typecheck for code samples.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: cast globalThis to any for Symbol index access in e2e workflow
TypeScript's strict mode doesn't allow using a symbol to index
globalThis. Cast to any since this runs in the workflow VM where
the symbol is defined.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: classify WorkflowNotRegisteredError as RUNTIME_ERROR
The .is() check uses name-based matching, so WorkflowNotRegisteredError
(name='WorkflowNotRegisteredError') doesn't match WorkflowRuntimeError.is().
Add explicit check in classifyRunError so the error code is RUNTIME_ERROR
instead of USER_ERROR.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: use instanceof for WorkflowRuntimeError checks, improve docs
Address PR review feedback:
1. Revert .is() checks back to instanceof WorkflowRuntimeError in
runtime.ts and classify-error.ts. instanceof catches all subclasses
(current and future), which is the correct behavior for these catch
blocks.
2. Remove duplicated try/catch example from step-not-registered-error
API reference (troubleshooting page already has it).
3. Add Callout in API reference docs clarifying that .is() works in
server-side Node.js code but not inside "use workflow" functions
where errors arrive deserialized from the event log.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* changes
Signed-off-by: Pranay Prakash <pranay.gp@gmail.com>
---------
Signed-off-by: Pranay Prakash <pranay.gp@gmail.com>
Co-authored-by: v0 <v0[bot]@users.noreply.github.com>
Co-authored-by: Pranay Prakash <1797812+pranaygp@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: bump next to 16.2.1
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: run deferred Next dev e2e assertions on stable
Bump Next.js to 16.2.1 in docs and swc-playground and update lockfile.
* fix(next): copy all deferred step sources for step-mode transforms
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: JJ Kasper <jj@jjsweb.site>
Co-authored-by: Peter Wielander <mittgfu@gmail.com>