Commit Graph

994 Commits

Author SHA1 Message Date
Ziyak Jehangir c56c4f6c7e Fix compatibility with Zod 4.4.x (#1902)
* Fix compatibility with Zod 4.4.x

* DCO Remediation Commit for Ziyak <ziyak97@gmail.com>

I, Ziyak <ziyak97@gmail.com>, hereby add my Signed-off-by to this commit: 2bf58718e6

Signed-off-by: Ziyak <ziyak97@gmail.com>

---------

Signed-off-by: Ziyak <ziyak97@gmail.com>
Signed-off-by: Nathan Rajlich <n@n8.io>
Co-authored-by: Nathan Rajlich <n@n8.io>
2026-05-04 22:57:37 +00:00
Nathan Rajlich 5f22832675 Serialize run_failed/step_failed errors through serialization pipeline (#1851)
* Serialize run_failed/step_failed errors through serialization pipeline

Switch run_failed, step_failed, and step_retrying events to persist
the full thrown value via the workflow serialization pipeline (as
SerializedData / Uint8Array) instead of a lossy { message, stack, code }
StructuredError shape. Consumers hydrate via hydrateRunError /
hydrateStepError to reconstruct the original thrown value, preserving
Error subclass identity, cause chains, and custom properties.

- WorkflowRun.error and Step.error are now SerializedData
- WorkflowRun gains a top-level errorCode plaintext field
- WorkflowRunFailedError.cause is now the hydrated thrown value
- Adds world-postgres migration 0010_add_error_code.sql
- Legacy pre-pipeline errorJson records surface as undefined on read

* Update Next.js workbenches for new WorkflowRunFailedError.cause type

cause is now `unknown` (the hydrated thrown value) rather than
`Error & { code }`. Defensively extract Error-shaped fields when the
hydrated value is an Error, otherwise round-trip the raw value, and
expose the new `errorCode` classification field.

* Update docs for WorkflowRunFailedError.cause: unknown

The hydrated `cause` is now `unknown` (the original thrown value
through the serialization pipeline) and the error classification has
moved to the top-level `errorCode` property. Update the two affected
docs pages and the `TSDoc` interface to reflect the new shape, and
narrow `cause` with `instanceof Error` before accessing fields.

* Expand test coverage for the run/step error serialization pipeline

Unit tests:
- 19 new dehydrate/hydrate{Step,Run}Error round-trip tests covering
  FatalError, plain Error, built-in Error subclasses, non-Error thrown
  values (string, plain object), cause chains, encryption round-trip,
  the binary format prefix contract, and the unserializable / unknown-
  format error paths.
- 5 new tests for Run.returnValue when the run is failed: hydrated
  FatalError + cause as cause, plain Error preservation, non-Error
  thrown values surfaced verbatim, cross-class cause chains, and the
  hydration-failure fallback that still surfaces errorCode.

E2E tests (new, in 99_e2e.ts + e2e.test.ts):
- Step throw → workflow catch round-trips a FatalError with a TypeError
  cause chain, asserting class identity, fatal marker, and cause name +
  message all survive the step_failed event pipeline.
- Workflow throw → run_failed reaches  status with the new
  top-level errorCode metadata exposed (cause-shape coverage lives at
  the unit level, since the SWC plugin's class registration is not
  invoked in the plain-Node e2e runner).
- Workflow throw of a non-Error value round-trips that value verbatim
  as WorkflowRunFailedError.cause.

Adjustments to existing assertions:
- error.cause is now ; tests narrow with
  and use the new top-level  field instead of .
- step.error / run.error from CLI --withData are now hydrated payloads:
  unregistered class instances surface as Instance refs whose
  carries the original message + stack.

Observability hydration:
- hydrateStepIO / hydrateWorkflowIO in serialization-format.ts now
  hydrate the  field via hydrateData, so the CLI and web UI
  continue to surface readable run/step error messages and stacks.

* Tighten error serialization changeset description

* Trim error serialization changeset to a single sentence

* Resolve FatalError/RetryableError revivers via cross-realm registry

When a workflow runs in a Node `vm` context, its bundled
`@workflow/errors` is a different module instance than the host's
import (separate prototype chains, separate class identity). Calling
`new FatalError(...)` from the host-side reviver produces a
host-realm instance that fails `err instanceof FatalError` checks
in the workflow code — even when the serialized payload was correctly
tagged via the dedicated `FatalError` reducer.

Surfaced by the local-prod e2e "step throw round-trips FatalError"
test on Next.js Turbopack: each route gets its own bundled chunk, so
the flow handler's `@workflow/errors` and the workflow VM bundle's
`@workflow/errors` are two distinct copies of the same module.

Fix:

- Each bundled copy of `@workflow/errors` self-registers its
  `FatalError` and `RetryableError` classes on `globalThis` via
  `Symbol.for("@workflow/errors//FatalError")` /
  `Symbol.for("@workflow/errors//RetryableError")`. First load wins
  per realm; the descriptor is non-writable / non-configurable to make
  accidental clobbering loud.

- The revivers in `@workflow/core`'s common reducers module read the
  consumer's `globalThis` (passed in as `global`) to pick up the
  realm-local class, falling back to the host-imported class when no
  registration is present (e.g. in the CLI / test runner).

* Use `types.isNativeError` to remap workflow stacks across VM realms

The runtime's run-failure path computes a source-map-remapped stack
and then assigns it back onto the thrown value via `if (err
instanceof Error) err.stack = errorStack`. Workflows run inside a
Node `vm` context, so a workflow-thrown error is an instance of the
VM realm's `Error` — `instanceof` against the host realm's
`Error` returns `false`, the assignment is skipped, and the
serialized `run_failed` event carries the un-remapped (bundled-line-
number) stack instead of the source-mapped one.

Switch the gate to `types.isNativeError`, which uses V8's internal
type tag and works across realms — same approach already in place
for the serialization reducers.

Caught by the local-prod e2e "nested function calls preserve message
and stack trace" and "cross-file imports preserve message and stack
trace" tests, which assert that the persisted run-error stack
contains `99_e2e.ts` / `helpers.ts`.

* Sync CLI revivers with core + add toJSON shim for Error subclasses

Two issues with the CLI's hand-rolled reviver list:

1. It hadn't been updated for the new first-class Error subclass
   reducers (`TypeError`, `RangeError`, `FatalError`, `RetryableError`,
   etc.). devalue throws "Unknown type X" when it encounters a
   reduced value with no matching reviver, and `hydrateResourceIO`
   swallows that error and surfaces the raw `Uint8Array` payload —
   so `step.error` / `run.error` showed up as raw byte dumps in
   `workflow inspect` output.

2. Even with all the right revivers, `Error.prototype`'s `message`
   / `stack` / `cause` are non-enumerable, so `JSON.stringify`
   (used by `workflow inspect --json`) drops them — leaving the
   subclass-specific enumerable fields (e.g. `FatalError.fatal`)
   visible but the actual error data missing.

Fix:

- Build the CLI reviver set on top of `getCommonRevivers()` from
  `@workflow/core` so the CLI stays in sync with the runtime's
  reducer set automatically. New core reducers/revivers will Just
  Work without any CLI-side change.

- Wrap each Error reviver from the common set with a thin shim that
  attaches a non-enumerable `toJSON` method to the produced
  `Error` instance. `JSON.stringify` calls `toJSON` and gets a
  full object (`name` + `message` + `stack` + `cause` + any
  enumerable subclass fields like `fatal` / `retryAfter` /
  `errors`); `util.inspect` ignores `toJSON` and renders the
  canonical `Error: msg\\n at ...` format. Best of both worlds for
  CLI output without compromising the runtime hydration path.

Caught by the local-prod e2e "basic step error preserves" and
"cross-file step error preserves" tests, which read
`failedStep.error.message` / `.stack` from the CLI's JSON output.

* Clarify parseErrorJson JSDoc to match its always-null return

The previous JSDoc described preserving legacy values "for best-effort
hydration" which contradicted the implementation, where legacy errors
are intentionally surfaced as absent (the pre-pipeline shapes can't be
hydrated by the new error revivers). Rewrite the comment so the contract
matches behavior. Also rename the now-unused parameter to `_errorJson`
to reflect that the function ignores it.

Caught by a code review on #1851.

* Refine error-handler ergonomics on the step / run hot paths

Three review-driven adjustments that all touch the queue handlers and
their interaction with the error serialization pipeline:

1. Memoize the per-run encryption key fetch. The step handler used to
   eagerly fetch + import the key at the top of every step delivery so
   the value would be in scope for every potential dehydrateStepError
   path. That pessimized step-started early-return cases (the fetch
   happens unconditionally even when the step never reaches user code)
   and required duplicating the same boilerplate at four call sites in
   runtime.ts. Introduce `memoizeEncryptionKey(world, run)` in
   runtime/helpers.ts that returns a lazy, single-fetch accessor;
   step-handler / runtime call sites use `await getEncryptionKey()`
   instead. The first caller pays the fetch cost, subsequent callers
   await the cached promise, and steps that fail before any
   encryption-aware work happens skip the fetch entirely.

2. Preserve the prior attempt's serialized error as the cause on the
   defensive max-retries-exceeded `step_failed` re-invocation guard.
   The existing comment explicitly opted out of cause attachment, but
   the symmetric post-failure path below already does this and the
   reviewer is right that consumers shouldn't have to walk the
   step_retrying event history to recover the underlying error. Best-
   effort: if hydration of the prior `step.error` throws, fall back
   to a FatalError without cause rather than letting the event write
   itself fail.

3. Document the intentional `unflatten` throw in
   `hydrateStepError` / `hydrateRunError` for non-Uint8Array input.
   SDK version is pinned per workflow run via skew protection so the
   non-binary branch is dead in production; if a misshapen value
   reaches it, surfacing the throw via the surrounding o11y try/catch
   is more debuggable than masking it. Add a comment so future
   reviewers don't reach for a defensive fallback.

A standalone `falls back to plaintext` suggestion on the run_failed
key fetch was rejected: when encryption is configured we should fail
loudly rather than silently emit plaintext error data. The queue's
redelivery semantics will retry the key fetch; persistent KMS outages
get logged with the existing "persistent error preventing the run from
being terminated" message rather than a security regression.

* Hydrate `event.eventData.error` in event listings

`hydrateEventData` enumerated the per-event fields that need
hydration (`result`, `input`, `output`, `metadata`, `payload`)
but omitted the new `error` field on `step_failed`,
`step_retrying`, and `run_failed` events. Without this branch,
o11y tools that list events (e.g. `workflow inspect events`) surface
the raw `Uint8Array` payload instead of a hydrated
`{ name, message, stack, … }` object even though the entity-level
`Run.error` / `Step.error` paths already hydrate.

Mirrors the existing per-field branches; the `try/catch` leaves the
field un-hydrated on parse failure rather than failing the whole
event view. Adds a unit test.

* Use `.is()` static checks in `classifyRunError` for cross-realm safety

Workflows execute inside a separate `vm` realm: the
`WorkflowRuntimeError` class bundled into the workflow code and the
host-imported one are distinct constructors, so an
`err instanceof WorkflowRuntimeError` check on a VM-thrown error
returns `false` and we'd misclassify genuine runtime errors (corrupted
event log, missing timestamps, workflow/step not registered) as user
errors.

Switch to each subclass's `.is()` static (a name-based duck check that
works across realms). Since `WorkflowRuntimeError.is` only matches its
own concrete name, enumerate every concrete subclass we want to
recognize (`StepNotRegisteredError`, `WorkflowNotRegisteredError`)
in a `RUNTIME_ERROR_CHECKS` table; keep that table in sync with the
class hierarchy in `@workflow/errors`.

