Pranay Prakash
90d · built 2026-07-24
90-day totals
- Commits
- 72
- Grow
- 11.9
- Maintenance
- 16.4
- Fixes
- 7.1
- Total ETV
- 35.4
Where this dev ranks
Percentile against the global top-100 leaderboard (all-time totals).
- By commits
- Top 87 %
- By Growth share
- Top 41 %
30-day trajectory
Last 30 days vs. the 30 days before. Up arrows on Growth and ETV mean improvement; up arrow on Fixes share means more time on fixes (worse).
↓-81.4 %
vs 43 prior
↓-11.2 pp
recent vs prior
↓-11.1 pp
recent vs prior
Daily performance
Daily ETV, stacked by Growth, Maintenance and Fixes.
Work-mix over time
Share of Growth / Maintenance / Fixes over a rolling 7-day window. Reads as 'where is effort flowing right now'.
Repository spread
Where this developer's commits land. Concentrated work (top1 > 80%) vs polymath spread (top1 < 30%).
Most impactful commits
Top 20 by ETV in the 90-day window.
- 3.0ETVFriendlier 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>github.com-vercel-workflow · 1203dae7 · 2026-05-04
- 2.0ETVRFC: compress serialized payload refs — zstd (gzip fallback), specVersion 5 (#2394) * feat(core,world): gzip-compress serialized payloads behind specVersion 5 Add a composable 'gzip' format prefix layer to the serialization pipeline (compress before encrypt: encr(gzip(devl))), cutting stored payload bytes by ~70-87% on real-world-style workloads. Compression is gated on run specVersion 5 (new SPEC_VERSION_SUPPORTS_COMPRESSION) and on target-deployment capabilities for cross-deployment writes; payloads under 1KB or that don't compress meaningfully are stored unchanged. Reads dispatch on the format prefix so both compressed and uncompressed data are always readable. WORKFLOW_DISABLE_COMPRESSION=1 disables writes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(core): add CPU/perf compression benchmark + shared workloads Split the compression benchmark into reproducible size and CPU scripts sharing deterministic workloads (lib/workloads.mjs). The CPU benchmark measures serialize/deserialize overhead per payload, total CPU across thousands of events, and compares gzip levels/brotli/deflate. Documents how to run the size, CPU, and end-to-end (bench.bench.ts) benchmarks against local and Vercel in scripts/README.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(world-vercel): advertise specVersion 5 to enable compression on Vercel Now that workflow-server declares spec-5 support (vercel/workflow-server#520), bump the Vercel world's advertised specVersion from 4 to 5 so new Vercel runs are stamped spec 5 and become eligible for gzip payload compression. Payloads stay opaque to the server (compression is client-side); spec 5 is a superset of spec 4, so initial run attributes still work. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(core): emit OTel span attributes for compression impact Track gzip payload compression on both the serialize (write) and deserialize (read) paths via span attributes: workflow.serialization.{operation,compressed,uncompressed_bytes, stored_bytes,compression_ratio}. Sizes are measured at the compression boundary (pre-encryption), so they reflect compression's effect rather than the at-rest size. The compression codec stays pure — compress/decompress optionally populate a CompressionStats sink, threaded through CodecOptions to the mode serializers and read by the dehydrate/hydrate wrappers, which set attributes on the active span. Telemetry failures are swallowed so they can never break the serialize/deserialize data path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(core,web-shared): prefer zstd compression codec (gzip fallback) Switch the payload compression codec to zstd, which benchmarks 3–7× faster than gzip at an equal-or-better ratio on representative workloads (compression runs at every step boundary, so the write CPU is a per-step tax). zstd uses node:zlib (>= 22.15); gzip via the portable CompressionStream remains the fallback when zstd is unavailable, and WORKFLOW_COMPRESSION_CODEC=gzip forces it. Reads dispatch on the format prefix, so 'zstd' and 'gzip' payloads are both always decodable. zstd is Node-only (Web CompressionStream has no zstd), so the browser o11y read path registers a WASM-backed decoder (@tootallnate/zstd-wasm) via a new registerZstdDecoder hook; node:zlib handles Node-side reads (runtime replay, CLI, server o11y). A new workflow.serialization.codec span attribute reports which codec applied. gzip and zstd read support co-ship, so the existing specVersion-5 capability gate is unchanged. Verified end-to-end: spec-5 runs store zstd-prefixed payloads on disk and replay/complete correctly; the WASM decoder round-trips node:zlib zstd output. Benchmarks updated to compare zstd vs gzip. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>github.com-vercel-workflow · 5f0b8452 · 2026-06-16
- 2.0ETVdocs: move World SDK and getWorld under workflow/runtime, split out workflow/observability (#2375)github.com-vercel-workflow · 055b6664 · 2026-06-12
- 1.8ETVtarballs: redesign preview tarballs index page (#1911) * tarballs: redesign preview tarballs index page Rebuild the static index page produced by `tarballs/scripts/pack.ts`: - Featured `workflow` package up top with prominent install command, copy button, and direct tarball download - Top-of-page metadata chips: short SHA (linked to commit), branch, PR number, build timestamp, package count + total size - Collapsible "What is this?" explainer - Package-manager tab toggle (pnpm / npm / yarn / bun) that swaps the install command for every row in place - Live filter input over the rest of the package list (with `/` shortcut) - Per-row install command, copy button, and direct download - Modern dark/light theme with system preference, Geist-inspired styling Also captures tarball size during pack and renders human-readable byte counts. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * tarballs: fix client-side interactivity broken by HTML-encoded JSON `escapeHtml(JSON.stringify(catalog))` was HTML-encoding every quote in the embedded catalog JSON to `"`, so `JSON.parse(textContent)` threw on the first character and the IIFE bailed before attaching any event listeners — package-manager toggle, search filter, copy buttons, and the `/` shortcut were all dead UI on the deployed page. `<script type="application/json">` content is treated as text by the HTML parser; the only sequence that can break out is `</script>` (or `</` in legacy parsers). Replace `<` with the JSON `<` escape, which is legal per the JSON spec and prevents the breakout without needing entity encoding. Also switch `formatBytes` from `KB`/`MB` to `KiB`/`MiB` since the divisor is 1024. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * tarballs: rewrite as Vite + Preact SPA with file breakdown, fix bundling Address TooTallNate's review feedback by replacing the hand-rolled HTML- in-template-literal approach with a small Vite + Preact SPA. The old ~600 lines of inlined HTML/CSS/JS in `pack.ts` is now `~80 lines of TSX`, fully type-checked. Layout: - `tarballs/index.html`, `vite.config.ts`, `tsconfig.json` at the root - `src/main.tsx` mounts the Preact app and fetches `/catalog.json` - `src/app.tsx` is the page (Header, FeaturedCard, PackageRow, etc.) - `src/catalog.ts` is the shared types + helpers (`buildInstallCommand`, `formatBytes`) - `src/icons.tsx`, `src/styles.css` - `scripts/pack.ts` is now data-only — it scans packages, packs tarballs, and writes `public/catalog.json` The eliminates several smells the reviewer called out: - The interactive script is now TypeScript with strict mode and JSX type checking instead of an inline `<script>` block - The `escapeHtml`-around-JSON-blob hack that broke client-side JS in the prior commit is gone; the SPA fetches `catalog.json` and parses it natively - Pack-time logic and presentation logic no longer share a file # Fix bundling: tarballs now actually contain compiled code While verifying real tarball sizes I noticed `workflow-serde.tgz` was only 828 bytes — it had `package.json`, `LICENSE.md`, `README.md` and *nothing* else, because each package's `files: ["dist"]` excludes sources but `dist/` hadn't been built. The Vercel build was running `pnpm --filter tarballs build`, which only builds the `tarballs` package itself — its workspace dependencies were never built. Switch `vercel.json#buildCommand` to `pnpm turbo run build --filter=tarballs`, which transitively builds dependencies first via the `dependsOn: ["^build"]` rule already in the root `turbo.json`. With the fix: workflow: 241 KiB → 252 KiB tarball, 916 KiB unpacked, 205 files @workflow/core: 59 KiB → 493 KiB tarball, 1.70 MiB unpacked, 236 files @workflow/serde: 828 B → 1.4 KiB tarball, 4.6 KiB unpacked, 7 files Add a smoke check that the `workflow` package has at least 5 files in its tarball — catches the regression directly. # Per-package contents view (packagephobia-style) `pack.ts` now also runs `tar -tvzf` on each tarball and records the file list with sizes. The SPA renders this as an expandable "What's inside?" disclosure per package, grouped by top-level directory (e.g. `dist/`, `docs/`) with proportional bars showing each group's share of the unpacked size, and the largest files listed below. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * tarballs: replace tar shell-out with in-process tar reader The smoke check broke in CI: `'workflow' tarball only has 0 files`. Root cause is that `tar -tvzf` emits a different verbose layout on GNU tar (Linux, what CI runs) vs BSD tar (macOS, where I tested locally) — the parser only matched the BSD column ordering, so on Linux every line was rejected and `fileCount` came out as 0. Replace the shell-out with a small in-process tar reader using `zlib.gunzipSync` + manual 512-byte block walk. ustar headers are trivially structured (name at offset 0, octal size at 124, typeflag at 156, ustar prefix at 345). We emit regular files only (`typeflag` `0` or NUL) and consume but skip pax extended headers (`x`/`g`) and GNU long-name entries (`L`). Result is identical on every platform. Verified locally: 206 files / 998413 bytes for `workflow.tgz` matches `tar -tvzf` exactly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * tarballs: redesign per-package details with packagephobia-style stats The previous "What's inside?" view crammed nested directory groups, proportional bars, and per-group file lists into a `<details>` inside an already-narrow row. It was hard to read and harder to compare. Replace it with the layout packagephobia uses on its result page: - Two large headline metric tiles (Publish size / Unpacked size) with a big bold value, smaller unit, and small uppercase label. Modeled directly on packagephobia's `Stats` component but using our existing CSS variables so it tracks light/dark theme. - A single sortable file table beneath. Default is size-descending so the contributors to package size are immediately visible. Click a header to flip direction or switch sort key. Sticky header keeps the columns visible inside the scrollable region. Drop the `groupByTopLevel`, `ContentsGroup`, and bar-chart styles — they were the source of the "hard to use" feedback and don't add information that the flat sortable table doesn't already convey. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * tarballs: address Copilot review feedback (a11y, dev script, caching) - main.tsx: drop `cache: 'no-store'` from the catalog fetch. Each tarballs deployment is immutable per commit, so HTTP caching is appropriate; forcing no-store made every visit re-download the full catalog (which now includes per-package file lists). - app.tsx (search input): add `aria-label="Filter packages"`. The visible label only contained an icon and placeholder, so screen readers had no name for the control. - app.tsx (PmTabs): replace `role="tablist"` / `role="tab"` / `aria-selected` with plain buttons that use `aria-pressed`. The ARIA tab pattern requires arrow-key roving focus we never wired up; toggle buttons are the honest representation. Each button also gets an explicit `aria-label`. - app.tsx (row buttons): include the package name in the accessible label of every per-row copy/download button (and on the featured card too), so the screen reader buttons/links list distinguishes them. Added an `accessibleName` prop to `CopyButton`. - app.tsx (CopyButton): only flip to the "Copied" state when the write actually succeeded. Both the modern `navigator.clipboard` path and the `execCommand` fallback can fail; the new `writeToClipboard` helper returns success and the button shows a short "Failed" state if both paths fail. # Make `pnpm dev` work from a clean checkout The previous `dev: vite` couldn't actually serve the page because `/catalog.json` 404s and the SPA boots into the error fallback. Restructure the build layout to vite's conventional shape: - `public/` is now a true vite public dir — pack writes tarballs and catalog.json there. In dev, vite serves these at the root. - `dist/` is the production build output (vite copies public/ into it and adds index.html + assets/). - `vercel.json#outputDirectory` switches from `public` → `dist`. - `turbo.json` outputs updated to match. - `dev` chains pack before vite so the catalog exists when the dev server starts. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>github.com-vercel-workflow · b883ea0d · 2026-05-04
- 1.6ETVAdd native v4 workflow attribute events (#2226) * Add native workflow attribute events * Fix abbreviated attributes docs sample * Document attribute replay ordering for step races * Address native attribute review feedback * Validate before claiming attr_set dedup lock; clearer start() attribute errors - world-local: claim the attr_set correlation lock only after validation, so a validation failure does not permanently mark the correlationId as written and wedge the run in a re-invoke loop on retry - world-postgres: distinguish a concurrently-deleted run from a cap violation when the guarded attributes update matches no rows - core: reject non-string initial attribute values in start() with a clear error instead of a downstream schema failure Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Add attribute edge-case tests across all layers - core: normalizeAttributeChanges unit tests (non-object inputs, FatalError wrapping, key/value/batch limits, boundary lengths, UTF-8 byte counting) - core: start() rejects reserved keys, oversized keys/values, and over-cap initial attribute batches before any write - world-local + world-postgres: per-run cap enforced against existing attributes (upsert-at-cap allowed, removal frees room), oversized values rejected on attr_set, invalid initial attributes rejected on run_created - e2e: validation DX workflow asserting every invalid write throws a catchable FatalError naming the violated rule and limit, with the run staying healthy; start() rejects invalid initial attributes client-side Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Remove accidentally committed local e2e diagnostics artifact Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Bump world-vercel to spec version 4 for native attributes The deployed workflow-server (vercel/workflow-server#469) materializes native attr_set events and accepts initial run attributes, but world-vercel still advertised spec v3 — so start(..., { attributes }) rejected itself client-side ('requires spec version 4') on every Vercel deployment, failing the new e2e seeding test across the prod matrix. New runs are now stamped v4. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Reject duplicate correlated attr_set before materializing in Postgres A redelivered duplicate — including one carrying different changes for the same correlationId — previously re-applied the run attributes update and only then failed the event insert, leaving the snapshot out of sync with the event log. Pre-check the event log for the correlationId before mutating; the unique index still guards the truly-concurrent race, which is idempotent (deterministic replay carries identical changes). Also apply the suggested docs wording for initial attributes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Apply suggestion from @VaguelySerious Signed-off-by: Peter Wielander <mittgfu@gmail.com> * Fail the run on World-rejected attribute writes; un-nest runtime test Two fixes from review: - runtime.test.ts: the pre-existing test "propagates transient step_created failures..." was accidentally nested inside the new attribute-race test, failing the new test ("Calling the test function inside another test function is not allowed") and preventing the old test from running. Restored it verbatim at describe level. - A workflow-body attr_set the World rejects as invalid (e.g. the cumulative per-run attribute cap, which only the World can check) is deterministic: redelivering the orchestrator message replays the same write into the same rejection, wedging the run in redelivery with no terminal event. handleSuspension now wraps such rejections in FatalError, and workflowEntrypoint fails the run with the validation error instead of rejecting the delivery. Transient storage errors still propagate and retry via redelivery. Co-Authored-By: Claude Fable 5 <noreply@anthropic.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> Co-authored-by: Peter Wielander <peter.wielander@vercel.com>github.com-vercel-workflow · ae8d6fee · 2026-06-11
- 1.3ETVfix(docs): repair broken links, fix the link linter, and version-correct v5 Card + edit links (#2391) * fix(docs): repair broken links and make the docs link linter actually validate The docs link linter (docs/scripts/lint.ts) had been silently passing everything since the app moved under app/[lang]/ (#552): the next-validate-link populate key 'docs/[[...slug]]' no longer matched the real route, and the unpopulated [lang] homepage route produced a fallback regex (^\/(.+)$) that matched every href. It also only scanned v4 content. - Rewrite lint.ts to build explicit v4/v5 URL spaces from both fumadocs sources (including cookbook URL variants, app routes, worlds pages, public/ assets, and next.config.ts redirects) and validate each version's content against version-correct render semantics. Also validate frontmatter related/prerequisites references (version-relative) and heading fragments. - Rewrite Card hrefs on v5 pages: the v5 routes rewrote inline markdown links from /docs/... to /v5/docs/... but Card renders its own Link, so Card hrefs escaped to the v4 routes and 404'd for v5-only pages (e.g. /v5/docs/observability linking to /docs/observability/attributes). - Fix all dead content links surfaced by the working linter (56 across v4+v5): nonexistent use-workflow/use-step/start API pages now point at foundations/workflows-and-steps and workflow-api/start, getStepMetadata path corrected, /docs/worlds/local → /worlds/local, dead changelog/ internal references removed or unlinked, retired common-patterns links point at the cookbook, and a dead #returnvalue anchor now targets #returns. - Add an index page for api-reference/workflow-errors (both versions), which was linked from the API reference landing page but had no page. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(docs): add version prefix to 'Edit this page on GitHub' links All "Edit this page on GitHub" links 404'd since the v4/v5 content split (#1948): page.path is relative to the per-version content dir, but EditSource built URLs against docs/content/docs/ without the v4/ or v5/ segment. Add a required version prop, passed from each page route. Incorporates #2120 by Luke Howard (@gldkhoward), rebased onto the v5 route changes from this branch. Fixes #2119. Co-authored-by: Luke Howard <dev@lukehoward.com.au> 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>github.com-vercel-workflow · 3229d206 · 2026-06-12
- 1.1ETVValidate unique workflow step IDs at build time (#2018) * Validate unique step ids at build time * Fall back to file-path IDs for non-exported package files Instead of synthesizing a 'name/dist/<path>@version' specifier (which hardcoded the dist/ output convention), non-exported workspace/node_modules files now return moduleSpecifier: undefined and let the SWC plugin's './{filepath}' fallback produce per-file IDs. This is the same path local app files have always taken and avoids the dist/ assumption flagged in review. The build-time duplicate-ID check stays as the safety net. * Dedupe virtual-entry imports by canonical module identity When both the source and the compiled-dist copies of the same workspace package export end up in discoveredSteps/discoveredWorkflows (e.g. the 'workflow' package's internal/builtins in monorepo dev), they resolve to the same module via esbuild's package resolution. The virtual entry was emitting BOTH 'import "workflow/internal/builtins";' (the built-in preamble) and 'import "../../packages/workflow/src/internal/builtins.ts";' (via the isWorkspaceSourceBackedPackageFile carve-out in createImport), which made the swc plugin transform both copies and generate duplicate step IDs. Track a per-bundle set of emitted module identities (package specifier when reachable, otherwise the file path) and skip files whose identity has already been imported. The steps bundle pre-seeds the set with the built-in steps specifier so workspace step files at that path don't emit a competing relative-path import. * Stop rewriting workspace package /dist/ -> /src/ during Next.js discovery The Next.js deferred builder's `resolveSourceBackedPackagePath` rewrote any discovered `/dist/` path to its `/src/` sibling for workspace packages and for `workflow`/`@workflow/*` tarballs. That made the discovered step file list point at source files while base-builder's esbuild bundle (which builds the workflow VM and step registrations) resolved the same package imports through `pkg.exports` to `/dist/`. The workflow proxy ID — generated from the dist path — didn't match the step bundle's registration ID — generated from the src path — producing "Step function not registered" failures at runtime, most visibly with @workflow/ai's doStreamStep on Vercel and Windows Next.js deployments. App code that imports a package by name should resolve naturally through pkg.exports; the loader has no business reaching into the package's source tree. Drop the rewrite (and the now-unused `resolveCopiedStepImportTargetPath` helper that supported it). Workspace packages are still discovered — that's a separate predicate (`shouldPreferSourceBackedPackagePath`) which only gates inclusion, not path translation. Verified locally with the nextjs-turbopack workbench: agent e2e suite (19 tests, including the failing `agentBasicE2e`) and the addTenWorkflow duplicate-name suite all pass. * Address review nits: extract stripPackageVersion, expand duplicate-ID hint, note new build-time check in changeset --------- Co-authored-by: Nathan Rajlich <n@n8.io> Co-authored-by: Peter Wielander <mittgfu@gmail.com>github.com-vercel-workflow · f5f6d0ed · 2026-06-10
- 1.1ETVRedrive on transient workflow-server transport failures instead of failing the run (#2445) * Redrive on transient workflow-server transport failures instead of failing the run A firewall in front of workflow-server shedding load with sustained 429/503 makes undici's shared RetryAgent exhaust its retries and throw UND_ERR_REQ_RETRY. That raw error was rethrown unwrapped, so it was classified as USER_ERROR and the replay terminal branch wrote run_failed — permanently failing a run on a transient blip (or, in an outage, falling back to the ~5min queue visibility-timeout redrive). - world-vercel: map exhausted-retry / socket / connect / DNS / timeout failures to a typed WorkflowWorldError (code TRANSPORT/TIMEOUT) by walking the fetch() cause chain. - core: add isRetryableWorldError (429 / 5xx / TRANSPORT / TIMEOUT) and rethrow such errors from the replay terminal branch so the queue redrives quickly (1s->60s backoff) instead of failing the run. Reuse it in start() and step_started handling. - world-vercel: surface the Vercel firewall x-vercel-mitigated (challenge/deny) header alongside x-vercel-id in error diagnostics and logs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Address review: back off + cap on every retry path; fix mock; refine scope Revises the transport-error handling per PR review (VaguelySerious, karthikscale3). Blocking fix — step_started no longer self-enqueues a throttled defer for transient world errors. Returning `{ type: 'throttled', timeoutSeconds: 1 }` acked the delivery and enqueued a fresh message, resetting the delivery count so the path never backed off and never reached MAX_QUEUE_DELIVERIES — an unbounded flat-1s loop if step_started kept failing. It now throws, so the error flows through the replay loop's retryable-world-error rethrow and earns both the delivery-count backoff and the max-delivery cap. Throwing is safe on step_started (the body hasn't run; a write that landed dedupes to skipped). Also in this revision: - Backoff that lasts: raise the queue handler-error retry ceiling 60s -> 900s. VQS clamps each redelivery to its 900s SQS limit and adds its own post-32 exponential, so ramping our base toward 900s stretches survival from ~3.7h to most of the 24h message-visibility window. Corrected the stale MAX_QUEUE_DELIVERIES comment to match the real VQS schedule. - Stop amplifying firewall challenges: the undici RetryAgent no longer retries 429 in-process (a challenge is a 429 the client can't solve). 429s surface immediately as ThrottleError carrying x-vercel-mitigated / x-vercel-id, so the diagnostic header now reaches us for the challenge case too. - Track world faults as WORLD_CONTRACT_ERROR (not USER_ERROR) in classifyRunError so an outage isn't attributed to user code. - Fix queue.test.ts mock that `biome check --write` had rewritten from a newable `function` into an arrow (broke `new QueueClient`); pin with a biome-ignore. All Vercel-specific logic stays in @workflow/world-vercel; @workflow/core operates only on the generic WorkflowWorldError abstraction. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Update .changeset/transport-error-redrive.md Signed-off-by: Peter Wielander <mittgfu@gmail.com> * Trim changeset to a single sentence per review Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Route firewall-challenge 429s to the retryable transport path, not ThrottleError A 429 carrying `x-vercel-mitigated: challenge` is a firewall challenge our server-to-server client cannot solve, so it recurs for the life of the incident. Mapping it to `ThrottleError` meant the `step_started` write deferred it as `{ type: 'throttled' }`, which self-enqueues a FRESH queue message and resets the delivery count — so it never backed off past `retryAfter` and never reached `MAX_QUEUE_DELIVERIES`, hot-looping against an already-overloaded firewall (the exact amplification this PR set out to remove, and contrary to the "step_started can't loop unbounded" invariant, which only held for 5xx). Map a challenge to a retryable transport `WorkflowWorldError` (`code: 'TRANSPORT'`) in both the v3 `makeRequest` and v4 `throwForErrorResponse` (the hot event-write path) error mappings, via a shared `isFirewallChallenge429` helper. It then propagates through the V1/V2 step paths and the replay loop's retryable-world-error rethrow, earning the delivery-count backoff AND the delivery cap. A genuine application-level 429 (no `challenge` mitigation) stays a `ThrottleError` and keeps its `Retry-After`-paced defer. Also correct the survival-window comments: with the 900s ceiling, MAX_QUEUE_DELIVERIES=48 spans ~9-10h (~35,000s), not "the better part of 24h"; reaching 24h would need a higher delivery cap, not a higher per-hop ceiling. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Signed-off-by: Peter Wielander <mittgfu@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Peter Wielander <mittgfu@gmail.com> Co-authored-by: Peter Wielander <peter.wielander@vercel.com>github.com-vercel-workflow · 603ad976 · 2026-06-29
- 0.9ETV[codex] Guard event consumers during replay (#2030) * Guard event consumers during replay * Address event guard review feedback * Update event guard test fixturesgithub.com-vercel-workflow · b124365e · 2026-05-20
- 0.9ETVdocs: make /worlds the canonical home for World docs (#2934) * docs: make /worlds the canonical home for World docs The world pages (Local/Postgres/Vercel) and Building a World were duplicated inside the v4 and v5 docs trees while /worlds/[id] rendered the v4 copy — hiding v5-only content like multi-region and leaving two diverging sources of truth. - Move world docs to an unversioned docs/content/worlds/ collection (based on the v5 copies, with inline 4.x callouts for factory naming and 5.x-only env vars), rendered at /worlds/* - Add /worlds/building-a-world; flatten the docs Deploying section to a single intro page and drop its Rocket icon - Point every link, frontmatter ref, and worlds-manifest docs field at /worlds/*; add redirects for the removed v5 and building-a-world URLs - Keep world docs on agent-facing surfaces: search, llms.txt, sitemap.md/.xml, and .md exports now serve the worlds collection - Extend the docs link linter to validate worlds pages (with heading anchors) and their outgoing links Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Pranay Prakash <pranay.gp@gmail.com> * docs: version the world docs like the docs trees (v4/v5 switcher) Instead of a single unversioned copy, world docs now follow the same versioning strategy as the docs pages: content/worlds/v4 is served at /worlds/* (current) and content/worlds/v5 at /v5/worlds/*, restoring the original per-version content. Each world detail page (and Building a World) renders the docs version switcher — the worlds listing page has no natural home for it, so it lives on the world pages themselves. - Render-time href rewriting on v5 pages now covers /worlds/... links (shared rewriteHrefForVersion helper, also used by the v5 docs and cookbook routes), and the markdown-export rewrite does the same - v5 world pages are noindexed with a canonical to /worlds/<id>; community worlds stay unversioned (/v5/worlds/<id> redirects) - /v5/docs/deploying/world/* redirects now land on /v5/worlds/*; /v5/worlds and /v5/worlds/compare redirect to the unversioned pages - Link linter models the versioned worlds URL spaces (v5 pages resolve /worlds hrefs against the v5 collection); sitemap.md and the .md export routes cover /v5/worlds/* Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Pranay Prakash <pranay.gp@gmail.com> * docs: fix v4 multi-region anchor and tighten version-prefix matching Address PR review: - The v4 Deploying page linked /worlds/vercel#multi-region, but the Multi-region section only exists on the v5 world page; use the explicit cross-version /v5/worlds/vercel#multi-region link (this was the Docs Links CI failure) - rewriteHrefForVersion now uses the boundary-checked hasPathPrefix (shared leaf module lib/geistdocs/path-prefix.ts, also used by source.ts) instead of bare startsWith - buildVersionUrl's shared-route fast path is segment-based rather than substring includes() Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Pranay Prakash <pranay.gp@gmail.com> --------- Signed-off-by: Pranay Prakash <pranay.gp@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>github.com-vercel-workflow · 8a872529 · 2026-07-16
- 0.9ETVdocs: document run idempotency (#2011) * docs: document run idempotency * docs: address idempotency review feedback * docs: make hook tokens the idempotency pattern * docs: address toolbar idempotency feedback * docs: clarify idempotency page description * docs: scope idempotency descriptions * docs: move step idempotency example under section * docs: simplify idempotency guidance * docs: simplify idempotency cookbook * docs: add empty changeset Signed-off-by: Nathan Rajlich <n@n8.io> * docs: address idempotency review feedback * feat: add hook ready promise * docs: mention conflicting hook run id * test: cover hook ready continuation scheduling * feat: replace hook.ready with hook.hasConflict (Promise<boolean>) - hook.hasConflict resolves true when the token is owned by another active hook, false once registration is committed — no throw, so workflows can branch on conflicts early. Awaiting it suspends the workflow to commit the hook registration (createHook alone does not). - Chain the already-created fast-path through promiseQueue so resolution order matches event-log order (review feedback). - Skip inline step execution when a suspension has an awaited hook creation so the hasConflict continuation can advance independently of step execution (review feedback). - Update unit tests, e2e tests, workbench workflows, and v4/v5 docs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: fix inconsistent hasConflict bullet in create-webhook reference State both resolution values explicitly (true = token already owned, false = registered) instead of a parenthetical that only described the false case. * docs: require docs preview links in PR descriptions for docs changes Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: restore SWC Plugin heading in AGENTS.md Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: adopt hook.hasConflict in run idempotency docs - Primary claim pattern is now `if (await hook.hasConflict)` instead of try/catch on HookConflictError; payload awaits still reject with HookConflictError (with conflictingRunId) when the owner's run ID is needed. - Route example returns the active owner via resumeHook()'s runId instead of threading conflictingRunId through the workflow result. - Update claim-pattern prose across start(), getHookByToken(), world storage, scheduling, workflow composition, and cookbook idempotency pages (v4 + v5). - Add @skip-typecheck marker to the cross-block route sample, fixing a pre-existing docs typecheck failure. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: move resume-or-start guidance into a dedicated resumeHook example The early callout was too vague and out of place at the top of the API reference. Replace it with a 'Resume or Start' example section that explains the flow, shows the resume-first/start-then-retry route, and links to the run idempotency pattern. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: detect the concurrent-start race via runId comparison instead of awaiting returnValue The 'Resume or Start' example returned the just-started run's runId with reused: false even when a concurrent request's run won the token race — the payload had reached the actual owner, so the response pointed callers at a run that exits as a duplicate. The foundations route handled the race correctly but by awaiting run.returnValue, blocking the HTTP response on full workflow completion. resumeHook() always resolves against the actual active owner, so comparing the resumed hook's runId with the started run's runId detects the race in both examples — race-correct and non-blocking. * feat: replace hook.hasConflict with hook.getConflict (Promise<Run | null>) hasConflict's boolean didn't expose WHICH run owns the token, so the duplicate run couldn't act on the conflict. getConflict resolves with null once registration commits, or with a Run handle for the conflicting run — letting the workflow return/log the owner's runId, inspect its status, await its result, or cancel it and continue, all in code. The workflow-mode create-hook module exposes the bundle's compiled Run class (durable step-proxy methods) on a well-known symbol so the host- side hook consumer can construct the conflicting run inside the VM. Contexts without the class (plain unit tests) fall back to a { runId } object, which is also the documented v4 shape (no native Run serialization in v4). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: adopt hook.getConflict and add conflict-handling strategy guide Run idempotency docs now use getConflict (resolves with the conflicting Run in v5, { runId } in v4) and document code-driven conflict strategies in place of static ID-reuse policies: reject the duplicate, adopt the owner's result, inspect before deciding, signal the owner via resumeHook, and supersede via cancel-and-reclaim. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: never resolve getConflict with a non-Run fallback shape getConflict's contract is Promise<Run | null>. In the degenerate cases where a real Run cannot be constructed — a hook_conflict event persisted by an old world without conflictingRunId, or a context that never loaded the workflow-mode create-hook module — reject with HookConflictError instead of resolving with a { runId }-shaped impostor. Test harnesses now register the Run class on the (VM) globalThis like real bundles do. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor: make getConflict a method — hook.getConflict() A property getter that triggers registration/suspension reads as passive state; a method makes the side effect explicit. Update implementation, types, tests, e2e workflows, docs, and changeset. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: getConflict is a method — hook.getConflict() Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: typecheck every sample — drop skip-typecheck escape hatches Route examples typecheck as-is since the runId-comparison rewrite; strategy fragments are now complete self-contained workflows; the publishing-libraries cross-block dependency uses the declare @setup convention. 934 samples typechecked, none skipped by this PR. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * review: guard Run class registration, fix anchors, clarify changeset - Only register WORKFLOW_RUN_CLASS when the workflow runtime is present (WORKFLOW_CREATE_HOOK installed on globalThis), so host imports of the workflow-mode module neither mutate the host global nor expose the non-step-proxy host Run. - Drop #run-idempotency link fragments — that section lands in the stacked docs PR (#2011), which restores the anchored links. - Note in docs that getConflict() rejects with HookConflictError for legacy hook_conflict events lacking the owner's run ID. - Changeset now calls out the hasConflict -> getConflict() replacement. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: restore run-idempotency anchors now that the section exists here Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: describe fixed conflict policies generically, without naming other systems Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Signed-off-by: Nathan Rajlich <n@n8.io> Co-authored-by: Nathan Rajlich <n@n8.io> Co-authored-by: Peter Wielander <mittgfu@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>github.com-vercel-workflow · 5dbeecbb · 2026-06-14
- 0.9ETVperf(core): lazy inline step start (save one world round-trip per step) (#2478) * perf(core): lazy inline step start to save a world round-trip per step The owned-inline runtime path used to write step_created (suspension handler) and then step_started (executeStep) as two separate world round-trips for a step it already owns and is about to run inline. This defers the step_created write: executeStep sends a single step_started carrying the step input, and the world creates the step on the fly (materializing the step entity plus a synthetic step_created event so replay still observes it). Mirrors the existing resilient run_started -> run_created pattern. Exactly-one ownership is preserved by the world's atomic create-claim: the loser of a concurrent lazy step_started gets EntityConflictError, which executeStep maps to `skipped`, so it never runs the body. A lazy step_started is only ever sent for a brand-new step (the suspension handler defers only steps with no prior step_created), so crash recovery still re-runs a `running` step via the normal non-lazy step_started. Worlds updated: world-local, world-postgres (implicit create + synthetic step_created event), world-vercel (routes the input as the v4 frame payload and threads the server's stepCreated flag). @workflow/world adds optional `input` to step_started and a `stepCreated` EventResult signal. Rollout: server-first. The matching workflow-server change must deploy before this ships; the Vercel world targets a single Vercel-operated backend (server always >= SDK). For local/postgres the world ships in the same package as the runtime, so there is no version skew. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(core): materialize deferred step before failing unregistered step on lazy inline path The lazy inline step-start optimization defers a step's step_created write, expecting executeStep to materialize the step via a lazy step_started carrying its input. For an UNREGISTERED step, executeStep bails out before sending that step_started and writes step_failed directly — but the step entity was never created, so the world's "step must exist" ordering guard rejects the step_failed and the run wedges (times out). This regressed the StepNotRegisteredError e2e tests uniformly across every framework/world (the ghost step never reached `failed`). Fix: on the lazy path, send the lazy step_started first to materialize the step (entity + synthetic step_created, keeping replay correct), then write step_failed. The lazy step_started's atomic create-claim preserves exactly-one-owner: a concurrent winner makes ours reject with EntityConflictError → skipped, so the failure is never written twice. Adds world-level regression tests (world-local, world-postgres) asserting a lazy step_started followed by step_failed marks the step failed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>github.com-vercel-workflow · e7ef9d82 · 2026-06-18
- 0.8ETVfix(core): resolve forwarded stream keys across deployments (#2191)github.com-vercel-workflow · 8f68d352 · 2026-06-01
- 0.7ETVdocs: replace migration guides with a Comparisons section (#2676) * docs: replace migration guides with a Comparisons section Add a Comparisons section (v4 + v5) with an index/snapshot across all frameworks and deep-dive pages for Temporal, Cloudflare Workflows, AWS Step Functions, AWS Bedrock AgentCore, Inngest, and trigger.dev. Remove the old migration-guides section, folding its concept-mapping and migration content into the relevant comparison pages, and repoint top-level nav in both versions. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs: refresh comparison pages with current facts (July 2026) Re-verified each comparison against the vendor's current public docs and updated what changed since the June 2026 snapshot: - Temporal: Worker Versioning is now GA; Serverless Workers (AWS Lambda, pre-release) scale to zero, so soften the blanket "no scale-to-zero"; drop the unsubstantiated "Uber" customer claim (Uber is Cadence's origin, not a Temporal customer). - Cloudflare Workflows: note the new per-step billing dimension (500K/mo included, then $0.80/100K) landing no earlier than Aug 10, 2026; note the 50K concurrency ceiling was raised from 4,500 at GA. - AWS Bedrock AgentCore: add newer GA modules (Harness, Policy, Evaluations); correct compliance (SOC/PCI/ISO under internal assessment, audits pending; FedRAMP not yet authorized; drop GovCloud claim); refresh languages (@aws/agentcore CLI scaffolds TS or Python); "some modules preview" is stale. - Inngest: Pro pricing $75 -> $99/mo; encryption middleware now TS + Python; AgentKit/Realtime are Developer Preview and Connect is public beta; self-host is community/best-effort (not "unsupported"); Free-tier run duration 30 days vs 366 on Pro; soften funding to ~$30M+. - AWS Step Functions & trigger.dev: facts re-confirmed; date stamp only. Bumped every "as of June 2026" stamp to July 2026. v4 and v5 kept identical. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: present comparisons in the present tense; v5-only; Pro usage-based pricing Follow-up pass on the comparison pages: - Remove all date references (past and future). Anything that lands on a date is stated as already in effect: Cloudflare's per-step billing, Temporal Serverless Workers and GA Worker Versioning, AgentCore's Harness/Policy/ Evaluations modules. Dropped "as of July 2026" stamps, founding/GA years, funding round dates, and roadmap/"being added" phrasing. - Workflow SDK: reference v5 only and treat it as GA (was "v4 GA / v5 beta"). - Pricing and limits: quote the Pro/paid tier only and usage-based rates only; drop plan-included quotas and free-tier allowances (Step Functions 4K/mo free, Cloudflare 500K steps/mo included, Inngest 50K free execs, Inngest/Free 30-day run cap, Temporal $100/mo plan minimum). v4 and v5 kept identical. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: tighten comparison maturity/status wording - Drop dateless "upcoming change" phrasing: AgentCore compliance now states current facts only (no "self-assessed"/audit-pending implication); remove Inngest's "SSPL → Apache after 3 yrs" license-conversion note. - Don't label the Workflow SDK "GA" — non-beta is the default; also drop bare "GA" where it only meant "not beta" (Temporal "7 SDKs", Inngest "TypeScript", competitor maturity cells). - Maturity cells no longer cite version numbers; they describe backing/track record instead (e.g. "Built and maintained by Vercel", "Backed by AWS"). v4 and v5 kept identical. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: make "features without a 1:1 equivalent" sections directional Rename each heading to name the competitor that has the feature (e.g. "Temporal features without a direct Workflow SDK equivalent") and add a lead-in clarifying these are the competitor's capabilities the Workflow SDK doesn't replicate one-to-one, with how to cover each on the Workflow SDK side. v4 and v5 kept identical. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: point world links at the /worlds routes The comparison pages linked to /docs/deploying/world/* and /docs/deploying/building-a-world, which no longer exist in the docs trees (the Docs Links check rejects them on v5 pages, where /docs hrefs are render-rewritten and skip the legacy redirects). Link the canonical /worlds/* routes directly, in both body links and frontmatter refs. v4 and v5 kept identical. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: address toolbar review feedback on the comparison pages - Drop the Maturity row from every at-a-glance table - Add a "what the limits mean in practice" paragraph to each comparison, spelling out what the competitor's caps mean for long-running AI workloads, and link Vercel World limits to the pricing doc - Security cells: lead with zero-config per-run E2E encryption and note platform security is per-World, instead of the VM-sandbox framing - Temporal: drop the throughput sentence and the still-in-preview Serverless Workers mention from the performance cell - Cloudflare: end the recommendation on "already all-in on Cloudflare" - Convert the "features without a direct equivalent" bullet lists into two-column tables so it's unambiguous which product owns each feature - Fix the Inngest page's "no step cap" cell (Vercel World caps runs at 10K steps per the pricing doc) v4 and v5 kept identical. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Peter Wielander <peter.wielander@vercel.com>github.com-vercel-workflow · 918a2c55 · 2026-07-21
- 0.7ETVAdd `hook.hasConflict` for early hook conflict detection (#2015) * feat: add hook ready promise * test: cover hook ready continuation scheduling * feat: replace hook.ready with hook.hasConflict (Promise<boolean>) - hook.hasConflict resolves true when the token is owned by another active hook, false once registration is committed — no throw, so workflows can branch on conflicts early. Awaiting it suspends the workflow to commit the hook registration (createHook alone does not). - Chain the already-created fast-path through promiseQueue so resolution order matches event-log order (review feedback). - Skip inline step execution when a suspension has an awaited hook creation so the hasConflict continuation can advance independently of step execution (review feedback). - Update unit tests, e2e tests, workbench workflows, and v4/v5 docs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: fix inconsistent hasConflict bullet in create-webhook reference State both resolution values explicitly (true = token already owned, false = registered) instead of a parenthetical that only described the false case. * docs: require docs preview links in PR descriptions for docs changes Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: restore SWC Plugin heading in AGENTS.md Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Nathan Rajlich <n@n8.io>github.com-vercel-workflow · e1634225 · 2026-06-11
- 0.6ETVfix(core): make deploymentId 'latest' a no-op in non-Vercel worlds (#2397) * fix(core): make deploymentId 'latest' a no-op in non-Vercel worlds Previously, start({ deploymentId: 'latest' }) threw a WorkflowRuntimeError in any World that doesn't implement resolveLatestDeploymentId() (local dev, Postgres). That meant a workflow which opts into 'latest' on Vercel would fail outright in local development. Resolving 'latest' only means something in worlds with atomic, immutable deployments. In other worlds there is nothing to resolve between, so instead of throwing we now log a warning and fall back to the current deployment, making 'latest' an effective no-op there. - start.ts: warn + fall back to currentDeploymentId instead of throwing - start.test.ts: replace the "should throw" test with a warn + fallback test - e2e.test.ts: assert 'latest' completes (no-op) on non-Vercel worlds - docs: note the no-op behavior in v4 + v5 start.mdx Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(core): warn once for deploymentId 'latest' no-op; harden test cleanup Address PR review: - Gate the 'latest'-has-no-effect warning behind a once-per-process guard (mirrors the warnOnce pattern in constants.ts) so a workflow that hardcodes 'latest' for Vercel doesn't flood local/Postgres dev logs on every run. Exposes _resetLatestNoOpWarnForTests() (@internal) for unit tests. - start.test.ts: reset the guard in beforeEach and restore spies in afterEach via vi.restoreAllMocks() so a throwing assertion can't leak the runtimeLogger.warn spy into later tests; drop the manual mockRestore(). - Add a test asserting the warning fires exactly once across repeated 'latest' starts while every run still falls back to the current deployment. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>github.com-vercel-workflow · 4b7a7203 · 2026-06-16
- 0.6ETV[world-local] Reduce sequential replay I/O (#2152) * [world-local] Reduce sequential replay I/O * Fix relative local event cache lookups * Keep event cache eviction test lightweight --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>github.com-vercel-workflow · fc5bdcb0 · 2026-06-30
- 0.6ETVperf(core): skip per-step events.list via inline event-log delta (#2475) * perf(core): skip per-step events.list via inline event-log delta In the inline sequential loop, the runtime re-read its own just-written step events with an incremental events.list every iteration — pure latency on the Vercel world. Add an opt-in CreateEventParams.sinceCursor so a step-terminal write can return the event-log delta since that cursor (EventResult.events/cursor/hasMore), and have the inline loop consume it in place of the fetch. The delta is computed identically to events.list against the same log, so the consumed prefix is byte-for-byte what a fetch would return. The fast path is gated conservatively to the single-step sequential case with no open hooks/waits (so no out-of-band hook_received/wait_completed can land in the snapshot→replay window), and falls back to the normal fetch on any World that does not return a delta. world-local implements the delta; world-vercel/world-postgres are unchanged and fall back. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * perf(world-vercel): forward sinceCursor over the v4 wire for inline delta Adds `sinceCursor` to the v4 POST frame meta so a step-terminal write can ask the server for the authoritative event-log delta on the response (events/cursor/hasMore), letting the inline loop skip a follow-up events.list. The server-side computation ships in vercel/workflow-server#538; older servers ignore the field and the runtime falls back to events.list (no behavior change). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(inline-delta): cover truncated multi-page delta -> hasMore fallback The inline-delta query in world-local intentionally omits a `limit`, so a delta larger than one page is truncated and reports `hasMore: true`. The runtime consume gate only stashes a delta when `!hasMore` and otherwise falls back to the exhaustive `events.list` loop, so a partial page can never be consumed as the complete delta. Make that contract explicit with a comment at the query site, and add tests pinning it: a world-local test proving the delta truncates and surfaces `hasMore: true` byte-identically to `events.list(sinceCursor)`, and an executeStep test proving `hasMore: true` is threaded verbatim. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(world): clarify sinceCursor returns the first delta page, not the full set The CreateEventParams.sinceCursor docstring said the result is "exactly the delta an events.list(...) call would return," which read as the full set. It is the first page of that delta; hasMore signals more. Spell out the single-page-or-fallback contract so other World adapters implement sinceCursor consistently, and note that an in-band burst larger than one page bypasses the fast path (correct, but forgoes the saved round-trip). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>github.com-vercel-workflow · 2074f91b · 2026-06-18
- 0.6ETVRetry replay divergence before failing event logs (#2212) (cherry picked from commit 813cd9a9de0592f9660c0384c1d49e54be7b1dcb)github.com-vercel-workflow · 2a3b11bc · 2026-06-02
- 0.6ETVfix(world-vercel): retry idempotent event POSTs in-process to avoid step re-execution (#2675) * fix(world-vercel): retry idempotent event POSTs in-process to avoid step re-execution undici's RetryAgent never retries a POST, so a transient transport blip (UND_ERR_REQ_RETRY, ECONNRESET, socket/headers timeout, transient 5xx) when committing a step's terminal event bubbles out, the queue redelivers, and the step's user code re-executes with attempt++ even though it already ran to completion. workflow-server makes these writes idempotent in outcome: entity handlers run before the event-log row is inserted and state transitions are conditional writes excluding terminal states, so a retry whose original landed throws before any row is written and surfaces as a 409 the SDK already handles. This adds a bounded in-process retry (new event-retry.ts) gated by a validated per-event EVENT_RETRY_ELIGIBILITY map, excluding step_started (double-increments attempt), step_retrying (appends a duplicate row), and hook_received (no server guard). Complements #2666, which routes completion-persistence failures to queue redelivery instead of recording them as user failures; this avoids the redelivery (and re-execution) for transient blips. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(world-vercel): address review on event-POST retry - Don't retry external cancellation: drop AbortError from the transient set (keep self-timeout TimeoutError), so a caller-requested abort isn't re-issued or stalled by the backoff budget. - Add DEBUG-gated logging on each retry and on retry exhaustion so an in-process retry vs. a fall-through to queue redelivery is distinguishable in logs. - Clarify docs: a landed retry surfaces as 409 for most types, but run_started/attr_set return 200 success (not a 409). - Add createWorkflowRunEvent integration tests (events-retry.test.ts): eventType is threaded into the retry wrapper, and the 404->HookNotFoundError mapping still fires after the retry loop. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Peter Wielander <mittgfu@gmail.com>github.com-vercel-workflow · 897aac97 · 2026-06-28