A workflow name is derived from the module path it is defined in, so Next.js
App Router conventions end up in the name verbatim. `SAFE_WORKFLOW_NAME_PATTERN`
permitted alphanumerics, `_`, `-`, `.`, `/` and `@`, but not parentheses or
square brackets. Any workflow inside a route group (`app/(dashboard)/...`) or a
dynamic segment (`app/[teamId]/...`, `app/[...slug]/...`) threw
Invalid workflow name "workflow//./app/(group)/workflows/": must only
contain alphanumeric characters, ...
before it could be enqueued, and the generated name cannot be overridden.
The pattern exists to keep unsafe characters out of the queue name it is
interpolated into. These four are inert there: `ValidQueueName` accepts any
suffix after its prefix, and the only other consumers of the name are OpenTelemetry
span names. It is never placed in a URL or a SQL identifier.
Fixes#3991
Signed-off-by: Matias Torsello <23641125+torsello@users.noreply.github.com>
Signed-off-by: Matias Torsello <matiastorsello@gmail.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Matias Torsello <23641125+torsello@users.noreply.github.com>
Co-authored-by: Pranay Prakash <pranay.gp@gmail.com>
* fix(swc-plugin): register class expressions via an IIFE and reject unnameable classes
Class expressions with "use step" methods or custom serialization were
registered by module-level statements referencing the class by name. When no
module-scope binding could be resolved the plugin fell back to a placeholder
`AnonymousClass` identifier, which is a guaranteed ReferenceError at module
evaluation (vercel/workflow#3929). Other shapes were silently wrong as well:
`var A = class {}, B = class {}` registered A's steps under B, `X = class {}`
assignments and classes nested inside functions emitted unresolvable
references.
Class expressions are now wrapped in a single IIFE that receives the class,
performs every registration recorded for it, and returns it, so the
registration no longer depends on a name being in scope. The class name is
still needed for step/class IDs and is derived from the assigned variable,
the class's own identifier, or the property key it is assigned to
(`exports.Foo = class {}`, `{ Foo: class {} }`). When none is available, or
the class is declared inside a function, the plugin emits a compile error
instead of broken code.
Class declarations keep their existing module-level output; the emitters
were factored so both paths share the same statement builders.
* fix(swc-plugin): generate names for anonymous class expressions instead of erroring
With registration happening inside the IIFE, an anonymous class expression
in a position that provides no name (`foo(class { ... })`, an array element,
a conditional branch) only needs a name for its step/class IDs. Generate a
deterministic `AnonymousClass<N>`, counting only anonymous classes that have
something to register, instead of rejecting them. Classes declared inside a
function remain an error.
Dead-code elimination now keeps module-level declarations whose initializer
contains a wrapped class expression: evaluating the initializer is what
registers the class, and the binding may be otherwise unreferenced.
Signed-off-by: Nathan Rajlich <n@n8.io>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Long live reads were ending silently at the server's 2-minute connection
cap: the max-duration abort reaches the client as a clean EOF on some
transport paths, and the reader read that as end-of-stream. On EOF it
now consults `streams.getInfo` and reconnects from the next chunk unless
the stream is done and every chunk up to the tail was delivered. A
failed metadata read trusts the EOF, so a transient blip can't fail a
healthy completion.
Tests added for the reconnect, verified-completion, metadata-failure,
and reconnect-budget paths.
---------
Signed-off-by: Alex Langenfeld <alex.langenfeld@vercel.com>
Signed-off-by: Alex Langenfeld <alexlangenfeld@gmail.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Pranay Prakash <pranay.gp@gmail.com>
* feat(world-local): add WORKFLOW_LOCAL_RECOVER_ACTIVE_RUNS env var (#2914)
The recoverActiveRuns factory option had no environment variable, so
disabling startup re-enqueueing of pending/running runs required a custom
world module via WORKFLOW_TARGET_WORLD. Wire an env fallback
(0/false disables, 1/true enables, explicit factory option wins) and
document it in the worlds configuration reference and local world guide.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Karthik Kalyan <105607645+karthikscale3@users.noreply.github.com>
* Fix: Destructuring a non-existent `clearCache` property from `createStorage()` return causes TS2339 build failure in `packages/world-local/src/index.ts`
This commit fixes the issue reported at packages/world-local/src/index.ts:65
## Bug
At `packages/world-local/src/index.ts:65` the backport introduced:
```ts
const { clearCache: clearStorageCache, ...storage } = createStorage(
mergedConfig.dataDir,
tag
);
```
`createStorage` returns `LocalStorage`, defined in `storage/index.ts:13` as:
```ts
export type LocalStorage = Omit<Storage, 'runs'> & { runs: LocalRunsStorage };
```
On the `stable` branch this type has **no** `clearCache` property (a repo-wide grep found no `clearCache` definition anywhere under `packages/world-local/src` — the only match was this destructuring itself). Destructuring a property that doesn't exist on the type triggers:
```
src/index.ts(65,11): error TS2339: Property 'clearCache' does not exist on type 'LocalStorage'
```
Additionally, `clearStorageCache` was never referenced after being bound, so even if the property existed it would be dead code.
**Trigger:** Any `tsc` build/typecheck of the `world-local` package fails deterministically — all 15 deployment builds reported the identical error.
## Fix
The `clearCache` destructuring is unrelated to the intended backport (which is only the `resolveRecoverActiveRuns` env-var fallback). Reverted line 65 to the original form:
```ts
const storage = createStorage(mergedConfig.dataDir, tag);
```
This removes the reference to the non-existent property while preserving the env-var feature, resolving the TS2339 error.
Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Co-authored-by: VaguelySerious <mittgfu@gmail.com>
---------
Signed-off-by: Karthik Kalyan <105607645+karthikscale3@users.noreply.github.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: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com>
Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Co-authored-by: VaguelySerious <mittgfu@gmail.com>
* 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>
Instead of accessing private files from SvelteKit, we use `@sveltejs/load-config` to load the Svelte config (that package also knows about checking Vite config). The deadlock is avoided by having a module-level `Set` to see if we're currently recursing or not.
This is necessary for SvelteKit 3 since there the config lives exclusively in the vite config, and the previous logic did not handle that.
This also uncovered that we're needlessly rebuilding the generated files in sub builds/workers (SvelteKit, at least below 3, starts off secondary builds; and some things are done in workers), which a new file cache now checks.
---------
Signed-off-by: Simon Holthausen <simon.holthausen@vercel.com>
* test: add failing test for Date subclassing in workflow VM
* fix(core): preserve `new.target` in the deterministic `Date` override so `Date` subclasses work in workflow functions
The VM's `Date` override was a plain function, so `class X extends Date`
lost the subclass identity: `super()` returned a fresh plain `Date` that
became `this`, dropping the subclass's methods and fields. This silently
broke `Date` subclasses like `TZDate` from `@date-fns/tz`.
Using `class Date extends Date_` keeps `new.target` intact, and `extends`
already wires up the prototype chain and statics, so the manual
`prototype` assignment and `Object.setPrototypeOf` fix-ups are no longer
needed. Determinism is unchanged: zero-arg construction still returns the
fixed timestamp and `Date.now()` is still overridden.
Fixes#3371
* test: add failing test for calling `Date()` without `new`
* fix(core): keep `Date()` callable without `new`
Use a plain function that branches on `new.target` and constructs via
`Reflect.construct(Date_, args, new.target)` instead of a class: subclassing
still works (`new.target` is forwarded), and calling `Date()` without `new`
now matches the spec — arguments are ignored and the (fixed) time string is
returned, where the previous override returned a `Date` object.
* chore: update changeset to match the final `Reflect.construct` implementation
---------
Signed-off-by: ar_tama <arata.makoto@gmail.com>
Signed-off-by: Makoto Arata <arata.makoto@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>