Commit Graph

90 Commits

Author SHA1 Message Date
Nathan Colosimo 62d570ed4b Remove retired v1 step route plumbing (#3061) 2026-07-24 23:50:55 +00:00
Nathan Rajlich c31e30caac cli: show world-specific run fields in inspect output via World.describeRun (#2896)
* cli: show run region in inspect output via World.regionForRunId

Adds an optional reverse-lookup hook to the World interface —
regionForRunId(runId): string | null — so tooling can display a run's
region generically. Worlds without a regional dimension simply omit
the hook and no region output appears.

- @workflow/world: new optional interface member (documented: must not
  throw; null = undeterminable)
- @workflow/world-vercel: implements it from the run-ID region tag
  (tagged -> embedded region, untagged legacy -> default region,
  malformed -> null)
- @workflow/cli: 'workflow inspect runs' gains a region column
  (between workflowName and status) and 'workflow inspect run <id>'
  a region property, in both table and JSON output — only when the
  world defines the hook

* Generalize the inspect hook: World.describeRun display fields

Replaces regionForRunId on the World interface with describeRun, per
review: worlds may want to expose more than a region, and the
information need not be encoded in the run ID — describeRun receives
the run entity itself (loosely typed, mirroring createRunId), so a
world can derive fields from executionContext or any other property.
Each returned key becomes an inspect column/property; null values are
preserved in structured output ('applicable but undeterminable' vs.
the hook being absent entirely).

- world-vercel: describeRun returns { region } decoded from the run
  ID tag (regionForRunId stays exported as a utility); entities
  without a usable runId contribute nothing
- CLI listing: columns come from the union of keys the world returns
  for the page, inserted before status; both analytics and storage
  paths; detached call site binds this
- CLI showRun: merges the world fields into detail/JSON output via a
  method-style call (preserves this), keeping nulls
- tests: field merging (multi-key), null preservation in JSON, hook
  absent, and world-vercel describeRun coverage incl. no-runId
  entities

* cli: evaluate describeRun defensively

Per review: the World interface says describeRun is pure and must not
throw, but it is an external extension point and the CLI should not
trust that. New safeWorldFields helper, used by both the listing and
showRun paths:

- a throwing implementation contributes no fields instead of crashing
  the inspect command
- keys that already exist on the run row are dropped, so a world can
  never overwrite canonical fields (status, runId, ...) in output

Tests: canonical fields survive a clobbering describeRun (extra keys
still merged); a throwing describeRun leaves rows untouched and the
command succeeds.

* Allow async describeRun implementations

Per review: widening a sync signature to async later would break every
consumer, while accepting sync-or-async from day one is free — sync
implementations (like world-vercel's) remain valid, and consumers
simply await, which handles both. The performance intent lives on as
documented guidance: the hook is called once per displayed run, so
implementations should stay cheap and avoid I/O; the CLI evaluates a
page's rows concurrently so an async world costs one await per page,
not per row. Promise rejections get the same treatment as throws:
no fields, never a crash.

* Update packages/world/src/interfaces.ts

Co-authored-by: Peter Wielander <mittgfu@gmail.com>
Signed-off-by: Nathan Rajlich <n@n8.io>

---------

Signed-off-by: Nathan Rajlich <n@n8.io>
Co-authored-by: Peter Wielander <mittgfu@gmail.com>
2026-07-13 15:08:19 -07:00
Nathan Colosimo 145835b647 Centralize workflow event semantics (#2790)
* Centralize workflow event semantics

* Simplify centralized event helper usage

* refactor: finish centralizing event semantics

* refactor(world): derive Hook from its schema

* fix(world): preserve event helper compatibility
2026-07-10 09:31:22 -07:00
Karthik Kalyan fe327e69e2 [world][web][cli] o11y: window-aware runs listing (#2812)
* web: infinite scroll for the runs table

Replace Previous/Next cursor paging with front-style infinite scroll:
a useInfiniteList hook accumulates cursor pages with per-run dedup and
generation-guarded resets, and useLoadMoreOnScroll drives loadMore from
an IntersectionObserver sentinel (400px prefetch margin, guarded against
double-fetch, observed against the table's scroll container). Footer now
shows the loaded count and the analytics lookback window.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* web: back the runs infinite list with SWR so tab switches serve from cache

Rewrite useInfiniteList on useSWRInfinite: pages are keyed by
[cacheKey, cursor] in SWR's global cache, so unmount/remount (switching
tabs) restores fetched pages instantly instead of refetching. Revalidation
is conservative because analytics list queries are expensive:
revalidateFirstPage and revalidateIfStale are off; freshness comes from
the Refresh button and the visibility-change auto-reload, which map to
reload() (reset to first page + revalidate).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* world: expose startTime/endTime on analytics runs listing

The workflow-server /v2/analytics/runs endpoint has accepted a bounded
startTime/endTime window since it shipped, and is significantly faster
with one (the window prunes the ClickHouse scan: ~2s for 12h vs ~8s for
the default 30-day entitlement window). The world client never exposed
the params, so the CLI and web UI could only issue windowless requests.
Pass them through so clients can send bounded windows (e.g. a period
picker like front's workflows o11y).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* web: front-style period picker for the runs list

Add a time-window picker (1h/6h/24h/3d/7d/30d, default 24h, URL-backed
via ?period=) that sends an explicit startTime/endTime window through
fetchRuns -> world.analytics.runs.list, keeping the ClickHouse scan
bounded. The window is frozen per selection/refresh so all cursor pages
share the same bounds, and it participates in the SWR cache key.

Plan tiers are honored data-driven from the server's pageInfo: presets
longer than the plan's observability lookback are disabled in the picker
(labeled Observability Plus when an upgrade is available), and a 402
observability-upgrade-required response renders through the existing
upgrade-required error handling. The footer now labels the selected
window instead of the plan lookback. The runtime (local) fallback path
ignores the window since the storage API has no time filter.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* web: allow status filtering without a workflow name filter

The status dropdown was disabled on Vercel backends until a workflow was
selected — a limitation of the runtime DynamoDB API's index design. The
runs list now reads via world.analytics, whose ClickHouse query filters
derived status independently of workflowName, so drop the guard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* cli: time-window flags for runs listing; widen name lookups past the default window

The analytics backend now defaults windowless runs listings to the
trailing 24h. Replicate the web's window support in the CLI:

- 'workflow inspect runs' gains --since/--until (relative durations like
  30m/12h/7d/2w, or timestamps) which are sent as an explicit
  startTime/endTime window. Out-of-plan windows surface through the
  existing observability-upgrade-required handling; non-analytics
  backends warn that the flags are ignored.
- 'workflow start <name>' resolves the workflow's latest run via a
  windowless (default-window) listing and now retries across the plan's
  whole observability window on a miss, so names idle for more than a
  day keep resolving.
- Bulk 'workflow cancel' matches across the plan window up front — a run
  can sleep or wait on a hook for days without recent events, so the
  default recent window must not bound cancellation matching.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: tighten changeset descriptions

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* web: persist frozen listing windows across remounts; minimize lockfile diff

Address review findings:

- The frozen startTime/endTime lived in component state, but RunsTable
  fully remounts on tab switches, so every remount minted a new SWR cache
  key — the cached-pages restore never hit and cache entries grew
  unboundedly (one per key, including every 5s local-backend poll tick).
  Move the frozen windows to a module-scope store keyed by period: a
  remount reuses the stored window (same cache key, instant restore), and
  the window only advances on explicit refresh/reload. Non-analytics
  backends now send no window at all (the runtime APIs ignore it anyway),
  which also hides the period picker and window label there.

- Regenerate pnpm-lock.yaml from main so the diff contains only the swr
  addition (plus its own use-sync-external-store dependency), dropping
  the unrelated docs-importer radix-ui re-resolutions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 18:12:17 -07:00
Nathan Colosimo 49a50e83d9 Document configuration environment variables (v5) (#2468) 2026-07-07 17:56:41 -07:00
Karthik Kalyan 54f46f976d cli: restore dynamic world loading for community backends (#2806)
* cli: restore dynamic world loading for community backends

Since #2752 moved world selection to static injection, setupCliWorld only
constructed the vercel, local, and postgres worlds explicitly and threw
'Unsupported workflow backend' for anything else — breaking community
worlds (e.g. @workflow-worlds/turso) that previously loaded through the
dynamic createWorld() in @workflow/core/runtime.

Generalize the postgres-only dynamic path: any backend other than
vercel/local is now resolved from the user's project directory and loaded
via its createWorld() export, matching @workflow/web's world construction
(#2804), with clear errors when the package is missing or does not export
createWorld().

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* cli: fall back to default.createWorld for CJS world packages

Review feedback on #2804: import() of a require-resolved CJS entry relies
on cjs-module-lexer to surface named exports; fall back to
mod.default.createWorld when detection fails.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Update .changeset/cli-generic-world-backends.md

Signed-off-by: Peter Wielander <mittgfu@gmail.com>

---------

Signed-off-by: Peter Wielander <mittgfu@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Peter Wielander <mittgfu@gmail.com>
2026-07-07 15:47:58 -07:00
Karthik Kalyan 17d4ce2253 cli: read list views from world.analytics when available (#2648)
* Add workflow analytics world APIs

* cli: read list views from world.analytics when available

inspect list views (runs, steps, events, hooks, sleeps) now read from the
optional world.analytics namespace when the active backend provides one,
falling back to the runtime storage APIs otherwise. Payload and detail views
are unchanged. Deprecate --with-data for list views; payloads are viewable
per-resource via 'inspect <resource> <id>'.

* cli: keep hook listing on the runtime storage API

The analytics read path omits ownerId (and the secret hook token), so routing
hook listing through it silently drops the ownerId column. Keep inspect hooks
on the runtime APIs, consistent with the web observability UI. Runs, steps,
events, and sleeps continue to use the analytics read path when available.

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

* Handle analytics access metadata in CLI

* test(cli): preserve analytics pageInfo in json output

* fix(cli): paginate analytics sleeps output

* fix(cli): correct deprecation message flag name to --withData

The list-view deprecation warning referenced '--with-data', but the actual
oclif flag is '--withData' (with '-d' alias); '--with-data' errors with
"Nonexistent flag". Fix the warning text, the doc comment, and the changeset
to reference the real flag name.

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

* fix(cli): preserve inspect json array output

* fix(cli): fall back when analytics lists are empty

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 11:44:57 -07:00
JJ Kasper 0f557d5ae4 Statically inject workflow world target (#2752)
* Statically inject workflow world target

* Fix static world injection in host bundles

* Fix static world injection gaps

* Fix Vite Nitro server startup

* Fix Nitro pg-native aliasing

* Fix static world target CI gaps

* Fix static world dev rebuild gaps

* Avoid broad runtime alias in Nitro

* Refresh Next dev route for step HMR

* Externalize Nest target world

* Use canary HMR rediscovery timeout

* Bundle local world in Nest builds

* Dedupe world target helpers and fix SvelteKit chunk patch guard
2026-07-06 14:19:45 -07:00
Pranay Prakash cb181392b9 feat(cli): print run deep links with --url, fix dashboard route (#2467)
Add a `--url` flag to `inspect`/`web` that prints a run's observability
dashboard deep link to stdout and exits — no browser, no local server —
so scripts and agents can share a link instead of opening a UI.

Fix the Vercel dashboard URL to the current
`…/workflows/runs/<id>?environment=<env>` route (drop the legacy
`/observability` segment) and respect `--env`. Apply the same route fix
to the e2e helpers, CI aggregation scripts, and the nextjs-turbopack
workbench. Document deep-linking in the workflow skill and observability
docs.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 17:37:03 -07:00
Karthik Kalyan 5bf2c167a5 Add serializable reviver compatibility check (#2250) 2026-06-04 12:50:32 -07: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
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
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
Karthik Kalyan eba7df381c [cli] Fix false "data expired" warning in CLI for non-expired runs (#1736)
* fix cli expiredAt check

* fix cli expiredAt check

* fix cli expiredAt check
2026-04-14 19:12:24 -07:00
Nathan Rajlich ac09f40771 feat: add clickable Run reference rendering in observability UI (#1681)
* feat: add clickable Run reference rendering in observability UI

When a serialized Run object appears in step input/output data, it is
now rendered as a clickable purple badge showing the runId. Clicking
navigates to the target run's detail page.

Changes:
- serialization-format.ts: Add RunRef type, isRunRef(), serializedRunToRunRef(),
  and 'Run' entry in observabilityRevivers
- data-inspector.tsx: Add RunRefInline component (purple badge), RunClickContext,
  collapseRefs() to make refs non-expandable in ObjectInspector
- attribute-panel.tsx: Thread onRunClick prop, wrap in RunClickContext.Provider
- entity-detail-panel.tsx: Thread onRunClick prop
- run-trace-view.tsx: Thread onRunClick prop
- workflow-trace-view.tsx: Thread onRunClick prop, reset selected span on run change
- trace-span-construction.ts: Show step name for builtin steps instead of empty string
- hydration.ts: Re-export RunRef types
- run-detail-view.tsx: Add handleRunRefClick that navigates to /run/{targetRunId}

* fix: guard collapseRefs against class instances and memoize result

Only recurse into plain objects (prototype is Object.prototype or null)
to avoid stripping class instances like Date, Error, Map, etc. that
have their own rendering in NodeRenderer. Also memoize the collapsed
result to avoid recomputing on every render.

* fix: detect Run instances in Instance reviver instead of fake Run serde key

The Run class goes through the standard Instance serialization pipeline
(WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE), not a dedicated 'Run' key.
Move the RunRef detection into serializedInstanceToRef() which checks
if the className is 'Run' and the data contains a runId string, then
returns a RunRef instead of a generic ClassInstanceRef.

* fix: use serializedInstanceToRef in web and CLI Instance revivers

Both the web and CLI hydration layers override the observabilityRevivers
Instance handler with their own implementation (for react-inspector
named constructors and CLI inspect.custom respectively), bypassing the
RunRef detection in serializedInstanceToRef. Fix by calling
serializedInstanceToRef first and returning a RunRef when detected.
2026-04-14 21:09:49 +00:00
Peter Wielander 873b4e2bb4 [core] Refactor getWorld interface to be asynchronous (#942) 2026-04-09 13:54:32 -07:00
Peter Wielander 66d49c0db6 [world] Restructure stream interface, require run ID for all step and stream operations (#1293) 2026-04-09 13:25:16 -07:00
Peter Wielander e4362421ab [builders] Switch Vercel Build Output API from CJS to ESM (#1562) 2026-04-08 16:37:37 -07:00
Peter Wielander a6bcea9d28 [cli] [core] Probe deployment specVersion before CLI start (#1629) 2026-04-07 11:03:33 -07:00
Nathan Rajlich 2680a427f0 fix: add Request and Response revivers to web and CLI hydration (#1414) 2026-04-06 06:30:36 +00:00
Nathan Rajlich f5d2aef58f Add serde compliance tooling and improve custom class serialization DX (#1552)
* Add serde compliance tooling and update skill documentation

- Add serde compliance checker library to @workflow/builders
- Add build-time warnings for serde classes with Node.js imports in workflow bundle
- Add 'workflow transform' CLI command for inspecting SWC output
- Implement 'workflow validate' CLI command with serde compliance checks
- Add serde analysis panel to SWC playground
- Update workflow skill with custom class serialization documentation

* Fix --json + --strict: use process.exitCode so JSON output is returned before exit

* Address code review feedback

- Fix --strict exit code: honor process.exitCode in BaseCommand.finally()
- Remove unused --module-specifier flag from transform command
- Add missing Node.js builtins (e.g. test) to playground detection list
- De-dupe build-time serde warnings by grouping identical issues across classes
- Make Serde Analysis panel collapsible like the output panels

* Use .gitignore for validate file discovery; fix changeset wording
2026-04-03 21:03:59 +00:00
Peter Wielander 5837d577c2 [cli] [world-local] Ensure update checks don't suggest upgrading from stable release to pre-releases (#1490) 2026-03-23 17:29:34 -07:00
Lucas Ralph 0d72b2d363 [cli] Add bulk cancel, --status filter, fix step JSON hydration (#1467) 2026-03-23 11:56:00 -07:00
Peter Wielander da6adf7798 [o11y] Polish display when run data has expired (#1438) 2026-03-23 11:38:19 -07:00
Peter Wielander fdbe853531 Fix CLI health check for Astro/Sveltekit and add debug http logs to world-vercel (#1442) 2026-03-18 12:58:03 -07:00
Nathan Rajlich 3c3f80a1f0 fix(cli): remove short flag collision on -e in health command (#1343)
The health command's `endpoint` flag and the shared `env` flag both
declared `char: 'e'`, causing ambiguity. Remove the short flag from
`endpoint` so `-e` unambiguously maps to `--env`.
2026-03-12 12:23:51 -07:00
Nathan Rajlich 9f3551caec Fix flaky Vercel prod e2e tests by skipping CLI update check (#1350)
The e2e tests spawn a CLI subprocess for every inspect/cancel/health call.
Each subprocess performs an npm registry version check on startup, which
can hang under load and exceed the 20s spawn timeout, causing SIGTERM.

Add WORKFLOW_NO_UPDATE_CHECK=1 env var support to skip the check, and
set it in the e2e test harness.
2026-03-12 12:22:52 -07:00
Nathan Rajlich d842ce1c43 fix: surface 429 rate-limit errors in e2e tests and CLI (#1309)
Multiple layered issues caused encryption key 429 errors to produce
confusing, unrelated-looking test failures:

- awaitCommand() in e2e utils now rejects on non-zero exit codes
  instead of silently resolving (the most critical fix)
- cliInspectJson/cliHealthJson throw on empty stdout instead of
  falling back to '{}'
- maybeDecryptFields re-throws HTTP errors instead of silently
  falling back to encrypted placeholders
- fetchRunKey includes response body and status text in errors
- Added 429 to CLI status text map
2026-03-10 00:23:05 -07:00
Peter Wielander 887cc2bd55 [web-shared] [cli] Refactor observability data fetching (#1261) 2026-03-10 01:36:03 +00:00
Nathan Rajlich 83dbd46456 Stop reading WORKFLOW_VERCEL_* env vars at runtime to prevent unintended proxy routing (#1304)
* Stop reading WORKFLOW_VERCEL_* env vars at runtime to prevent unintended proxy routing

createWorld() in core no longer reads WORKFLOW_VERCEL_PROJECT, WORKFLOW_VERCEL_TEAM,
WORKFLOW_VERCEL_AUTH_TOKEN, WORKFLOW_VERCEL_ENV, or WORKFLOW_VERCEL_PROJECT_NAME from
process.env. These env vars are intended for CLI/observability tooling only, and
reading them at runtime caused all workflow traffic to route through the
api.vercel.com proxy when users mistakenly set them as Vercel project env vars.

The Vercel runtime already provides everything needed: OIDC tokens for auth,
VERCEL_PROJECT_ID for encryption context, and VERCEL_DEPLOYMENT_ID for world
selection. createWorld() now calls createVercelWorld() with no config.

A warning is emitted if the env vars are detected at runtime, telling users to
remove them.

The CLI and e2e tests are updated to call createVercelWorld() directly with an
explicit config object and inject via setWorld(), keeping the WORKFLOW_VERCEL_*
env vars scoped to tooling contexts only.

* Refactor inferVercelEnvVars to return config instead of relying on process.env

inferVercelEnvVars() now returns a VercelEnvVars object with the resolved
config values. setup.ts uses the returned object directly for
createVercelWorld() instead of re-reading from process.env via getEnvVars().

The writeEnvVars() calls inside inferVercelEnvVars are consolidated into a
single call at the end, retained only for the embedded web UI which reads
process.env as a fallback in its server actions. The scattered writeEnvVars
calls after each inference step are removed.

* Address review: consistent e2e gate and expanded warning

- Use WORKFLOW_VERCEL_ENV as the gate in both e2e.test.ts and
  bench.bench.ts for consistency (was WORKFLOW_VERCEL_AUTH_TOKEN in
  e2e.test.ts, WORKFLOW_VERCEL_ENV in bench.bench.ts)
- Expand the misconfiguration warning to also detect
  WORKFLOW_VERCEL_AUTH_TOKEN and WORKFLOW_VERCEL_ENV, listing the
  specific env vars that are set

* Add changeset

Signed-off-by: Nathan Rajlich <n@n8.io>

---------

Signed-off-by: Nathan Rajlich <n@n8.io>
2026-03-09 20:46:19 +00:00
Nathan Rajlich 97932d3086 fix: thread runId through stream inspection for encryption key resolution (#1277)
CLI showStream:
- Requires --run with --decrypt for encrypted stream decryption
- Warns when --decrypt is used without --run

Web stream reading:
- readStreamServerAction accepts runId parameter for key resolution
- Stream API route reads runId from query param
- readStream client function passes runId to the API route
- useStreamReader hook accepts and passes runId
- run-detail-view passes runId to useStreamReader

Removes getRunIdFromStreamId helper (stream IDs don't always share
the run's ULID, e.g. streams serialized across step/workflow boundaries).
2026-03-05 23:36:36 +00:00
Nathan Rajlich b68ed630ec fix(cli): read orgId from project entry in repo.json, not root (#1263)
The Vercel CLI's repo.json format puts orgId on each project entry,
not at the root level. The CLI was reading repoConfig.orgId (undefined)
instead of project.orgId, which meant teamId was never set. Without
teamId, the world-vercel URL selector used the direct vercel-workflow.com
URL instead of the api.vercel.com proxy, causing 401 errors because
vercel-workflow.com only accepts OIDC tokens (not CLI auth tokens).

Also fix the RepoProjectConfig/RepoProjectsConfig type definitions
to match the actual repo.json structure.
2026-03-04 23:22:46 +00:00
Nathan Rajlich bbe40ff00a Opt-in decryption for o11y tooling (CLI + web) (#1256)
* Add browser-compatible AES-GCM to core and HKDF key derivation to world-vercel

* update changeset

* Move HKDF key derivation server-side: API returns per-run derived key

* Refactor encrypt/decrypt to accept CryptoKey, export importKey for callers to import once per run

* Overload getEncryptionKeyForRun: accept context for start(), fetch WorkflowRun in resume-hook

* Split changeset into per-package descriptions for world, world-vercel, and core

* Remove unnecessary Uint8Array.from() wrapper around Buffer.from()

* Use zod to parse Vercel API response

* fix: restore world-vercel files to main versions

The rebase incorrectly picked up older versions of these files from
early encryption branch commits. The main versions are correct and
up-to-date.

* fix: add type cast for hydrateStepReturnValue return in hook.ts

* Make decryption an explicit opt-in for o11y tooling

* Restore encrypted data handling in o11y hydration layer

* Use EncryptedDataRef with util.inspect.custom for CLI encrypted data display

* Fix Decrypt button crash: use correct 'refresh' callback from useWorkflowResourceData

* Implement client-side decryption for web o11y with getEncryptionKeyForRun RPC

* Fix CLI decrypt: fetch WorkflowRun for key resolution, cache per runId

* Use named constructor pattern for encrypted data display in web o11y

* Decrypt event data when encryption key is available after Decrypt button click

* Lift encryption key to run-level state, auto-decrypt on fetch, fix field pollution

* Re-load expanded event data when encryption key becomes available

* Consolidate Decrypt to title bar Button, remove sidebar decrypt card

* Add hover tooltip to Decrypt button explaining scope and state

* Show flat Encrypted label for encrypted fields, use Lucide Lock icon in DataInspector

* Render eventData subfields individually to avoid encrypted markers in collapsed preview

* Revert: render eventData subfields individually

* Fix Lock icon vertical alignment in DataInspector encrypted label

* update changeset

* Update CLI, web, and stream callers for CryptoKey: importKey at resolution sites

* Pass teamId to the get-key endpoint

* fix: remove unused DataInspector import in events-list.tsx

* fix: restore world-vercel files to base branch versions

Cherry-pick conflict resolution incorrectly took the older opt-in-decrypt
versions of these files, reverting improvements from main (dispatcher,
createGetEncryptionKeyForRun extraction, nullable key response).

* fix: address PR review feedback

- Remove duplicate AttributePanel/EventsList rendering in entity-detail-panel.tsx.
  Thread encryptionKey into the existing EventsList render instead.
- Restore missing re-exports (isClassInstanceRef, isStreamId, isStreamRef)
  in web-shared/src/index.ts to maintain backwards compatibility.
- Add 'error' to replaceEncryptedWithMarkers field list in web-shared
  hydration.ts to match the decrypt path.
- Extend CLI hydration eventData decrypt/placeholder to cover all known
  serialized fields (output, metadata, payload) not just result/input.
- Add 'error' to CLI replaceEncryptedWithRef field list.
- Remove invalid encryptionKey option from useWorkflowResourceData call
  (hook doesn't support it yet), add TODO.
- Add 4 unit tests for hydrateDataWithKey in serialization-format.test.ts:
  encrypted+key decrypts, encrypted+noKey returns raw, non-encrypted
  hydrates normally, non-Uint8Array legacy data passes through.

* feat: thread encryptionKey through useWorkflowResourceData hook

Instead of leaving a TODO, implement the encryptionKey support directly:
- Add optional encryptionKey to useWorkflowResourceData options
- When key is available, use hydrateResourceIOWithKey (async decrypt)
  instead of hydrateResourceIO for all resource types
- Remove redundant hydrateResourceIO from fetchResourceWithCorrelationId

* fix: address comprehensive review feedback on PR #1256

High priority:
- Gate showStream key fetch on --decrypt flag, warn when --decrypt
  used without --run
- Fix workflow-server-actions.server.ts missing cryptoKey params
  (undefined for both getExternalRevivers and getDeserializeStream)
- Add hydration + decryption to listEvents (was completely missing)
- Fix error/eventData display: check isEncryptedMarker before
  hasDisplayContent so encrypted markers don't silently disappear

Medium priority:
- handleDecrypt: use toast.error() instead of console.error for
  user-visible feedback on key fetch failures
- CLI maybeDecryptFields: add try/catch with graceful fallback to
  encrypted placeholders + warning, also decrypt error field
- use-resource-data: wrap hook/sleep hydrate() in try/catch to
  prevent stuck loading state on decryption errors
- Decrypt button: also check run.error and step input/output for
  encrypted markers, not just run.input/output

Low priority:
- event-list-view: add .catch() to re-load useEffect promise
- Export ENCRYPTED_DISPLAY_NAME from hydration.ts and import in
  data-inspector.tsx instead of raw 'Encrypted' string
2026-03-04 13:17:09 -08:00
Nathan Rajlich 7618ac36c2 Wire AES-GCM encryption into serialization layer (#1251)
* fix(core): chain unconsumed event check onto promiseQueue to prevent false positives

The EventsConsumer's unconsumed event check (setTimeout(0)) was racing
against the promiseQueue's async deserialization. When parallel steps
completed and their hydrateStepReturnValue did real async work (e.g.,
decryption), the setTimeout(0) fired before the promise chain resolved
the step results and triggered the next subscribe() call. This caused
step_created events for sequential steps to be falsely flagged as
unconsumed/orphaned.

Fix: chain the unconsumed check onto the promiseQueue via getPromiseQueue()
so it only fires after all pending async work completes. Use
process.nextTick (not setTimeout) after the queue drains to give
synchronous subscribe() calls from resolved user code a chance to cancel.
Version-based cancellation replaces clearTimeout since the check is now
promise-based.

Adds getPromiseQueue option to EventsConsumerOptions. The workflow.ts
context uses a getter/setter to keep the promiseQueue holder in sync.

Reproduction test: parallel steps A+B with 10ms mock deserialization
delay, followed by sequential step C. Previously failed with
'Unconsumed event: step_created(C)'. Now passes.

* fix: chain hydrateWorkflowArguments onto promiseQueue to prevent false unconsumed events

The unconsumed event check was firing during the async gap between
run_started consumption and the workflow function subscribing its
first step callbacks. This happened because hydrateWorkflowArguments
is async, and during its await, the EventsConsumer advanced to
step_created events that had no subscriber yet.

Fix: chain hydrateWorkflowArguments onto the promiseQueue so the
unconsumed check (which waits for the queue to drain) doesn't fire
until after the workflow arguments are hydrated and the workflow
function has been invoked.

* fix: use setTimeout(0) macrotask for unconsumed check to ensure VM promise propagation completes

The process.nextTick-based unconsumed check was still racing against
VM promise propagation. After promiseQueue resolves and the user code's
resolve() fires, there are multiple microtask hops through the VM
boundary before the workflow code actually calls subscribe() for the
next steps. process.nextTick fires before those VM microtasks complete.

setTimeout(0) is a macrotask that is guaranteed to fire only after ALL
microtasks (including VM promise chain propagation) have drained. The
pendingUnconsumedTimeout handle is stored and cleared in subscribe()
to prevent keeping the event loop alive unnecessarily.

* fix: increase unconsumed event check delay to 100ms for cross-VM promise propagation

setTimeout(0) is insufficient because Node.js does not guarantee that
macrotasks fire after all cross-context (VM boundary) microtasks settle.
After promiseQueue resolves and resolve() fires in the host context,
there are multiple microtask hops through the VM boundary before the
workflow code actually calls subscribe(). A 100ms delay provides
sufficient time for this propagation while still detecting truly
orphaned events promptly.

Also update sleep.test.ts to wait 200ms for the unconsumed check.

* Add browser-compatible AES-GCM to core and HKDF key derivation to world-vercel

* update changeset

* Move HKDF key derivation server-side: API returns per-run derived key

* Refactor encrypt/decrypt to accept CryptoKey, export importKey for callers to import once per run

* Overload getEncryptionKeyForRun: accept context for start(), fetch WorkflowRun in resume-hook

* Split changeset into per-package descriptions for world, world-vercel, and core

* Remove unnecessary Uint8Array.from() wrapper around Buffer.from()

* Use zod to parse Vercel API response

* Wire encryption into serialization layer

* Wire AES-GCM encryption into serialization layer

* update changeset

* Add encryption unit tests: primitives, maybeEncrypt/maybeDecrypt, isEncrypted, complex type round-trips

* Accept CryptoKey in encrypt/decrypt, export importKey for callers to import once per run

* Fix review comments: cache stream encryption key, remove redundant casts, fix stale comments

* Trying to clean up some type non-sense

* fix: restore world-vercel files to main versions

The rebase incorrectly picked up older versions of these files from
early encryption branch commits. The main versions are correct and
up-to-date.

* fix: add type cast for hydrateStepReturnValue return in hook.ts

* fix: address review feedback on encryption PR

- Remove Vercel-specific error message from maybeDecrypt (core should
  not reference VERCEL_DEPLOYMENT_KEY)
- Move stream encryption/decryption from transport layer
  (WorkflowServerReadableStream/WritableStream) to framing layer
  (getSerializeStream/getDeserializeStream). Frame length headers stay
  in the clear so frame boundaries are always parseable regardless of
  transport chunking; encryption wraps the frame payload.
- Remove explicit Promise<unknown> return types from all 4 hydrate
  functions. On main these had inferred types (any from devalue),
  so callers didn't need casts. The encryption branch added explicit
  annotations that broke this.
- Revert unnecessary type casts in run.ts, step-handler.ts, hook.ts
  that were only needed due to the explicit Promise<unknown> annotations
- Revert closureVars type from unknown back to Record<string, any>
  in context-storage.ts to match the contract with getClosureVars
- Fix hydrateWorkflowArguments JSDoc for unused _runId parameter

* Revert more unnecessary changes

* cleanup: remove unused runId param, deduplicate processFrames, add legacy comments

- Remove unused _runId parameter from WorkflowServerReadableStream
  constructor and all 4 call sites
- Deduplicate processFrames decryption: decrypt first and reassign
  format/payload, then fall through to single deserialization path
- Add comments on all legacy non-Uint8Array branches explaining when
  this happens (specVersion 1 runs stored data as plain JSON arrays)
- Fix duplicate code block in hydrateStepReturnValue

* feat: wire cryptoKey through stream serialize/deserialize pipeline

Thread the encryption key through the entire stream serialization chain
so that ReadableStream and WritableStream values are encrypted/decrypted
at the framing level.

- Add optional cryptoKey param to getExternalReducers, getStepReducers,
  getExternalRevivers, getStepRevivers
- Pass cryptoKey to getSerializeStream/getDeserializeStream at all 8
  internal call sites within reducers/revivers
- Thread key from dehydrate/hydrate functions into their reducers/revivers
- Cache encryption key in Run class (resolved once via getEncryptionKey(),
  reused for returnValue, getReadable(), etc.)
- Make Run#getReadable() async to resolve the cached key before creating
  the deserialize stream
- Add encryptionKey to step context storage so getWritable() can access
  it during step execution

* fix: make cryptoKey required-but-nullable to prevent silent omission, add stream encryption tests

Change cryptoKey parameter from optional (cryptoKey?) to required-but-
nullable (cryptoKey: CryptoKey | undefined) on all 6 functions:
- getSerializeStream, getDeserializeStream
- getExternalReducers, getStepReducers
- getExternalRevivers, getStepRevivers

This ensures every call site must explicitly pass the key or undefined,
making it impossible to accidentally omit it and silently skip encryption.

Add 7 stream encryption round-trip tests:
- Encrypted frames have 'encr' prefix inside length header
- Full round-trip: encrypt serialize -> decrypt deserialize
- Concatenated encrypted frames (transport coalescing)
- Split encrypted frames (transport splitting)
- Error when encrypted data encountered without key
- No encryption when key is undefined
- Large payload round-trip

Full audit confirms all encryption key threading is complete:
- All 8 dehydrate/hydrate functions pass key to reducers/revivers
- All stream serialize/deserialize call sites pass key
- Run class caches key for reuse across returnValue and getReadable()
- Step context storage carries key for getWritable()

* fix: keep Run#getReadable() sync, resolve encryption key lazily in streams

- Revert Run#getReadable() to synchronous (non-breaking API).
  The encryption key is passed as a Promise through the chain and
  resolved lazily inside the first async transform() call.
- Add EncryptionKeyParam type alias that accepts CryptoKey, undefined,
  or Promise<CryptoKey | undefined>. Used by getSerializeStream,
  getDeserializeStream, and all reducer/reviver functions.
- Key promises are resolved once on first use via a keyState cache
  object inside each stream's transform closure.
- Fix CLI showStream to resolve encryption key from world when runId
  is provided via --run flag, instead of passing undefined.
- Remove incorrect CLI warning that --run is not supported for streams
  (it is now needed for encrypted stream decryption).

* .

* fix: address review feedback from PR #1251

- Fix 4 broken dehydrateWorkflowArguments calls in workflow.test.ts
  that were passing ops as runId (missing runId and key params)
- Use WorkflowRuntimeError instead of plain Error in decodeFormatPrefix
  for unknown serialization formats, for consistency and programmatic
  error handling
- Document maybeDecrypt throw behavior: callers should be aware this
  surfaces as a rejected promise during key rotation/misconfiguration
- Document key-fetch rejection timing in streams: promise rejection
  won't surface until the first chunk is processed
2026-03-04 12:19:49 -08:00
JJ Kasper a9fea9132e Update workbench tests to build and run outside of monorepo (#1230)
* Setup fixes

* ci: run local e2e against staged tarball workbenches

* ci: update staged workbench tarball setup script

* chore: set nextjs workbenches back to next 16.1.6

* update lock

* test(e2e): resolve workbench path from WORKBENCH_APP_PATH

* fix: address deferred builder issues outside monorepo

* ci: stage tarball workbenches only for nextjs local e2e

* fix(next): discover deferred steps imported via workflows

* test(core): gate deferred step-discovery dev test to canary

* test(e2e): cover cross-file imported step in build/start lanes

* fix(e2e): use local manifest in local runs and relax dev rebuild timeout

* fix(workbench): add imported-step workflow symlink for sveltekit/astro

* test(e2e): scope imported-step workflow test to nextjs lanes

* fix(next): rebuild deferred entries on discovered file updates

* fix(next): watch transitive deferred step deps for dev rebuilds

* fix(next): restore socket-driven deferred step rebuilds

* add changeset

* chore: address review feedback on deferred e2e updates

* fix(cli): guard stream flush against closed write streams
2026-03-03 11:17:39 -08:00
Peter Wielander dda67421cf [world-postgres] [cli] Skip graphile logs for CLI json mode, observe DEBUG env (#1167) 2026-02-23 22:25:02 +00:00
Pranay Prakash 14863bf622 fix(workflow): improve runtime API stub error message and fix SKILL.md streaming docs (#1077)
* fix(workflow): improve error message for runtime API usage in workflow context and fix SKILL.md streaming docs

The workflow stub error now tells users to move calls to a step function or outside the workflow context. Also corrects the SKILL.md streaming section which incorrectly stated getWritable() only works in steps.

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

* chore: add changeset

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

* fix(skill): address PR feedback on SKILL.md

- Remove `npx workflow start` from debugging section (args can't easily be passed via CLI)
- Mention --json and --help flags for CLI inspection
- Add reminder to check AI docs for DurableAgent details

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

* fix(skill): fix SKILL.md debugging section CLI commands

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

* other improvements

* fix(skill): address Copilot PR feedback on streaming examples

- Add missing imports and define all referenced variables
- Wrap writer.releaseLock() in finally blocks for safety
- Remove undefined `data`/`agent`/`messages` references
- Simplify examples to be self-contained and copy-pasteable

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

* chore(skill): bump SKILL.md version to 1.1

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

* docs: add skill version bump rule to CLAUDE.md

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: John Lindquist <johnlindquist@gmail.com>
2026-02-19 15:17:04 -08:00
Nathan Rajlich ea3254e7ce Fix projectConfig.projectId containing project name instead of ID (#999)
* Fix projectConfig.projectId containing project name instead of ID

* add changeset

* Address review: fix changeset description, normalize slug-based project IDs, add clarifying comments
2026-02-18 02:03:31 -08:00
Nathan Rajlich 6e72b295e7 Add World.getEncryptionKeyForRun and thread encryption key through serialization (#979)
## Summary

- Adds `World.getEncryptionKeyForRun(run)` returning `Uint8Array | undefined` as the interface for retrieving per-run encryption keys
- Updates all 8 dehydrate/hydrate serialization functions to accept `key: Uint8Array | undefined`
- Updates runtime callers, CLI, and tests to thread the key parameter through
2026-02-18 00:53:49 -08:00
Nathan Rajlich 0d5323c0a7 Make serialization functions async (#978)
## Summary

- Makes all 8 dehydrate/hydrate serialization functions in `@workflow/core` async (returning `Promise<...>`)
- Updates all call sites across runtime, CLI, and test code to `await` the results
- Pure mechanical refactor — no functional changes, prerequisite for encryption support

Part 1 of the end-to-end encryption PR stack.
2026-02-17 23:18:44 -08:00
Nathan Rajlich 54879835f3 Fix pages router default args: use empty array instead of [42] (#1081) 2026-02-17 01:47:48 -08:00
Nathan Rajlich 262ef3a21a Fix CLI missing specVersion in "run_cancelled" event payload (#1078)
The CLI's `cancelRun` in `packages/cli/src/lib/inspect/run.ts` sent `{ eventType: 'run_cancelled' }` without `specVersion`, causing the Vercel API's `/v2/runs/{id}/events` endpoint to reject the request with a 400 error. The core runtime's `cancelRun` already handled this correctly, so the fix removes the redundant CLI wrapper and calls the core implementation directly.

The bug was introduced in 4966b72 ("implement event-sourced architecture (#621)") which changed the CLI cancel from `world.runs.cancel(runId)` to `world.events.create(runId, ...)` but omitted `specVersion` from the payload. The same commit correctly included it in `Run#cancel()` in core — it was simply missed in the CLI path. A later commit (86a7930) added v1Compat branching but still never added specVersion. The final merged state in a2b688d (PR #894) still omits it.

Also adds a CLI cancel e2e test that starts a long-running workflow, cancels it via the CLI command, and verifies the run status.
2026-02-16 18:16:06 -08:00
Nathan Rajlich 8cfb43808b Use @vercel/cli-auth for auth token reading and OAuth refresh (#1043)
* Use `@vercel/cli-auth` for auth token reading and OAuth refresh

Replace the manual auth.json reading logic in the CLI with the `@vercel/cli-auth` package, which handles credential storage via CredentialsStore and OAuth token refresh via the OAuth client.

Previously, the CLI would read the token from disk but had no refresh logic — if the token was expired, API calls would fail.
Now, getAuthToken() checks token expiry and automatically refreshes it using the stored refresh token before returning it.

* Remove dead logging
2026-02-13 21:46:11 +00:00
Nathan Rajlich c56dc3848e Extract browser-safe serialization format and move hydration client-side (#1015)
## Summary

Split the serialization/deserialization logic into environment-specific layers so data hydration can happen client-side in the browser. This is a prerequisite for e2e encryption where decryption keys are only available in the browser.

## Architecture

### Layer 1: `@workflow/core/serialization-format` (new, browser-safe)
- Format prefix encoding/decoding (`devl`, future `encr`, etc.)
- Generic `hydrateData()` dispatch — handles Uint8Array (v2 binary), legacy arrays (v1), and plain values
- `hydrateResourceIO(resource, revivers)` resource-type dispatcher (step/hook/event/workflow field mapping)
- `ClassInstanceRef` (plain data class, no `node:util` dependency)
- `StreamRef`, type guards (`isStreamRef`, `isStreamId`, `isClassInstanceRef`), utility functions (`extractStreamIds`, `truncateId`)
- Shared `observabilityRevivers` for stream/class/step display overrides
- 36 unit tests covering all of the above

### Layer 2: Environment-specific revivers
- **`@workflow/web-shared`** (`lib/hydration.ts`) — browser-safe revivers using `atob()` for base64, real `URLSearchParams`/`Headers`/`URL` instances, `ClassInstanceRef` for UI rendering
- **`@workflow/cli`** (`lib/inspect/hydration.ts`) — Node.js revivers using `Buffer.from()` for base64, `CLIClassInstanceRef` with `util.inspect.custom` for CLI output

Each module exports a pre-bound `hydrateResourceIO(resource)` that uses its environment's revivers.

### Removed: `@workflow/core/observability`
- Deleted `observability.ts` and `observability.test.ts` entirely (no remaining consumers)
- Removed `"./observability"` export from `@workflow/core/package.json`
- Removed the `workflow` package's `internal/observability.ts` re-export
- All functionality has been split between `serialization-format.ts` (shared types/utilities) and the environment-specific hydration modules

## Web package changes
- Server passes raw world data through without hydration (CBOR preserves `Uint8Array`)
- Client calls `hydrateResourceIO` from `@workflow/web-shared` after receiving CBOR-decoded data
- No Vite `node:*` stubs needed since `@workflow/core/serialization-format` is browser-safe
- Optimized: event hydration finds the matching event before hydrating (instead of hydrating all)
- Reduced server log noise from handled API errors (4xx errors no longer logged)

## Packages affected
- `@workflow/core` — new `serialization-format` export (with tests), removed `observability` export
- `@workflow/web-shared` — new `lib/hydration.ts` with browser-safe revivers
- `@workflow/cli` — new `lib/inspect/hydration.ts` with Node.js revivers, updated `output.ts` import
- `@workflow/web` — client-side hydration, removed server-side hydration
- `workflow` — removed `internal/observability.ts` re-export
2026-02-12 16:58:19 -08:00
Nathan Rajlich 7653e6bfdb Migrate @workflow/web from Next.js to React Router v7 (#1005)
## Summary

- Replace Next.js App Router with React Router v7.13.0 framework mode (Vite-based), eliminating the large `next` dependency from the web, CLI, and workflow metapackages
- Serve the web UI in-process from the CLI via Express instead of spawning `next start` as a child process
- Switch RPC transport from JSON to CBOR to preserve binary data types across the wire
- Replace `nuqs` URL state management with React Router's `useSearchParams`
- Replace Next.js server actions with an RPC resource route (`/api/rpc`) and a thin CBOR-based client

## Motivation

The `next` package is ~300MB installed and was the single largest dependency in the monorepo. It also required spawning a separate child process from the CLI to run the o11y web server, adding complexity around process lifecycle management, port readiness polling, and environment variable forwarding.

With React Router framework mode, the web package builds to a standard Express-compatible server bundle that the CLI can import and serve directly in its own process.

## What changed

**Framework swap (`@workflow/web`):**
- `next.config.ts` / `postcss.config.mjs` → `react-router.config.ts` / `vite.config.ts`
- `src/` directory → `app/` directory (React Router convention)
- `src/app/layout.tsx` + `layout-client.tsx` → `app/root.tsx`
- `src/app/page.tsx` → `app/routes/home.tsx`
- `src/app/run/[runId]/page.tsx` → `app/routes/run-detail.tsx`
- Path alias `@/` → `~/`
- Removed all `'use client'` / `'use server'` directives

**Data transport:**
- Server actions → RPC resource route at `/api/rpc` with CBOR encoding
- CBOR preserves `Uint8Array` and other binary types natively (no base64 overhead)
- Stream reading → dedicated `/api/stream/:streamId` resource route

**URL state:**
- `nuqs` (`useQueryState`) → `useSearchParams` from `react-router`

**Fonts:**
- `next/font/google` → Geist `.woff2` files referenced directly from `node_modules/geist` via `@font-face` in CSS

**CLI integration (`@workflow/cli`):**
- `import('@workflow/web/server').then(m => m.startServer(port))`
- No child process, no readiness polling, no cleanup handlers

**Radix UI compatibility:**
- `onSubmit` preventDefault on `AlertDialogContent` and `SheetContent` to prevent Radix's internal `<form method="dialog">` from triggering React Router route actions
- Catch-all action on root route for any stray POSTs

## Dependencies removed
- `next`, `swr`, `nuqs`, `@tailwindcss/postcss`

## Dependencies added
- `react-router` / `@react-router/dev` / `@react-router/node` / `@react-router/express` (all `7.13.0`)
- `express`, `vite`, `@tailwindcss/vite`, `cbor-x`, `isbot`, `cross-env`
- `geist` (devDep)
2026-02-12 16:30:51 -08:00
Peter Wielander f7fd88ea96 [core] Tidy health check latency calculation (#951) 2026-02-06 15:54:53 -08:00
Peter Wielander a2b688d062 [core] Add runtime and world backwards compatibility for specVersion v1 (#894) 2026-01-30 13:23:08 -08:00
Nathan Rajlich 1060f9d04a Change user input/output to be binary data at the World interface (#853) 2026-01-28 10:36:28 -08:00
Peter Wielander 57f637653d [world-local] Fix package info stored in data dir showing the wrong version (#718) 2026-01-23 13:46:44 -08:00
Nathan Rajlich bd8116d40b Add WORKFLOW_SERVER_URL_OVERRIDE var to "world-vercel" for testing (#833)
Added `WORKFLOW_SERVER_URL_OVERRIDE` configuration to the Vercel world adapter and removed the deprecated `WORKFLOW_VERCEL_SKIP_PROXY` and `WORKFLOW_VERCEL_BACKEND_URL` environment variables.

### What changed?

- Added a changeset for a patch release across multiple packages
- Removed `WORKFLOW_VERCEL_SKIP_PROXY` environment variable from GitHub workflow tests
- Removed `WORKFLOW_VERCEL_BACKEND_URL` from environment variables in CLI and core packages
- Simplified the URL resolution logic in the Vercel world adapter
- Added support for a `WORKFLOW_SERVER_URL_OVERRIDE` constant for testing against different workflow-server versions
- Added the `x-vercel-workflow-api-url` header when the URL override is set

### How to test?

1. Verify that Vercel deployments continue to work without the removed environment variables
2. Test with a custom workflow server URL by setting the `WORKFLOW_SERVER_URL_OVERRIDE` constant in the world-vercel package

### Why make this change?

This change simplifies the configuration for the Vercel world adapter by removing deprecated environment variables and standardizing on a cleaner approach for specifying the workflow API URL. The new implementation automatically determines whether to use the proxy based on project configuration, making it more intuitive and reducing the need for explicit configuration.
2026-01-23 11:01:45 -08:00