Existing `classify-error.test.ts` already covers `WorkflowRuntimeError`
and `WorkflowNotRegisteredError` cases — both still pass.

* Add e2e coverage for step throws of non-Error values

We had `errorWorkflowThrowNonErrorValue` (workflow body throws a plain
object — round-trips verbatim as `WorkflowRunFailedError.cause`) but
no symmetric coverage for the step-throw side. Step-throw goes through
a different code path: non-Error values aren't recognized as
`FatalError` (no `name === 'FatalError'`) nor `RetryableError`,
so they take the transient retry path. After max retries the runtime
wraps the original thrown value as `cause` on a fresh `FatalError`
which the workflow's catch block then sees.

Add a workflow that throws a recognizable plain object from a step
with `maxRetries = 0` (so we exhaust on first attempt and avoid a
long test wait) and a workflow that asserts the wrapped FatalError
shape: `isFatal`, `instanceof FatalError`, message includes the
original object's serialized form, `cause` is the original non-Error
object verbatim with structure preserved.

Documents the current retry-then-wrap behavior so any future change
to "non-Error throws skip retries" semantics has to update the test.

* Note legacy postgres error-data loss in the run/step error changeset

Pre-upgrade failed runs that wrote into world-postgres's deprecated
`error` text column can't be hydrated through the new pipeline (the
shape is incompatible with the new revivers). The new runtime
intentionally surfaces them as `error: undefined` on read; the
original payload is still readable directly from the `errorJson`
column for manual inspection. Add a one-sentence note to the
changeset's migration text so consumers upgrading don't get blindsided
by suddenly-empty error fields on historical runs.
2026-05-04 15:18:46 -07:00
JJ Kasper 00a011dee4 Add stable Next.js eager and lazy test coverage (#1747)
* Add stable Next.js eager and lazy test coverage

* Address PR review feedback

* Fix eager Next step route builds

* Fix eager Next manifest refreshes

* Fix eager Next e2e stack assertions

* Externalize native step bundle bindings

* Lazy load Vercel world runtime

* Fix Next dev step sourcemap assertions

* Consolidate eager build changesets

* Fix Vercel world tracing in Next deployments

* Externalize Vercel world in Next builds

* Fix webpack tracing for Vercel world deps

* Fix eager workflow route bundling

* Rely on Next server externals
2026-05-04 21:09:20 +00:00
Nathan Rajlich 7c45e9e213 Enforce per-(run, correlation) uniqueness for entity-creating events in world-postgres (#1878)
Adds a unique partial index on workflow_events(run_id, correlation_id, type)
filtered to step_created/hook_created/wait_created, and translates the
resulting unique-violation (pg code 23505, surfaced via DrizzleQueryError.cause)
into EntityConflictError. The steps table already deduped via
onConflictDoNothing, but the event row still inserted, leaving duplicate
events in the log. Now both rows are kept consistent and the runtime's
existing dedup catch path handles concurrent writers cleanly.
2026-05-04 19:36:32 +00:00
Karthik Kalyan 5eb0b792b8 fix(world-vercel): add default request timeout to workflow-server HTTP calls (#1807)
* fix(core): add hard exit deadline to replay timeout handler

When the replay timeout fires on attempt 4+ and tries to write a
run_failed event, the process.exit(1) call is gated behind an await
on world.events.create(). If workflow-server is unresponsive and the
response never arrives, process.exit(1) is never reached, causing the
function to idle until the platform kills it at maxDuration (e.g. 15m).

Add a 30s hard exit deadline (setTimeout → process.exit) that fires
independently of the run_failed write, guaranteeing the process exits
promptly even when the server hangs.

Made-with: Cursor

* Update packages/core/src/runtime.ts

Co-authored-by: Peter Wielander <mittgfu@gmail.com>
Signed-off-by: Karthik Kalyan <105607645+karthikscale3@users.noreply.github.com>

* Update .changeset/fix-replay-timeout-exit.md

Co-authored-by: Peter Wielander <mittgfu@gmail.com>
Signed-off-by: Karthik Kalyan <105607645+karthikscale3@users.noreply.github.com>

* Update packages/core/src/runtime/constants.ts

Co-authored-by: Peter Wielander <mittgfu@gmail.com>
Signed-off-by: Karthik Kalyan <105607645+karthikscale3@users.noreply.github.com>

* fix(world-vercel): add default request timeout to workflow-server HTTP calls

Move the hang mitigation from the core replay-timeout handler to the
world-vercel transport layer, per review feedback. A default
AbortSignal.timeout(60s) now applies to every makeRequest() call,
covering all world.* API methods (not just the replay timeout path).
Timeouts are converted to WorkflowWorldError so existing catch sites
handle them uniformly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(world-vercel): drop incident-specific latency note from timeout comment

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(world-vercel): address review nits on makeRequest timeout

- Compose per-request timeout with caller-provided AbortSignal via
  AbortSignal.any() so a future caller passing options.signal doesn't
  silently lose the hang protection (and vice versa).
- Move the floating eslint-disable-next-line for the undici dispatcher
  cast back next to the actual `fetch(... as any)` call where the
  suppression applies, instead of pointing at `const fetchStart`.

Both nits flagged by @VaguelySerious in the AI review on PR #1807.

Made-with: Cursor

* Apply suggestions from code review

Co-authored-by: Peter Wielander <mittgfu@gmail.com>
Signed-off-by: Karthik Kalyan <105607645+karthikscale3@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: Peter Wielander <mittgfu@gmail.com>
Signed-off-by: Karthik Kalyan <105607645+karthikscale3@users.noreply.github.com>

---------

Signed-off-by: Karthik Kalyan <105607645+karthikscale3@users.noreply.github.com>
Co-authored-by: Peter Wielander <mittgfu@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-04 09:39:20 -07:00
Peter Wielander 6dd5c72d8a Allow disabling step sourcemap with new sourcemap option in builders (#1842) 2026-05-04 11:00:15 +00:00
Nathan Rajlich 45d1eb2340 web: configure vercelPreset() for Vercel deployments (#1815)
* web: configure vercelPreset() for Vercel deployments

Enables per-route bundle splitting, function-level configuration, and an
accurate Deployment Summary for the packages/web Vercel deployment.

Gated on WORKFLOW_WEB_VERCEL_BUILD (rather than the ambient VERCEL var)
so that the standard build layout consumed by server.js (self-hosted
deployments and the CLI's in-process server via @workflow/web/server) is
still produced when the package is packed as a tarball by the docs
Vercel deployment. Existing VERCEL-based checks in vite.config.ts have
been migrated to this same variable for consistency.

* web: allow WORKFLOW_WEB_VERCEL_BUILD through Turborepo

Turborepo strips environment variables not declared in turbo.json, which
prevented WORKFLOW_WEB_VERCEL_BUILD (set on the Vercel project) from
reaching the build. Declare it in the env list and also include the
.vercel/ directory (where the preset writes react-router-build-result.json)
in the build outputs.

* web: narrow Turbo output to react-router-build-result.json

* web: address PR review feedback

- Check WORKFLOW_WEB_VERCEL_BUILD === '1' explicitly (rejects '0', 'false',
  etc.) in both react-router.config.ts and vite.config.ts
- Use double-quoted package name in changeset frontmatter to match the
  repo's dominant convention
2026-05-04 10:04:21 +00:00
Peter Wielander 8ea1532e48 [core] Combine flow+step bundle and process steps eagerly (#1338) 2026-05-04 09:53:02 +00:00
Peter Wielander 9d94cf45cc [world-vercel] Revert stream close control framing (#1891)
This reverts commit 340c085681.
2026-05-04 06:29:08 +00:00
Pranay Prakash 1203dae70c Friendlier workflow errors (consolidated) (#1849)
* Introduce structured context-violation errors + Ansi renderer

Phase 1: Add Ansi rendering helpers (frame, hint, note, help, code, inline)
to @workflow/errors, and a chalk mock for readable snapshot tests.

Phase 2: Add four context-violation error classes to @workflow/core
(NotInWorkflowContextError, NotInStepContextError,
NotInWorkflowOrStepContextError, UnavailableInWorkflowContextError)
and apply them to all twelve user-facing throw sites so errors now
include docs links and a structured "what/why/fix" frame.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Address review: tighten changeset, implement ansifyName, harden Ansi

- Tighten phase 1 changeset to a single sentence (per pranaygp review) and switch to double-quoted frontmatter (per Copilot + repo convention).
- Implement `ansifyName` to actually apply dim styling to workflow/ / step/ prefixes; add an `Ansi.dim` helper to `@workflow/errors` so callers don't need to import chalk directly.
- Remove the `void getWorkflowMetadata;` workaround in context-errors.ts by dropping the unused value import (we only needed the type and symbol).
- Render the plain-Error throw in `workflow/get-workflow-metadata.ts` with `Ansi.frame` + docs link so the VM path matches the structured-class styling from the sibling step path (still uses a plain Error to avoid the module-init cycle).
- Guard `buildUnderline` against zero-length markers so a stray empty token can't produce a negative `String.repeat` count.

* Structured runtime logger metadata + fold in replay-timeout logging

Adds a `.child()` and `.forRun(runId, workflowName)` child-logger API to
the structured logger so runtime/step code doesn't have to repeat
`workflowRunId`/`workflowName`/`stepId` on every call. Normalizes error
metadata to structured `errorName` / `errorMessage` / `errorStack` fields
instead of ad-hoc `error: err.message` strings, and adds comments to
silent catches that swallow expected idempotency conflicts.

Also folds in the pending changes from #1812 so that PR can be closed:

- Standardize the console prefix to `[workflow-sdk]`.
- Split the replay-timeout log into a warn-while-retrying vs.
  error-when-giving-up, and surface the underlying error when we can't
  mark a timed-out run as failed.
- Include the error stack in the "Fatal runtime error during workflow
  setup" log and in the top-level user-code workflow error log so the
  stack surfaces in flattened log drains.
- Drop the `[Workflows] "<runId>" - ` prefix from
  `buildWorkflowSuspensionMessage` — the structured logger now attaches
  run context.

Supersedes #1812.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Use double-quoted changeset frontmatter per repo convention

* Add SerializationError + apply to user-facing serialization sites

Phase 4 of friendlier errors: introduce a `SerializationError` class with
an optional `hint` and a docs link (workflow-sdk.dev/err/serialization-failed),
and adopt it at every user-facing serialization boundary in @workflow/core:

- Locked ReadableStream at a workflow boundary
- Unregistered class / missing `classId` / missing `WORKFLOW_DESERIALIZE`
- Attempting to return step functions to clients or call workflow functions
  directly
- Webhook `respondWith()` called outside a step
- `dehydrate*` / `getSerializeStream` failures (workflow args/return, step
  args/return, stream chunks)

Internal invariants (format prefix length checks, unknown format bytes,
missing `STREAM_NAME_SYMBOL`, encryption key/size guards, etc.) now throw
`WorkflowRuntimeError` instead of plain `Error` so the classifier and logger
treat them consistently.

`formatSerializationError` now returns `{ message, hint }` so the hint
fragment can be rendered with the standard SerializationError framing
instead of being baked into the message string.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Use double-quoted changeset frontmatter per repo convention

* Presentation-only user vs SDK error attribution

Add describeError() that derives attribution and class-aware hints from
existing error classes + RUN_ERROR_CODES — no event data changes. Wire into
step failures, max-delivery exhaustion, run failures, and fatal setup errors
so terminal logs include errorAttribution and a hint for known error types.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Address review: describeError accepts precomputed errorCode + instanceof

- `describeError(err, errorCode?)` now accepts an optional precomputed
  `RunErrorCode`. `classifyRunError(err)` only narrows to USER_ERROR /
  RUNTIME_ERROR, so the REPLAY_TIMEOUT and MAX_DELIVERIES_EXCEEDED branches
  were previously unreachable from the step / run failure log sites.
  Callers that know the failure category (runtime.ts for replay timeout and
  max-deliveries exhaustion) now pass the code in.
- Context-violation checks use `instanceof` against the actual classes from
  context-errors.ts instead of a name-string set. Type-safe + survives
  class renames.
- Wire the new hints through to the REPLAY_TIMEOUT and MAX_DELIVERIES_EXCEEDED
  log sites so those branches actually render a hint now.
- 3 new tests cover the reachable code paths + precomputed-code override.
- Changeset frontmatter switched to double quotes per repo convention.

* Cosmetic consistency pass on remaining bare throws

Internal invariants now use WorkflowRuntimeError so describeError attributes
them to the SDK: missing startedAt, VM generateKey, closure-vars outside
step context, ENOTSUP. defineHook().resume() formats schema validation
failures as a readable list instead of a JSON blob.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Use double-quoted changeset frontmatter per repo convention

* Data-driven describeRunError + expose via @workflow/core/describe-error

Observability renderers read persisted run_failed / step_failed event data,
not live Error instances. describeRunError takes { errorCode, errorName }
and returns the same { attribution, hint } shape as describeError, so the
CLI and web UI can derive user-vs-SDK framing from the event log directly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Friendlier build-time errors: WorkflowBuildError class + applications

Add `WorkflowBuildError` class in `@workflow/errors` with optional `hint`
for an actionable next step, and apply it in `@workflow/builders` at
user-facing sites: failed esbuild phases, unresolved built-in steps, and
empty esbuild output now throw `WorkflowBuildError` with a hint pointing
at the likely fix. Runtime invariants remain plain `Error`.

* Polish friendlier-errors rendering: drop functionName leak, simplify docs link, redirect stack

- Drop the readonly `functionName` param-property on context-error classes so
  util.inspect no longer prints a trailing `{ functionName: 'foo()' }` block.
- Replace the `DocLink` ("label: https://…") shape with a plain `DocsUrl`
  template-literal type. Error output now renders a single clean line:
  `docs: https://…` (new `Ansi.docs` helper) instead of the noisier
  "note: Read more about foo(): https://…".
- Add throw helpers (`throwNotInWorkflowContext`, etc.) that call
  `Error.captureStackTrace(err, stackStartFn)` on V8 engines so the top frame
  of the thrown error points at the user's call site instead of at the gate
  function inside the framework. Callers pass themselves as the boundary.
- Refactor `defineHook()` (both root and `/workflow`) to use named function
  closures rather than `this.create`/`this.resume`, since the stack redirect
  relies on a stable function identity that survives destructuring.
- Update context-errors.test.ts to snapshot the new `docs:` framing and to
  add a regression test asserting the top stack frame is the user call site.

* Consolidate friendlier-errors stack: fix ANSI leak + non-retry semantics

Addresses PR review feedback across the 8-phase friendlier-errors stack and
fixes issues surfaced by manual testing (createHook() inside a step):

- ANSI no longer leaks into .message / .stack. Context-violation errors
  now store plain text on .message and render the colored framed form
  lazily via [util.inspect.custom] / toString(). Structured logs, log
  drains, CBOR-serialized events, and JSON payloads no longer contain
  raw \x1B[...m bytes.

- Context violations are now fatal. ContextViolationError sets
  fatal = true; FatalError.is(err) recognizes any error with a
  fatal: true own property. Calling createHook() from a step no longer
  burns three retry attempts on a guaranteed-to-fail context violation.

- Ansi helpers moved to @workflow/errors/ansi subpath so imports from
  @workflow/errors no longer pull chalk into consumers that only want
  error classes (addresses reviewer VaguelySerious).

- Shared redirectStackToCaller helper in packages/core/src/capture-stack.ts,
  used by both context-errors.ts and workflow/get-workflow-metadata.ts
  (addresses Copilot review on #1849).

- Structured framed content: ContextViolationError now takes a structured
  FramedContent (title segments + detail branches) and renders plain/pretty
  from the same source of truth.

Tightens the eight existing phase changesets to 1-2 sentences each and adds
four new scoped changesets (errors-ansi-subpath, context-errors-plain-message,
context-errors-fatal, capture-stack-shared) for the followup fixes, so the
final changelog history stays readable.

* test: update step-handler mocks for scoped forRun() logger

The runtime logger now uses .forRun(runId, name, {stepId, stepName})
to attach scope context, so 409-handling log calls no longer repeat
{workflowRunId, stepId} in every metadata bag — those live on the
scoped logger instance. Update the mock to return itself from forRun()
and tighten assertions to check both the log args (errorName/errorMessage)
and the forRun() scope.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Mark SerializationError fatal + route dehydration through step-failure path

SerializationError now carries readonly fatal = true. Step-return
dehydration is wrapped inside the user-code try/catch so that the
resulting error flows through userCodeFailed → step_failed →
FatalError.is() short-circuit instead of bubbling up as HTTP 500 and
triggering a queue retry loop. Retrying a step that returned a non-POJO
is guaranteed to fail the same way, so this saves ~20s and 3 near-
identical error blocks per serialization failure.

* Add logging snapshot tests + manual-test artifacts

Snapshot tests lock in the exact shape of:
- describeError() payloads (attribution, errorCode, hint) for every
  classification — plain Error, SerializationError, context-violation,
  WorkflowRuntimeError, REPLAY_TIMEOUT, MAX_DELIVERIES_EXCEEDED.
- The scoped-logger call signature for the two canonical runtime
  failure paths (fatal-bubble and hit-max-retries), so refactors of
  forRun() / child() metadata merging can't silently change what users
  see in their log drains.

SerializationError now also has a direct test for readonly fatal=true
+ FatalError.is() recognition.

pr-artifacts/ contains real log-output snapshots from running the
nextjs-turbopack workbench against five error scenarios. These are
reference material for reviewers and are flagged to be removed before
merge.

* Readable step-fatal logs: inline stack + friendly step/workflow names

The step-level fatal-error log used to embed the full stack trace inside
an `errorStack` string field in the metadata object, so util.inspect
rendered it as a quote-escaped, line-continuation blob when the log
hit the terminal — unreadable in practice. Move framing + stack into
the log *message* (matching the workflow-level log in runtime.ts) and
keep the metadata object compact with only the indexable structured
fields (`errorAttribution`, `errorName`, `errorMessage`, `hint`,
IDs). Log drains still get the same keys; humans now see a readable
stack trace.

Also introduce `formatStepName` / `formatWorkflowName` in
`@workflow/utils` that render machine names
(`step//./workflows/1_simple//add`) as `add (./workflows/1_simple)` in
log framings, using the existing `parseStepName` / `parseWorkflowName`
parsers. Applied to step-fatal, hit-max-retries, exceeded-max-retries,
and workflow-threw log sites.

Artifacts in pr-artifacts/ updated to show the new output shape, and
renamed .log → .md since they're Markdown and IDE previews are nicer
that way.

* Opinionated pretty formatter for runtime structured-log metadata

Replace util.inspect's default object dump (which quote-escapes
multi-line stacks and paragraph hints into a single-line JSON-y blob)
with a workflow-aware formatter that composes the entire log line
into a single string passed to console.error / console.warn.

Highlights of the new output:
- Per-run / per-step IDs render with their parsed friendly names so
  users see `wrun_… · simple (./workflows/1_simple)` instead of just
  the raw `workflowName: 'workflow//./workflows/1_simple//simple'`.
- Color-coded attribution badge (user error red / sdk error magenta)
  paired with the error class in bold.
- Hints render as a paragraph under `hint:` rather than a backslash-
  `\n`-escaped string.
- Drops redundant fields (errorStack always; errorMessage when it's
  already in the parent message) to avoid double-printing.
- Unknown fields fall through as a sorted `key  value` tail so we
  never silently drop log information.

@workflow/errors/ansi gains bold/red/magenta helpers used by the
formatter. The web / web-shared packages don't consume stderr — they
read structured event payloads from the World event log — so this is
presentation-only at the runtime layer.

* ci(benchmarks): disable pnpm cache for getCommunityWorldsMatrix

The job never runs `pnpm install` (it just calls `node` against a
checked-in script), so the pnpm store path never exists. The post-job
`actions/setup-node@v4` cache-save then fails with `Path Validation
Error: Path(s) specified in the action for caching do(es) not exist`
and red-X's the entire job even though the matrix step succeeded.

The setup-workflow-dev composite already has a `cache-pnpm` opt-out
input for this exact case — wire it through here.

* Address PR review comments: inspect dedup, cause leak, retry-loop tests

- ContextViolationError: util.inspect(err) duplicated every framed detail
  line because the stack-tail strip only sliced the first message line.
  V8's Error.stack reads `Name: messageLine1\n  messageLine2\n  at ...`,
  so for our multi-line `title\n╰▶ docs: …` messages every detail line
  was getting prepended twice (once in the pretty form, once via the
  unsliced message tail). Count the actual message lines and slice past
  all of them. Repro test asserts `╰▶ docs:` appears exactly once.

- WorkflowError: stop assigning `cause: undefined` as an enumerable own
  property when no cause is provided. Subclasses (every error in this PR)
  inherit the parent constructor; the unconditional assignment polluted
  `util.inspect(err)` output with `{ cause: undefined, … }` on every
  no-cause instance. The `super(...)` call already conditionally sets
  `.cause` non-enumerably when `options.cause` is provided.

- step-handler.test.ts: add a regression-gate suite that exercises the
  fatal-vs-retryable retry-loop wiring directly. Asserts that an error
  with `fatal: true` produces exactly one `step_failed` event with no
  `step_retrying`, and that a non-fatal `Error` retries via
  `step_retrying` on early attempts and emits `step_failed` once the
  retry budget is exhausted. Catches the silent-regression case where
  `fatal = true` is removed from a context-violation error class but
  the `FatalError.is()` unit tests stay green.

* Consolidate changesets + remove pr-artifacts

Address review feedback to drastically shorten the changesets — fold
the 15 file-by-file entries into a single user-facing changeset for
@workflow/core / errors / builders / utils. Also drop the pr-artifacts/
folder (reviewer-only log captures, no longer needed).

* Polish runtime error logging: layout, stack trim, hint consolidation

Five user-driven fixes from manual smoke-testing of #1849:

1. Logger layout. composeLogLine() now puts the structured-fields block
   (attribution badge, run/step IDs, error code) **between** the framing
   line and the stack body, instead of after it where 30+ lines of stack
   buried the most useful information. The framing stays at the top,
   stack at the bottom, structured info readable at a glance.

2. Stack trim. Drops framework-internal frames (`node_modules/.pnpm/`,
   `node:internal/`, Turbopack-bundled `node_modules__pnpm_*` chunks,
   `_next_dist_*` chunks) and caps the surviving frame count at 6
   so the stack stays compact even on heavy async wrappers. Suppressed
   runs emit one summary line so users know the trim happened.

3. Wrapper-route noise. The nextjs-turbopack workbench's start route
   was catching `WorkflowRunFailedError` rejection on
   `Promise.race([readLoop(), run.returnValue])` and re-logging it via
   `console.error('Error in workflow stream:', error)` plus
   `controller.error(error)` — which then triggered Next.js's
   `⨯ failed to pipe response` overlay. The SDK already logs the
   failure cleanly upstream and the runId is on the response header, so
   the wrapper now closes the SSE stream cleanly on
   WorkflowRunFailedError.

4. Consistent framed `╰▶ hint:` / `╰▶ docs:` layout for all errors
   that carry a hint or docs slug. WorkflowError, SerializationError,
   and WorkflowBuildError now share one `appendFramedDetails` helper
   matching the box-drawing structure that ContextViolationError
   already used. Was: blank-line-separated `Learn more: <url>`. Now:
   one tree, indistinguishable from context-violation rendering.

5. Drop the duplicate logger-side `hint` field. Hints now live on the
   error message only — actionable hints get serialized into the event
   log, rehydrated on the workflow side, and shown in observability
   automatically. The previous logger-only hint duplicated stderr but
   never made it past the step boundary.

   Updated SerializationError hint to point at the foundations doc
   ("Ensure you're returning workflow serializable types. Check the
   serialization docs to see what's serializable:
   https://workflow-sdk.dev/docs/foundations/serialization") instead
   of the hardcoded `(plain objects, arrays, primitives, …)` list,
   which drifted out of sync as the supported types grew. Same hint
   reuses for step args, workflow args/return, stream messages, and
   any other site that goes through `formatSerializationError`.

Also retitled the retry summary `3 retries` → `3 max retries` since
"3 retries" next to "4 attempts" was ambiguous (already-happened vs.
budget).

* Trim error-card title + drop machine step name from persisted error

- ErrorStackBlock (web observability): show just the first non-empty
  trimmed line of the error message in the card title with single-line
  truncation. Multi-line messages (`Failed to serialize step return
  value\n╰▶ hint: …`) were rendering the entire framed body in the
  title, pushing the copy button off-screen and burying the
  scannability of the headline. Full message stays in the body via
  the stack (V8 prepends `Name: message` to `Error.stack`), so no
  information is lost; hover-tooltip exposes the full title text.

- Persisted error message: drop the `Step "step//./.../foo"` machine
  name from `Step failed after N retries: …` and `Step exceeded max
  retries (…)` strings. Observability already attributes the event
  to a specific step via the UI tree, and the CLI logger emits the
  friendly `Step foo (./...) hit max retries` framing on its own
  line. Embedding the raw `step//./...` machine name in the persisted
  message text was duplicate noise.

* Update .changeset/friendlier-errors.md

Co-authored-by: Peter Wielander <mittgfu@gmail.com>
Signed-off-by: Pranay Prakash <pranay.gp@gmail.com>

* Update .changeset/pretty-log-format.md

Co-authored-by: Peter Wielander <mittgfu@gmail.com>
Signed-off-by: Pranay Prakash <pranay.gp@gmail.com>

* Update SerializationError snapshot tests for slug-less message

The class no longer attaches a slug-based `╰▶ docs:` line — the
foundations URL is embedded directly in the hint via the
`formatSerializationError` helper in @workflow/core. Update the test
expectations accordingly:

- bare-title case is now a single line (no docs link)
- hint case renders one `╰▶ hint: …` branch (no second branch)

* Update serialization.test.ts hint assertions for foundations URL

Four `should throw error for an unsupported type` cases were still
asserting on the old hardcoded type list. Update to the new hint
phrasing that points at the foundations doc, matching the change in
`formatSerializationError` (`packages/core/src/serialization/errors.ts`).

---------

Signed-off-by: Pranay Prakash <pranay.gp@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Peter Wielander <mittgfu@gmail.com>
2026-05-04 05:04:24 +00:00
Peter Wielander 8202663857 [workbench] Add TanStack Start workbench and tests (#1875) 2026-05-04 00:42:44 +00:00
Nathan Rajlich 92dc82608a Atomically dedupe duplicate step_created/wait_created events in world-local (#1877)
Concurrent invocations producing identical correlationIds (as the snapshot
runtime does by design across replays) previously both succeeded and
persisted duplicate events. step_created had no guard at all; wait_created
used a TOCTOU read-then-check that allowed both writers through under
concurrency. Both now claim a per-(runId, correlationId) constraint file
with O_CREAT|O_EXCL before writing, so the loser surfaces as
EntityConflictError — which the runtime's dedup catch path already
handles.
2026-05-04 00:21:53 +00:00
Nathan Rajlich 98ca60c318 Replace fixed-sleep hook waits with event-driven waitForHook helper (#1879)
Hook-related e2e tests (hookWorkflow, hookCleanupTestWorkflow,
hookDisposeTestWorkflow, hookWithSleepWorkflow, distributedAbortController)
previously slept a fixed 5 seconds before calling getHookByToken to wait
for the hook to be registered. On slower runtimes — notably the snapshot
runtime on Vercel where each workflow round-trip is several seconds longer
than replay — that fixed budget is too tight and the test fails with
HookNotFoundError. On faster runtimes it's unnecessarily slow.

Adds a waitForHook(token, { timeoutMs, intervalMs, runId }) helper that
polls until the hook resolves or the timeout (default 30s) expires, with
an optional runId filter for token-reuse tests where eventually-consistent
backends may briefly still report a stale hook. Each hook-wait site now
uses this helper. Non-hook fixed sleeps (workflow-progress polling for
sleepingWorkflow cancel tests, payload-processing waits in
hookWithSleepWorkflow) are unchanged.
2026-05-04 00:05:11 +00:00
Peter Wielander c1163eb146 [world-local] Reject run_failed on non-existent run (#1894) 2026-05-02 10:45:05 +00:00
Peter Wielander 2f52d14f38 [vitest] [world-local] Fix local-world data recovery isolation (#1895) 2026-05-02 19:30:40 +09:00
Nathan Rajlich cd50618d1f ci: switch Vercel deployment-protection bypass to OIDC Trusted Sources (#1882)
* ci: switch Vercel deployment-protection bypass to OIDC Trusted Sources

The e2e, benchmark, and docs-smoke CI jobs previously used the static
`VERCEL_AUTOMATION_BYPASS_SECRET` deployment-protection bypass token
to reach protected Vercel deployments. Switch them over to the new OIDC
Trusted Sources flow: the GitHub Actions runner mints a short-lived
OIDC token via `core.getIDToken()` and forwards it on requests in the
`x-vercel-trusted-oidc-idp-token` header.

Each workbench project (and `workflow-docs`) has been configured with a
matching trusted-source rule:
  aud=https://github.com/vercel, repository=vercel/workflow

The shared header helper now lives at `scripts/trusted-sources-headers.mjs`
and is imported by both the e2e/bench tests and the docs smoke script,
removing the previous duplication.

* rename to VERCEL_OIDC_TOKEN and wire through world-vercel

- Rename the env var from VERCEL_TRUSTED_OIDC_TOKEN to VERCEL_OIDC_TOKEN
  to match Vercel's convention (also read by @vercel/oidc's
  getVercelOidcToken()).
- In @workflow/world-vercel, replace the legacy
  VERCEL_WORKFLOW_SERVER_PROTECTION_BYPASS / x-vercel-protection-bypass
  flow with VERCEL_OIDC_TOKEN / x-vercel-trusted-oidc-idp-token. The
  trusted-source header is attached on every outbound workflow-server
  request (both proxied through api.vercel.com and direct).
- Drop the bypass header from the encryption-key and
  resolve-latest-deployment fetches: those go to api.vercel.com which
  is public.
- Drop VERCEL_WORKFLOW_SERVER_PROTECTION_BYPASS plumbing from tests.yml.
- Update the pending world-vercel changeset to describe the final
  trusted-sources flow.

* .

* .

* ci: add statuses:read permission for wait-for-vercel-project action

The action queries /commits/{sha}/status (Commit Statuses API) in addition
to the Deployments API, in order to extract the Vercel `dpl_...` ID. With
an explicit permissions block in place, GITHUB_TOKEN now needs
`statuses: read` or the action 403s when resolving the deployment ID.

Reported by Copilot review on #1882.

* ci(docs): log status code and body when waitForServer times out

Helps diagnose deployment-protection / OIDC-trusted-source bypass
failures (e.g. SSO redirects) on the workflow-docs preview.

* ci(docs): log OIDC token claims (aud, repository, etc.) for diagnostics

Helps determine whether the bypass is failing because of missing
trusted-source config, claim mismatch, or audience mismatch.

* ci(docs): add curl debug step to verify OIDC header reaches Vercel

* .

* ci: remove debug logging now that trusted-sources config is correct

The fetch-failure root cause was the trusted-sources rule format: the
labs workbench projects had been PATCHed with just `to.slugs` (no
`preset`), but Vercel's edge requires the dashboard-form-style
`to.preset: 'all-custom'` field plus `development` in the slug list to
match incoming requests. After re-PATCHing all projects with the
correct format, the bypass works end-to-end.

* ci(docs): debug — test trusted-sources bypass against docs and labs deployments

Trying repository_owner claim added to one labs project to see if that
fixes the bypass.

* ci(docs): revert curl debug step

The GitHub Actions OIDC trusted-sources bypass returns 401 on all tested
projects regardless of claim configuration (including workflow-docs which
was set up via the dashboard). This is not a per-project config issue.
Need to investigate with Vercel team before continuing.

* ci(docs): probe trusted-sources bypass and surface x-vercel-id

Adds a debug step that does two HEAD requests against the docs preview
deployment (with and without the OIDC trusted-sources header) and prints
the response status line plus `x-vercel-id` for each. The proxy-side
trusted-sources changes for GitHub Actions OIDC tokens are rolling out
gradually (~12+ hours), so the edge-node identifier in `x-vercel-id`
helps explain why a request might succeed or fail during the rollout
window.

Also includes `x-vercel-id` in the `waitForServer` timeout error so
post-mortem analysis of failing runs has the same edge-node info.

* ci(docs): drop trusted-sources curl probe — bypass works once proxy fix reaches the serving edge node

The probe served its purpose: confirmed the bypass is functional once
the request lands on a region that has the proxy-side trusted-sources
fix rolled out. The waitForServer error message still surfaces
x-vercel-id for any future rollout-window debugging.

* .

* world-vercel: log outbound OIDC token claims once per process

Adds a one-shot diagnostic that prints the non-sensitive claims of the
OIDC token (`iss`, `aud`, `owner_id`, `project_id`, `environment`,
`sub`, `scope`, `exp`) on the first request that uses bearer auth.

This is invaluable for debugging Vercel deployment-protection
trusted-source rule mismatches: a 401 from the edge tells you nothing
about why the rule didn't match, and the token's claims are the only
thing that determines that. The signature is never logged.

Gated to once per process — Vercel-issued tokens are process-stable for
the lambda's lifetime so further log lines would just be redundant
spam.

* world-vercel: route trusted-sources header through getVercelOidcToken()

The Authorization bearer correctly preferred config.token (a static
Vercel auth token from CLI / Actions runner) and fell back to
getVercelOidcToken() inside a Vercel function. But the trusted-sources
bypass header (x-vercel-trusted-oidc-idp-token) was being read directly
from process.env.VERCEL_OIDC_TOKEN inside getHeaders(). That env var is
the bake-time token, frozen at deployment-creation time — on a project
that has been redeployed after a settings change, it carries stale
claims (e.g. an iss from when the project was briefly in 'global' mode)
that no longer match the workflow-server's trusted-sources rule.

Move trusted-sources header attachment from getHeaders() (sync) to
getHttpConfig() (async) and source it from getVercelOidcToken(). That
function reads getContext().headers['x-vercel-oidc-token'] first — a
freshly minted per-request token that always reflects current project
settings — and only falls back to the env var when that header is
missing.

Bearer auth source remains config.token-first.

Also expand the diagnostic to log claims from BOTH the per-request OIDC
token AND the bake-time env var so the divergence is visible in logs
when debugging future trusted-source mismatches.

Removes the now-misleading getProtectionBypassHeader() helper (its
'read env var directly' semantics were exactly the bug).

* world-vercel: skip OIDC trusted-sources header on proxied path

The two outbound flows have different auth requirements:

  1. Proxied (usingProxy=true) — calls api.vercel.com/v1/workflow.
     Public endpoint, authenticated with a static Vercel auth token via
     config.token. The api-workflow proxy mints its own OIDC token
     before forwarding to workflow-server, so the trusted-sources
     bypass header on the SDK→proxy hop is meaningless. CLI, GitHub
     Actions, and other API-client callers take this path.

  2. Direct (usingProxy=false) — runs inside a Vercel deployment
     talking straight to workflow-server. workflow-server validates a
     Vercel OIDC bearer; Vercel's edge validates the trusted-sources
     header. Both must come from getVercelOidcToken() (the per-request
     fresh token), not process.env.VERCEL_OIDC_TOKEN (the bake-time
     token that can be stale after a project config change).

Previously getHttpConfig attached x-vercel-trusted-oidc-idp-token on
both paths whenever getVercelOidcToken() resolved. That accidentally
forwarded the GitHub Actions OIDC token (when wired into
VERCEL_OIDC_TOKEN by the test runner) onto every SDK→proxy request,
which is harmless but wrong-by-design — the proxy is public, doesn't
look at that header on its inbound side, and the GHA token isn't its
intended audience.

Bearer auth source rules:
  - Proxied: only config.token. (No fallback to OIDC; that auth
    pathway doesn't go through the proxy's auth checks.)
  - Direct: config.token (for tests / local dev), falling back to
    getVercelOidcToken() (for Vercel-runtime calls).

* world-vercel: throw if proxied path is hit without a Vercel auth token

The api-workflow proxy authenticates the caller with a regular Vercel
auth token (not OIDC), so reaching the proxied path with no
config.token is always wrong: the proxy will reject the request and
the SDK caller would see an opaque 401 with no actionable hint.

Throw at config-resolution time with a clear message that points to
the WORKFLOW_VERCEL_AUTH_TOKEN env var the SDK reads from. Adds tests
covering both the no-token-throws case and the with-token-attaches-
bearer-and-skips-trusted-sources case.

* test(e2e): include x-vercel-id in startWorkflowViaHttp error message

When the trusted-sources bypass returns 401, the error message now
surfaces the response's x-vercel-id header so we can identify which
edge node served the failure. Helps distinguish proxy-rollout
incompleteness from actual config errors during incremental
rollouts of edge-side changes.

* ci: mint GHA OIDC tokens on demand to survive 5-minute expiry

GitHub Actions OIDC tokens have a hard 5-minute lifetime that cannot be
extended (no API to ask for a longer TTL — exp is always iat + ~300s).
Pre-minting once at the start of the job and shipping the result down
to the test runner via env var means tests that run late in the suite
hit an expired token and 401 on /api/trigger-pages (and any other
trusted-sources protected endpoint).

Move minting into scripts/trusted-sources-headers.mjs:
  - getTrustedSourcesHeaders() is now async.
  - It calls the runner's ACTIONS_ID_TOKEN_REQUEST_URL endpoint directly
    (the env vars GHA exposes when permissions: id-token: write is on)
    and re-mints 60s before the cached token's exp.
  - Falls back to process.env.VERCEL_OIDC_TOKEN for non-GHA contexts
    (Vercel runtime, local dev).

Workflow files drop the now-redundant 'Mint OIDC token' step and the
VERCEL_OIDC_TOKEN env-var passthrough on the test step. The runner env
vars propagate to subsequent steps automatically.

Updates all 17 callers in e2e.test.ts / bench.bench.ts / utils.ts /
docs/scripts/check-docs-smoke.mjs to await the now-async call.

* address PR #1882 code review

- Drop `statuses: read` from the three workflow permission blocks (the
  wait-for-vercel-project action works without it on a public repo).
- Revert the `x-vercel-id` debug logging in `startWorkflowViaHttp`.
- Delete `packages/world-vercel/src/jwt-claims.ts` (debug-only helper).
- Drop the JWT claims diagnostic logging from `getHttpConfig`.
- Tighten the auth-flow comment in `getHttpConfig` and remove the
  historical 'no longer attaches' note from `getHeaders`/its test.
- Restore `.changeset/world-vercel-protection-bypass.md` (already
  shipped in a beta release per .changeset/pre.json).
- Trim the `.changeset/world-vercel-trusted-sources.md` description to
  one short paragraph.

* docs(AGENTS): document local VERCEL_OIDC_TOKEN via vercel env pull

Configured trustedSources.projects on all 11 workbench app projects so
each one accepts a Vercel-issued OIDC token from any of the others. A
developer running e2e locally can now do `vercel env pull` from any
workbench app's directory and use the resulting VERCEL_OIDC_TOKEN to
bypass Deployment Protection on any of the workbench preview/prod
deployments — no need to disable protection on the project just to run
the suite locally.
2026-05-02 19:21:52 +09:00
Pranay Prakash 072e86bbef Add additional tests for event consumer fixes for hook/sleep/step race conditions (#1528)
Co-authored-by: Peter Wielander <mittgfu@gmail.com>
2026-05-02 07:47:58 +00:00
Nathan Rajlich 74b13cd3ed Add first-class serialization for FatalError and RetryableError (#1513)
* Add WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE to FatalError and RetryableError

Add custom serialization methods to FatalError and RetryableError in
@workflow/errors, enabling the SWC plugin to discover and register them
through the standard class serialization pipeline. This preserves class
identity (instanceof), the fatal flag, and the retryAfter date when
these errors cross serialization boundaries.

- Add @workflow/serde dependency to @workflow/errors
- Add WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE static methods to both classes
- Add unit tests verifying Instance-based round-trip serialization
- Add e2e workflow tests verifying class identity preservation end-to-end

* Address review feedback on FatalError/RetryableError serde

- FatalError: preserve cause property when present (Copilot feedback)
- RetryableError: preserve cause property when present
- RetryableError: serialize retryAfter as numeric timestamp for realm
  safety (the Date reducer uses instanceof global.Date which fails
  across VM realms; timestamps sidestep that issue)
- Replace e2e tests with step return value serialization (step throw
  path always reconstructs errors as FatalError, so those tests don't
  exercise the new serde code path)
- Add unit tests for cause preservation on both classes

* Bump fatal-retryable-error-serialization changeset to minor

Adding WORKFLOW_SERIALIZE / WORKFLOW_DESERIALIZE hooks to FatalError
and RetryableError is a feature, not a bug fix.

* Switch FatalError/RetryableError to first-class serialization

Replace the WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE static methods on
FatalError and RetryableError with dedicated reducers/revivers in the
common serialization module. The Instance/Class pipeline relies on the
SWC plugin discovering classes and registering them by classId, which
means values constructed in environments that don't run the plugin
(vitest e2e runner, ad-hoc Node scripts) can't be deserialized.
Treating FatalError/RetryableError as first-class serialization targets
makes them round-trip from any environment with no setup, matching the
behavior of TypeError, RangeError, etc. added in the previous commit.

- Drop @workflow/serde dependency on @workflow/errors
- Remove WORKFLOW_SERIALIZE/DESERIALIZE statics from FatalError/RetryableError
- Add FatalError/RetryableError reducers to serialization/reducers/common.ts
  with cached base-reducer factories for the subclasses that wrap the
  shared shape (RetryableError, AggregateError)
- Migrate unit tests off registerSerializationClass setup
- Extend the errorSubclassRoundTripWorkflow e2e test to cover FatalError
  and RetryableError, and drop the parallel errorFatalSerdeRoundTrip /
  errorRetryableSerdeRoundTrip tests

* Address review feedback on FatalError/RetryableError serde

- Soundness: split makeErrorSubclassReducer into a shared base helper
  (reduceErrorBase / reduceNamedErrorSubclassBase returning the
  BaseErrorPayload shape) plus a thin wrapper constrained to subclass
  keys whose serialized shape is exactly that base payload. The
  AggregateError and RetryableError reducers — which extend the base
  with extra fields — now consume reduceNamedErrorSubclassBase
  directly instead of calling makeErrorSubclassReducer with an
  unsound type cast. The compiler now rejects accidental misuse
  (SimpleErrorSubclassKey type guard).
- Realm safety: RetryableError reviver constructs retryAfter via
  new global.Date(...) to match the rest of the module and ensure
  the resulting Date passes instanceof global.Date checks in the
  target realm.
- Test strength: assert serialized payloads contain the literal
  devalue marker ["FatalError",N] / ["RetryableError",N] rather
  than the bare class name (which would also match a generic Error
  payload whose name happens to be "FatalError"). Also assert the
  generic ["Error",N] marker is absent.

* Match Error subclass reducers by `value.name`, not constructor name

Bundlers like Turbopack compile `export class FatalError extends Error
{...}` into a registration call like `e.s(["FatalError", 0, class
extends Error {...}])` — passing an anonymous class expression as a
function argument. The resulting constructor function has `name === ''`,
which broke the previous `value.constructor?.name === subclassName`
match: an instance of the bundled FatalError class no longer matched the
dedicated FatalError reducer and instead fell through to the generic
`Error` reducer, losing class identity across the workflow boundary.

This was caught by the local-prod CI matrix, where each Next.js route
gets its own bundled chunk: a real `new FatalError('fatal!')` returned
from a workflow was serialized as a plain Error and revived without
`instanceof FatalError` holding on the consumer side.

Switch the match in `reduceNamedErrorSubclassBase` to `value.name`,
which:
- works for built-in subclasses (TypeError/RangeError/… all set
  `name` automatically and aren't bundled, so behavior is unchanged
  in practice).
- works for FatalError/RetryableError, whose constructors set
  `this.name` explicitly — robust across realms AND bundlers.
- is consistent with how `FatalError.is()` / `RetryableError.is()`
  already identify their values.

Two existing cross-VM Error tests (added in #1164) used `name = 'FatalError'`
on a plain Error to stand in for any cross-realm error — which now hits
the dedicated FatalError reducer (returning a host-realm FatalError)
instead of the generic Error reducer (which constructs a VM-realm Error).
Renamed the stand-in to `'CustomError'` so they continue to exercise the
intended path.
2026-05-01 20:36:08 +00:00
Nathan Rajlich e7ea0684f4 Add first-class serialization for built-in Error subclasses (#1511)
* Add first-class serialization for built-in Error subclasses

Replace the generic Error reducer/reviver with specific handlers for
TypeError, RangeError, SyntaxError, URIError, ReferenceError, EvalError,
and AggregateError. Preserve the cause property on all Error types,
only serializing it when present to maintain absence vs presence
semantics.

* Refactor Error subclass reducers/revivers with helper functions

Extract repetitive reducer/reviver logic into makeErrorSubclassReducer
and makeErrorSubclassReviver helpers. Reduces ~120 lines of duplicated
code to ~50 while preserving identical behavior. AggregateError remains
as a wrapping extension since it needs to preserve the errors array.

* Bump error-subclass-serialization changeset to minor

Adding new serialization support for built-in Error subclasses is a
feature, not a bug fix.
2026-05-01 18:39:01 +00:00
Nathan Rajlich 78301695ea fix(builders): avoid false-positive node-module errors for step-only usage in shared modules (#1821)
The workflow-node-module-error plugin records violations in onResolve, which
runs before esbuild's tree-shaker. A shared module with both a workflow-safe
export and a step-only export that imports node:* would be flagged even when
the workflow never reached the step-only export.

Mark node:/bun: imports as sideEffects: false so esbuild can tree-shake them
when unused in surviving code, enable metafile, and filter recorded violations
in onEnd against outputs[].imports so only builtins that actually survive in
the emitted bundle are reported.

Fixes #1817
2026-05-01 17:57:56 +00:00
Nathan Rajlich 9f3516ec28 Refactor: Extract serialization into modular architecture and wire into existing pipeline (#1299)
* Add serialization module foundation: types, codec interface, format prefix

Start of the serialization refactor (separate from snapshot-runtime).

New files:
- serialization/types.ts — SerializationFormat enum, SerializableSpecial
  interface, Reducers/Revivers types
- serialization/codec.ts — Codec interface with formatPrefix, serialize,
  deserialize, and optional deserializeLegacy
- serialization/format.ts — Format prefix encode/decode/peek, moved from
  the monolithic serialization.ts

The Codec interface enables future alternative formats (CBOR, JSON) while
keeping the devalue implementation as the current default.

* Add reducers, devalue codec, encryption, and mode-specific modules

Serialization refactor Phase 1: create the new module structure alongside
the existing monolithic serialization.ts (which continues to work).

New files:
- serialization/reducers/common.ts — Date, Error, Map, Set, URL, BigInt,
  typed arrays, Headers, Request, Response, RegExp, URLSearchParams
- serialization/reducers/class.ts — Class/Instance with WORKFLOW_SERIALIZE/
  DESERIALIZE support
- serialization/reducers/step-function.ts — StepFunction with closure vars
- serialization/codec-devalue.ts — devalue Codec implementation
- serialization/encryption.ts — composable encrypt/decrypt layer
- serialization/workflow.ts — synchronous, no encryption, for VM use
- serialization/step.ts — async with encryption, for step handler
- serialization/client.ts — async with encryption, for start() API
- serialization/index.ts — re-exports all public API
- serialization/serialization.test.ts — 25 focused tests

All modes compose their reducer/reviver sets from the shared building blocks.
Cross-mode compatibility verified: data serialized in any mode can be
deserialized in any other mode (for common types).

Existing 108 serialization tests continue to pass unchanged.

* Add sub-path exports for workflow serialization module

- Add ./serialization/workflow export to @workflow/core package.json
- Add ./internal/serialization re-export to workflow meta-package
- The workflow bundle can now import serialize/deserialize via:
  import { serialize, deserialize } from 'workflow/internal/serialization'

Full test suite passes: 493 tests across 22 files (including 25 new
serialization module tests).

* Address code review feedback

1. Fix reducer composition order: Class/Instance reducers now come BEFORE
   common reducers in all three modes (workflow, step, client). This ensures
   custom Error subclasses with WORKFLOW_SERIALIZE are handled by the
   Instance reducer before the generic Error reducer (devalue uses
   first-match-wins semantics).

2. Fix encryption decrypt() to fail fast when encrypted data is encountered
   without a decryption key, instead of silently returning encrypted bytes
   that would fail later with an unhelpful format error.

3. Remove Request/Response from common reducers — they don't have matching
   common revivers, so including them caused asymmetric behavior (serialize
   as Request, deserialize as plain object). Request/Response handling
   belongs in mode-specific modules that can provide proper revivers.

4. Document Node.js dependency in the workflow serialization re-export.
   The current implementation uses node:util and Buffer. For the QuickJS
   VM (snapshot runtime), these will need polyfills — tracked separately.

* Move reducer/reviver composition into the devalue codec

The Codec interface now takes a SerializationMode ('workflow', 'step',
'client') instead of raw reducers/revivers. The reducer/reviver
composition is internal to the devalue codec implementation.

This is the right abstraction because reducers/revivers are devalue-
specific concepts. A future CBOR codec would handle Date, typed arrays,
Map, Set natively via the CBOR type system — it wouldn't use reducers
at all. A JSON codec would only support standard JSON types.

The mode-specific modules (workflow.ts, step.ts, client.ts) are now
simpler — they just pass the mode string to the codec.

* Replace SerializationFormatType enum with open-ended FormatPrefix type

The format prefix is now a branded string type validated by
isFormatPrefix() — any 4-character [a-z0-9] string is valid.
This removes the hard-coded enum of known formats, making the system
truly open for extension:

  type FormatPrefix = string & { __brand: 'FormatPrefix' };
  function isFormatPrefix(value: string): value is FormatPrefix;

The SerializationFormat object still provides well-known constants
('devl', 'encr') but they're now just typed constants, not an
exhaustive enum.

peekFormatPrefix() and decodeFormatPrefix() use isFormatPrefix() for
validation instead of checking against a known list. Unknown but valid
prefixes (e.g. 'cbor', 'json', 'v2b1') are accepted — the caller
decides whether they can handle the format.

6 new isFormatPrefix tests covering: valid strings, too short, too long,
uppercase, special characters. 1 new test for unknown-but-valid prefixes.

* Wire modular serialization modules into serialization.ts, add 138 unit tests

Replace duplicate format prefix, reducer/reviver, and encryption helper
code in the monolithic serialization.ts with imports from the modular
serialization/ directory. This completes the refactoring started in the
earlier additive-only commits.

Key changes:
- serialization.ts now imports types, format prefix, common/class/step-function
  reducers and revivers, and encryption helpers from ./serialization/ modules
- Removed ~450 lines of duplicate code from serialization.ts
- Made encryption error messages consistent between old and new modules
- Added 138 comprehensive unit tests covering types, format prefix,
  encryption, codec, all three reducer modules, all three mode modules,
  cross-mode compatibility, and edge cases
- Updated one existing test assertion for new error message wording

* Address code review feedback

- encryption.ts: throw WorkflowRuntimeError instead of plain Error in
  decrypt() to preserve the error contract from legacy maybeDecrypt()
- format.ts: document that open-ended prefix validation ([a-z0-9]{4})
  is intentional for forward compatibility — callers check support
- errors.ts: extract duplicated formatSerializationError into shared
  utility, remove 4 copies from workflow.ts, step.ts, client.ts
- codec-devalue.ts: document that globalThis default is a known
  limitation; legacy dehydrate/hydrate path still supports custom global

* Fix codec-devalue.ts comment: clarify modular modules are not used in current runtime

The globalThis default is not a limitation for the current runtime —
all serialization goes through dehydrate*/hydrate* in serialization.ts
which passes the correct global. The modular modules are infrastructure
for the future snapshot runtime where serialization runs inside the VM.

* Wire dehydrate/hydrate functions through modular serialize/deserialize

The dehydrate*/hydrate* functions in serialization.ts now delegate to
the modular mode modules (workflowModule, stepModule, clientModule)
instead of directly calling devalue stringify/parse/unflatten.

Key changes:
- Extended Codec interface with CodecOptions (global, extraReducers,
  extraRevivers) so the codec can receive VM globals and mode-specific
  stream/Request/Response handlers
- devalueCodec threads global through to all reducer/reviver factories
  so instanceof checks work across VM boundaries
- Mode modules (workflow.ts, step.ts, client.ts) accept CodecOptions
  and pass them through to the codec
- dehydrate*/hydrate* functions now call module serialize/deserialize
  with stream and Request/Response reducers/revivers passed as extras
- v1Compat path remains inline (pre-codec, uses stringify + revive)
- Error context strings preserved via try/catch re-wrapping

* Bump changeset from patch to minor for serialization refactor

Return types of public get*Reducers/get*Revivers functions narrowed
from Reducers/Revivers to Partial<Reducers>/Partial<Revivers>, which
is a TypeScript-level breaking change. Also adds new sub-path exports
(@workflow/core/serialization/workflow, workflow/internal/serialization)
which is additive. Minor bump is the appropriate semver for both.

* Remove unused workflow/internal/serialization re-export and @workflow/core/serialization/workflow sub-path

Both exports had zero consumers in the repo. The workflow/internal/serialization
export was previously removed on main in #1082 for the same reason. The
modular workflow.serialize/deserialize is still reachable via
@workflow/core/serialization when needed. These exports can be reintroduced
by the snapshot runtime branch if/when it actually needs them.

Also updates the changeset to drop the 'new sub-path exports' bullet.

* Downgrade changeset from minor to patch

After auditing actual consumers of the narrowed return types
(getExternalReducers/getWorkflowReducers/getExternalRevivers/getWorkflowRevivers
now return Partial<Reducers>/Partial<Revivers>), no in-repo or external
consumer indexes specific keys on the returned object in a way that would
break. The only internal caller that did (runtime/run.ts) was updated in
this same PR. The narrowing is type-safer but effectively invisible at
runtime and for idiomatic callers that spread or forward the object.
Since the refactor is internally restructuring only, patch is the
appropriate semver bump.

* Trim serialization-refactor changeset

* Dedup formatSerializationError: import from serialization/errors.ts

The legacy serialization.ts had its own inlined copy of
formatSerializationError. Now that the helper is exported from
serialization/errors.ts (already consumed by workflow.ts/step.ts/client.ts),
import it here too to keep the single source of truth.
2026-05-01 09:54:08 +00:00
Nathan Rajlich e0ec429bb3 fix(builders): bundle aliased project-local helpers instead of externalizing (#1885)
Externalizing aliased helpers was unsafe: their source on disk could
contain further alias imports that Node's ESM loader doesn't know about,
causing 'Package subpath ... is not defined by "exports"' /
ERR_MODULE_NOT_FOUND at runtime.

Now bundle inline so all alias resolution happens at build time.
2026-05-01 02:20:31 -07:00
github-actions[bot] eb8562ddda Version Packages (beta) (#1884)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-04-30 14:25:43 -07:00
Mitul Shah aeb495bccc [trace viewer] Polish items (#1872)
* wip

* context

* cleanup

* wip

* wip

* Update trace-viewer.tsx

* adjust hovers

* time marker

* tweak

* Update event-list.tsx

* header

* Update utils.ts

* search

* Update timeline.tsx

* ship

* Update timeline.tsx

* Update event-list.tsx

* Create icons.tsx

* Update trace-viewer.tsx

* Update utils.ts

* new trace viewrr

* Update workflow-trace-view.tsx

* Fix: Runtime crash when `trace` is `undefined`: `NewTraceViewerComponent` receives `undefined` (cast as `Trace`) and immediately accesses `trace.spans`, causing "Cannot read properties of undefined".


This commit fixes the issue reported at packages/web-shared/src/components/trace-viewer-new.tsx:28

**Bug Analysis:**

In `trace-viewer-new.tsx`, the `buildTrace` function returns `undefined` when `!run?.runId`. The result is stored in `trace` which has type `TraceWithMeta | undefined`. However, on line 28, `trace` is passed directly to `NewTraceViewerComponent` with a type assertion `trace as Trace`, which silences the TypeScript error but does not prevent the runtime crash.

Inside `NewTraceViewerComponent` (in `new-trace-viewer/trace-viewer.tsx` line 98), the component immediately accesses `trace.spans`:

```tsx
<ActiveSpanProvider spans={trace.spans}>
```

When `trace` is `undefined`, this produces: `TypeError: Cannot read properties of undefined (reading 'spans')`.

This happens whenever the component renders before `run.runId` is available — a normal scenario during initial loading.

The old `WorkflowTraceViewer` component (in `workflow-trace-view.tsx` line 953) correctly handles this with a `if (!trace)` guard that renders a loading skeleton. The new component lacks this guard.

**Fix:**

Added a null guard in `trace-viewer-new.tsx` that checks `if (!trace)` before rendering `NewTraceViewerComponent`. When trace is undefined, a simple loading placeholder is rendered instead. This prevents the crash and follows the same pattern as the existing `WorkflowTraceViewer`. The `as Trace` cast on line 28 is now safe because `trace` is guaranteed to be defined after the guard.


Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Co-authored-by: mitul-s <mitulxshah@gmail.com>

* fixed edge view

* Update timeline.tsx

* ship it

* cleanup

* Update trace-viewer.tsx

* wip

* wip

* Add detail pane to the new trace viewer + cleanup  (#1714)

* detail pane

* cleanup

* cleanup

* cleanu

* fix

* Update workflow-trace-view.tsx

* cleanup

* Update entity-detail-panel.tsx

* move sidebar provider into export

* dead code

* cleaning up more

* remove decorative indenting & output loader

* fix bars

* Update inspector-theme.ts

* cleanups

* Update copyable-data-block.tsx

* cleanup

* chonky

* Update timeline.tsx

* bug fxi

* marker lines

* wip

* changes

* Update event-list.tsx

* rounded

* Update event-list.tsx

* middle truncate component

* cleanup

* height updatres

* colour fixes

---------

Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Co-authored-by: Peter Wielander <mittgfu@gmail.com>
2026-04-30 16:50:24 -04:00
github-actions[bot] 8f9ca384ec Version Packages (beta) (#1868) 2026-04-30 13:34:08 -07:00
Mitul Shah 03dfab35e7 Updated Workflow Trace viewer (#1454) 2026-04-30 13:46:54 -04:00
Nathan Rajlich 3ad8ee7e33 fix(world-local): prevent path traversal via request-supplied IDs (#1829)
* fix(world-local): prevent path traversal via request-supplied IDs

Request-supplied identifiers (runId, eventId, stepId, hookId, correlationId,
stream names, and tags) flowed directly into path.join() calls, allowing a
client to send values like '../../../package' and cause the backend to read
or write files outside the workflow data directory.

Add a centralized validator (assertSafeEntityId) that rejects IDs which are
empty, start with '.', or contain path separators or NUL bytes. Apply it at
each storage-layer entry point that composes IDs into filesystem paths:
fs.taggedPath / readJSONWithFallback / paginatedFileSystemQuery, the runs /
steps / events / hooks storage methods, and the streamer.

* address review feedback

- UnsafeEntityIdError now extends WorkflowWorldError for consistency with
  other storage-layer errors and the platform error-to-HTTP mapping.
- Add resolveWithinBase(basedir, ...segments) containment helper and
  apply it at every taggedPath / readJSONWithFallback / .locks path
  construction site in events-storage and legacy, so a forgotten
  assertSafeEntityId at a future call site can't silently regress.
- Truncate attacker-controlled values in the error message.
- Drop unused assertSafeEntityIds helper and the unreachable typeof
  check under the TS signature.
- Fix docstrings on assertSafeEntityId / taggedPath JSDoc example /
  filePrefix validation comment to match what the code actually does.
- handleLegacyEvent now re-asserts runId locally so the invariant is
  documented at the call site instead of implicitly inherited from
  events.create.

---------

Co-authored-by: JJ Kasper <jj@jjsweb.site>
2026-04-30 09:27:46 +00:00
Nathan Rajlich 906f7c1213 refactor(next): remove step file copy mechanism from deferred builder (#1796)
* refactor(next): remove step file copy mechanism from deferred builder

Step sources are now imported directly into the generated `step/route.js`
using the same `getImportPath`-based logic already used for serde files.
This is possible because the SWC plugin's client mode was merged into
step mode (#1686), so the workflow loader always runs in step mode and
transforms every file it sees, including those from packages.

Removed:
- `__workflow_step_files__/` per-file copies with hashed names, metadata
  comments, inline source maps, and bare-specifier rewriting via
  `enhanced-resolve`
- `createResponseBuiltinsStepFile`; builtins are now imported via
  `require.resolve('workflow/internal/builtins')` at build time
- `step-copy-utils.ts` and all copy-specific branches in `loader.ts`
- `enhanced-resolve` dependency

The deferred builder still removes the legacy `__workflow_step_files__/`
directory on boot so upgrades leave no stale artifacts behind.

Dev tests that inspected copied step file contents now inspect the
manifest.json entries (which list every discovered step keyed by source
path).

* refactor(next): scrub historical phrasing from code comments

* test: update next step stack expectations

---------

Co-authored-by: JJ Kasper <jj@jjsweb.site>
2026-04-29 21:23:08 +00:00
Karthik Kalyan 0ec805a999 fix(e2e): increase utf8StreamWorkflow test timeout to 120s (#1869)
Matches the convention used by readableStreamWorkflow and webhookWorkflow.
The 60s budget is too tight when a single Vercel Queue delivery retry can
take 30-60s, causing intermittent flakiness on the nitro workbench.

Made-with: Cursor
2026-04-29 12:27:57 -07:00
Karthik Kalyan 9ea125427f Decode UTF-8 stream chunks (#1852)
* Decode typed array stream chunks

* Render decoded stream bytes with raw view

* Render decoded bytes in data inspector

* Use generic byte inspector for streams

* review feedback: narrow stream-display exports, fix tab a11y, add collapseRefs tests

- Remove unused formatStreamChunkForDisplay/sanitizeStreamChunkForDisplay
  exports; keep only the formatArrayBufferViewForDisplay path actually used
  by DataInspector.
- Replace broken role=tablist/role=tab on the Decoded/Bytes switcher
  with aria-pressed toggle-button semantics.
- Export collapseRefs/isBytesDisplay and add regression tests covering
  typed-array detection (top-level, nested in object/array/Map/Set,
  DataView exclusion).

* Replace eval with JSON.parse in serialization revive helper (#1848)

* Replace eval with JSON.parse in serialization revive helper

devalue.stringify() always produces valid JSON — special values
(undefined, NaN, Infinity, -0) are encoded as negative integer
sentinels. JSON.parse yields the same flattened array form that
unflatten() expects, without the eval anti-pattern (VULN-918).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Drop redundant workflow package from changeset

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>

* Add e2e test for UTF-8 parseable stream chunks

Emits Uint8Array chunks containing multi-byte UTF-8 (Latin Extended,
CJK, emoji, RTL Arabic) plus a UTF-8 encoded JSON document, and
asserts each chunk round-trips through TextDecoder({ fatal: true }).
Exercises the same decode path the web inspector relies on for
typed-array stream values.

Made-with: Cursor

---------

Co-authored-by: Pranay Prakash <pranay.gp@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 09:22:05 -07:00
Nathan Rajlich cbecbaa5fe feat(nitro): forward externals.external to workflow builder (#1844)
Nitro users can declare packages as externals via
`externals: { external: ['fsevents'] }` in `nitro.config.ts`. Previously
the workflow SDK's step/workflow/webhook bundles were built in isolation
and ignored that config, forcing users to either patch the Nitro adapter
or ship a custom builder (as in the 250MB bundle-size workaround gist).

Forward string entries from `nitro.options.externals.external` to the
builder's esbuild `external` option. Only string entries are forwarded
— RegExp and function matchers aren't supported by esbuild's
`external` API, so those are skipped.
2026-04-28 23:54:18 +00:00
Pranay Prakash 7d07fab692 Replace eval with JSON.parse in serialization revive helper (#1848)
* Replace eval with JSON.parse in serialization revive helper

devalue.stringify() always produces valid JSON — special values
(undefined, NaN, Infinity, -0) are encoded as negative integer
sentinels. JSON.parse yields the same flattened array form that
unflatten() expects, without the eval anti-pattern (VULN-918).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Drop redundant workflow package from changeset

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 22:58:24 +00:00
JJ Kasper baba580794 Write Next diagnostics to dist dir (#1857)
* Write Next diagnostics to dist dir

* Simplify Next dist dir config access
2026-04-28 22:02:03 +00:00
JJ Kasper a38f140ce3 Make TypeScript peer dependency optional (#1830)
* Make TypeScript peer dependency optional

* Handle missing TypeScript more gracefully

* Fix TypeScript plugin build after optional peer change
2026-04-28 21:48:13 +00:00
Nathan Rajlich d6f7eb620b 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.
2026-04-28 19:26:09 +00:00
Nathan Rajlich 354840e93b feat(world-vercel): support new env vars for Vercel Deployment Protection (#1824)
* feat(world-vercel): support WORKFLOW_VERCEL_PROTECTION_BYPASS env var

Allows sending a Vercel Deployment Protection bypass secret via the
`x-vercel-protection-bypass` header on all outbound requests made by
the Vercel world, enabling use against protected deployments (e.g.
previews, or workflow-server once protection is enabled).

* feat(world-vercel): support VERCEL_WORKFLOW_SERVER_URL env var

Replace hard-coded WORKFLOW_SERVER_URL_OVERRIDE constant with a function
that reads from the VERCEL_WORKFLOW_SERVER_URL env var. Allows configuring
the workflow-server URL per-deployment (e.g. workbench Preview envs
pointing to a branch deployment) without editing source.

* fix(world-vercel): preserve inline WORKFLOW_SERVER_URL_OVERRIDE const

Keep the inline const as an empty-string literal so external CI rewrite
tooling continues to work unmodified; the env var is a fallback when the
inline value is empty.

* refactor(world-vercel): rename to VERCEL_WORKFLOW_SERVER_PROTECTION_BYPASS

Align env var naming with VERCEL_WORKFLOW_SERVER_URL.

* ci: expose workflow-server protection bypass env vars to e2e-vercel-prod

Set VERCEL_WORKFLOW_SERVER_URL and VERCEL_WORKFLOW_SERVER_PROTECTION_BYPASS
on PR runs so e2e tests hit the protected workflow-server preview; leave
unset on main so production runs use the public default URL.

* refactor(world-vercel): address PR review comments

- Consolidate bypass header logic in getHeaders() to reuse
  getProtectionBypassHeader() instead of duplicating env lookup.
- Use consistent 'Authorization' casing in direct fetch() calls.
- Add unit tests for getProtectionBypassHeader, getHttpUrl, and getHeaders
  covering env var toggling and proxy/override combinations.
2026-04-21 17:43:30 -07:00
Pranay Prakash b27921985c Add distributed abort controller guide and implementation (#1811)
* feat: add distributed abort controller guide and implementation

Co-authored-by: Pranay Prakash <1797812+pranaygp@users.noreply.github.com>

* feat: implement abort signal step function and refactor workflows to use it

Co-authored-by: Pranay Prakash <1797812+pranaygp@users.noreply.github.com>

* feat: enhance distributed abort controller with user-provided ID

Co-authored-by: Pranay Prakash <1797812+pranaygp@users.noreply.github.com>

* feat: enhance distributed abort controller with TTL and reconnection logic

Co-authored-by: Pranay Prakash <1797812+pranaygp@users.noreply.github.com>

* feat: add grace period to abort controller workflow

Co-authored-by: Pranay Prakash <1797812+pranaygp@users.noreply.github.com>

* feat: add distributed abort controller workflow and tests

Co-authored-by: Pranay Prakash <1797812+pranaygp@users.noreply.github.com>

* add the pattern to the sidebar

* fix: correct start() call signature and run.id → run.runId

start() takes args as a positional second argument, not an options
object property. The Run object exposes runId, not id.

Made-with: Cursor

* fix: address PR review comments on distributed abort controller

- Make abort() idempotent by catching hook-not-found/expired errors
- Add error handling to signal getter's stream reader IIFE
- Fix tests: use instance methods instead of non-existent static
  methods, pass required ttlMs/graceMs args, include expired field
  in assertions

Made-with: Cursor

* fix: add missing import to Custom TTL docs code sample

The docs typecheck runs each code block in isolation. The Custom TTL
example was missing the DistributedAbortController import.

Made-with: Cursor

* fix: only sleep grace period on TTL expiration, not manual abort

Manual aborts now complete immediately instead of sleeping through
the full TTL + grace period. This fixes vitest timeouts where all
3 distributed-abort-controller tests exceeded the 30s limit.

Made-with: Cursor

* fix: wait for hook registration before aborting in test

The DistributedAbortController instance test was timing out because
abort() was called before the workflow had registered the hook. The
idempotent catch silently swallowed the "not found" error, leaving the
workflow running forever.

- Make runId readonly (was private) so tests can access it
- Add waitForHook(getRun(controller.runId)) before controller.abort()

Made-with: Cursor

* fix: apply conditional grace period to E2E workflow copy

The distributedAbortControllerWorkflow in 99_e2e.ts had the same
unconditional grace sleep bug, causing the reconnect test to timeout
at 60s while waiting ~66s for the grace period after manual abort.

Made-with: Cursor

---------

Co-authored-by: v0 <v0[bot]@users.noreply.github.com>
Co-authored-by: Pranay Prakash <1797812+pranaygp@users.noreply.github.com>
Co-authored-by: Karthik Kalyanaraman <karthik.kalyanaraman@vercel.com>
2026-04-20 13:27:48 -07:00
Pranay Prakash e295bae417 feat: allow start() to be called directly inside workflow functions (#1491)
* feat: allow start() to be called directly inside workflow functions

Add 'use step' to start() so it can be called directly from workflow
code. The SWC compiler strips the function body in workflow mode and
replaces it with a step proxy. When called from a workflow:

1. The workflow function reference is serialized via WorkflowFunction
   reducer (serializes { workflowId })
2. start() executes in the step context with full Node.js access
3. The returned Run is serialized via WORKFLOW_SERIALIZE and deserialized
   back in the workflow VM
4. Run getters (.status, .returnValue, etc.) are 'use step' getters
   that each execute as separate steps

Also re-exports start from @workflow/core/runtime/start in api-workflow.ts
instead of using a throwing stub, adds e2e tests for startFromWorkflow
(with hook communication) and fibonacciWorkflow (recursive composition).

* fix(next): don't copy package step files in deferred builder to avoid duplicate classes

Files belonging to packages (detected by walking up to find a
package.json with a name field) are imported via relative path
instead of being copied to __workflow_step_files__/. Copying creates
a second module instance which breaks JS native private field (#)
brand checks when the runtime creates instances from one copy and
the step handler accesses fields from the other.

* fix(next): only skip copying package files that are serde classes, not all package step files

Regular package step files (like fetch) must still be copied to ensure
the SWC loader registers them. Only serde class files from packages are
excluded from copying since those define classes with JS native private
fields (#) that break when duplicated.

* fix(next): generate thin wrappers for package serde step files instead of full copies

For package files that define serde classes (like Run), generate a thin
wrapper that imports the original class and registers steps/classes from
the manifest. This avoids duplicating the class definition (which breaks
JS native private field brand checks) while still registering all step
functions and the class in the serialization registry.

Regular package step files (like fetch) are still copied as before.

* fix(next): use forceStepModeFiles to transform package serde files in step mode

Instead of copying package serde+step files (which creates duplicate
classes with #private brand check issues) or generating fragile wrappers,
add the original file paths to a shared forceStepModeFiles set. The
loader checks this set and transforms those files in step mode directly,
so the SWC plugin generates proper step registrations on the original
class — no duplication, no reimplemented registration logic.

* fix(next): use step mode for all files with step/serde patterns, not just copies

The loader now selects step mode for any file that has 'use step'
directives or serde patterns, regardless of whether it's a deferred
step copy. Step mode is a superset of client mode — the only addition
is step registry IIFEs, which are harmless for non-step consumers.

This means package serde+step files (like Run) no longer need to be
copied to get step registrations. They're imported directly in the
step route and the loader transforms the original file in step mode.
One class instance, no duplication, no wrapper generation.

---------

Co-authored-by: Nathan Rajlich <n@n8.io>
2026-04-17 07:40:25 -07:00
Nishant Singh 3f6d98f3f0 [ai] Align toolsToModelTools with core AI SDK's prepareToolsAndToolChoice (#1544) 2026-04-17 07:38:29 -07:00
Nathan Rajlich 417c4930be refactor(swc-plugin): remove client transform mode, merge into step mode (#1686)
* 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.
2026-04-16 22:50:46 +00:00
workflow-devkit-release-bot[bot] 5714c2335a Version Packages (beta) (#1711) 2026-04-16 15:34:32 -07:00
Nathan Rajlich b7d6595c25 fix false-positive unconsumed event in hook loop replay (#1778) 2026-04-16 15:05:27 -07:00
Nathan Rajlich 136bd35a98 fix(swc-plugin): preserve step function names in stack traces after bundler minification (#1743)
* 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>
2026-04-16 20:20:26 +00:00
Peter Wielander 340c085681 [world-vercel] Use stream close control frame to decide whether to reconnect to stream (#1742) 2026-04-16 09:52:44 -07:00
Karthik Kalyan bcf818c2c7 disable minification for web (#1768) 2026-04-16 05:53:29 -07:00
Tom Dale 5a4296412f fix(packaging): embed source content in sourcemaps (#1769)
Enable TypeScript inlineSources in the shared package tsconfig so published @workflow packages ship self-contained JavaScript sourcemaps. This fixes Vite and Vitest warnings in downstream repos when sourcemaps point at ../src/*.ts paths that are not published.
2026-04-16 00:21:03 -07:00
Peter Wielander 11cfb8f3fb [world-local] Cross-process streaming for local dev via polling, enable e2e tests (#1739) 2026-04-15 19:38:00 -07:00
Nathan Rajlich 173756dc4d [docs] Rename workflowdevkit to workflowsdk and useworkflow.dev to workflow-sdk.dev (#1759)
* [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)
2026-04-15 18:01:48 -07:00
Peter Wielander 0810b75872 [core] Re-try runtime replays that exceed deadline up to three times (#1740) 2026-04-15 14:26:27 -07:00
John Lindquist ffefbbce01 [docs] Polish migration guides for scanability (#1721)
Co-authored-by: Peter Wielander <mittgfu@gmail.com>
2026-04-15 13:04:22 -07:00