Karthik Kalyan
90d · built 2026-07-24
90-day totals
- Commits
- 51
- Grow
- 7.4
- Maintenance
- 4.5
- Fixes
- 3.0
- Total ETV
- 14.8
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).
↑+43.8 %
vs 16 prior
↑+2.7 pp
recent vs prior
↑+16.2 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.
- 1.6ETVFix compressed workflow error display (#2680) * Fix compressed workflow data display * Add OSS web no-key hydration regression * Scope compression normalization to read paths; tidy hydration Address review feedback on the compressed-data fix: - world-vercel: keep gzip/zstd decompression on the o11y/display read paths (getStep/getRun/getEvent/getWorkflowRunEvents/getHook) but not on the runtime event-append path (world.events.create, createStep, updateStep). That path is runtime-only and re-hydrates every payload via the decompress-aware helpers, so decompressing at the adapter was redundant work on the TTFB-sensitive run_started/inline-delta path and skewed the runtime's deserialize compression telemetry to `codec: none`. deserializeStep is now shape-only; normalizeStepData runs in the read filter. Adds a regression test pinning the write-path pass-through. - serialized-data: drop dead `errorRef`/`metadataRef` normalization (refs are descriptor objects, never compressed byte payloads). - web: in the wait-entity path, filter events by correlationId before hydrating so an encryption key doesn't decrypt the whole event page. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Peter Wielander <peter.wielander@vercel.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>github.com-vercel-workflow · 89f4726b · 2026-06-29
- 1.1ETV[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>github.com-vercel-workflow · fe327e69 · 2026-07-08
- 1.0ETVotel: explicit traceparent injection + linked-trace mode for bounded per-invocation traces (#2363) * otel: explicit traceparent injection + linked-trace mode for bounded per-invocation traces - Add WORKFLOW_TRACE_MODE ('linked' default, 'continuous' legacy) to the workflow and step queue handlers. In linked mode, WORKFLOW_V2/STEP spans start a new trace root with span links to the incoming delivery context and the run-origin context, and re-enqueued messages forward the ORIGINAL run-origin trace carrier unchanged. - world-vercel now explicitly injects W3C traceparent/tracestate/baggage headers on outgoing workflow-server HTTP requests from inside the client span (no-op without an OTEL SDK registered). - New workflow.trace.mode span attribute; unit tests for both modes and for header injection. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * changeset: call out behavioral telemetry changes of the linked default Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: add v5 observability tracing page Documents OTEL spans/attributes, linked trace mode and WORKFLOW_TRACE_MODE, span links, context propagation, and the v4 behavior-change callout. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * otel: human-friendly span names for workflow and step spans WORKFLOW_V2/STEP prefixes with full machine names (workflow//./src/...//fn) become workflow.execute / step.execute / workflow.start with the short function name. New workflowDisplayName/stepDisplayName helpers in @workflow/utils handle both raw and queue-sanitized name forms; full names remain in the workflow.name/step.name attributes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * changeset: merge span-name and linked-trace notes into one changeset Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: update trace-shape prose to renamed span names Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: replace ascii trace diagram with mermaid Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * address review: empty carriers, shared trace helpers, mode warning, name edge cases, consumer span kind - Treat an empty ({}) trace carrier as absent everywhere the trace-mode logic branches, so linked mode falls back to a fresh origin instead of forwarding a useless {} forever; workflow.trace.propagated now reports whether a usable carrier arrived. - Extract the duplicated linked-mode logic into shared telemetry helpers getNextTraceCarrier() and buildInvocationSpanLinks(), used by both the workflow and step queue handlers; resume-hook now uses linkToTraceCarrier (gaining the isSpanContextValid guard). - Warn once per distinct unrecognized WORKFLOW_TRACE_MODE value instead of silently selecting linked. - shortNameFromSanitized: map default/__default to the module short name (mirroring parseName) and document the `$`-sanitization limitation. - Queue-delivered workflow.execute spans now use the CONSUMER span kind, matching queue-delivered step.execute spans; docs span table and changeset updated accordingly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>github.com-vercel-workflow · 926a5e7c · 2026-06-15
- 1.0ETVcli: 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>github.com-vercel-workflow · 17d4ce22 · 2026-07-07
- 0.7ETVUse and show event occurredAt (#2613)github.com-vercel-workflow · 148d4743 · 2026-06-27
- 0.6ETVtelemetry: move client stream spans from world-vercel to core (#2901) * fix(deps): dedupe @opentelemetry/api to a single workspace instance The lockfile resolved both 1.9.0 and 1.9.1, so the copy that registers the tracer provider (via @vercel/otel in the app) and the copy a package imports could differ. The API's global-registration version check rejects a consumer newer than the registered copy and silently hands back a noop tracer — which is why world-vercel's spans (workflow.stream.write/ chunk_rtt, read.connect, its http spans) never reached Datadog from deployed apps while core's spans flowed in the same process. Root-caused via the DEBUG=workflow:* run on #2900: import succeeds, no warn, spans dropped. Pin a single version via a workspace override so every bundle shares one API instance. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * debug: one-shot OTEL runtime diagnostic in core + world-vercel; DEBUG on turbopack workbench The dedupe alone did not restore world-vercel span emission (verified on this PR's own preview: stream traffic flowed, zero workflow.stream.write spans). Under DEBUG=workflow:*, both packages now log once how their module instance of @opentelemetry/api sees the world — global registration version, provider/delegate/tracer/probe constructor names, and whether a probe span is recording. Diffing the core line (spans work) against the world-vercel line (spans dropped) in one deployment's logs pinpoints the divergence. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * debug: log span identity for named world-vercel spans; namespace otel probes per package Diag round 1 showed world-vercel's tracer records and instrumentedFetch handles the stream PUTs, yet the named spans are unfindable in the backend. Round 2: log traceId/spanId/isRecording for every named instrumentedFetch span under DEBUG so export can be checked for a specific span id, and split the probe span names (.core / .world_vercel) so per-package export is attributable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * telemetry: emit stream RPC latencies from core (chunk_rtt, connect_ms, close span) world-vercel's instrumentedFetch spans never export from deployed apps (root cause still open — see PR discussion), so the operationally needed client-side latency signals move one layer up to core, whose spans are proven to export: - workflow.stream.write.chunk_rtt on the workflow.stream.flush span: the World write RPC duration, network included (same attribute key as world-vercel's per-request span so queries are layer-agnostic). - workflow.stream.read.connect_ms on the workflow.stream.read span: the world.streams.get await (read dispatch -> stream handle). - new workflow.stream.close span: the close RPC round trip. Bonus: measured at the World interface, these cover world-local and world-postgres too, not just Vercel deployments. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * telemetry: emit read-completion span (total duration, chunks, bytes) Completes the read-side picture: workflow.stream.read.complete is back-dated to the read dispatch so its duration is the total read, with chunk/byte counts for throughput. Cancelled reads emit nothing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: drop DEBUG from turbopack workbench; tighten changeset Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * telemetry: cover createReconnectingFramedStream in read telemetry Ordinary serialized streams read through createReconnectingFramedStream (which calls world.streams.get directly), so connect_ms / ttfc / read.complete never fired for that path — only WorkflowServerReadableStream was instrumented. Wire the same helpers into the framed reader: first- connect duration, first-frame TTFC, and completion totals — plus workflow.stream.read.reconnects, which only this path can know. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>github.com-vercel-workflow · 9242ddb0 · 2026-07-14
- 0.6ETVweb: read observability list views from world.analytics when available (#2647) * Add workflow analytics world APIs * web: read observability list views from world.analytics when available Route the runs/steps/events/hooks list server actions through the optional world.analytics namespace when the backend provides one, falling back to the runtime storage APIs otherwise. Events listing only uses the analytics path when no payload data is requested. Detail/get actions, streams, and mutations are unchanged. * web: keep events and hooks list reads on the runtime storage API The Events tab and trace viewer derive step names and wait resumeAt from resolved event payloads, and the hooks table needs the secret token and ownerId for its resume/copy-token actions. The metadata-only analytics rows do not carry these, so only the runs and steps list views use world.analytics. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * web: read events list from world.analytics with a runtime-shape remap The events list/trace consumers read only top-level eventType, correlationId, and createdAt; event payloads are loaded lazily per event via fetchEvent(..., 'all') on the runtime path. Map the flat analytics event rows into the runtime Event shape (reconstructing eventData.stepName) so fetchEvents and fetchEventsByCorrelationId can use the analytics read path when available. Hooks remain on the runtime path (they need the secret token + ownerId). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Handle analytics access metadata in web --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>github.com-vercel-workflow · 1518c486 · 2026-07-07
- 0.5ETVStamp run IDs on world spans (#2508) * Stamp run IDs on world spans * Apply suggestions from code review Co-authored-by: Peter Wielander <mittgfu@gmail.com> Signed-off-by: Karthik Kalyan <105607645+karthikscale3@users.noreply.github.com> --------- Signed-off-by: Karthik Kalyan <105607645+karthikscale3@users.noreply.github.com> Co-authored-by: Peter Wielander <mittgfu@gmail.com>github.com-vercel-workflow · 1332da3d · 2026-06-18
- 0.5ETVDecode UTF-8 stream chunks (#1852) * Decode typed array stream chunks * Render decoded stream bytes with raw view * Render decoded bytes in data inspector * Use generic byte inspector for streams * review feedback: narrow stream-display exports, fix tab a11y, add collapseRefs tests - Remove unused formatStreamChunkForDisplay/sanitizeStreamChunkForDisplay exports; keep only the formatArrayBufferViewForDisplay path actually used by DataInspector. - Replace broken role=tablist/role=tab on the Decoded/Bytes switcher with aria-pressed toggle-button semantics. - Export collapseRefs/isBytesDisplay and add regression tests covering typed-array detection (top-level, nested in object/array/Map/Set, DataView exclusion). * Replace eval with JSON.parse in serialization revive helper (#1848) * Replace eval with JSON.parse in serialization revive helper devalue.stringify() always produces valid JSON — special values (undefined, NaN, Infinity, -0) are encoded as negative integer sentinels. JSON.parse yields the same flattened array form that unflatten() expects, without the eval anti-pattern (VULN-918). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Drop redundant workflow package from changeset Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> * Add e2e test for UTF-8 parseable stream chunks Emits Uint8Array chunks containing multi-byte UTF-8 (Latin Extended, CJK, emoji, RTL Arabic) plus a UTF-8 encoded JSON document, and asserts each chunk round-trips through TextDecoder({ fatal: true }). Exercises the same decode path the web inspector relies on for typed-array stream values. Made-with: Cursor --------- Co-authored-by: Pranay Prakash <pranay.gp@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>github.com-vercel-workflow · 9ea12542 · 2026-04-29
- 0.5ETVdocs: split v4/v5 content trees and fix version switcher end-to-end (#1948) * docs: split v4/v5 content, fix version switcher end-to-end ## Content restructuring - Split `docs/content/docs/` into `docs/content/docs/v4/` and `docs/content/docs/v5/` so each version is a fully independent content tree with no shared-file coupling - v4 excludes the four pages that are v5-only (AbortController cancellation docs and the serializable-abort-controller internal page) - v5 retains all pages; `preRelease` frontmatter field removed (no longer needed now that each version is its own folder) - Removed `AbortController` / `AbortSignal` from v4 serialization page (section moved to v5 only) ## Fumadocs source - Added `v4docs` and `v5docs` as separate `defineDocs()` collections in `source.config.ts`; shared `docsSchema` (no more `preRelease` field) - `source.ts` exports both `source` (v4, `baseUrl: /docs`) and `v5Source` (v5, same base URL) ## Version routing - `version-source.ts` simplified: `filterPreReleaseFromNodes` and `isPreReleaseUrl` logic removed; v4 tree uses `source`, v5 tree uses `v5Source` + `rewriteNodeUrls` - v4 `page.tsx`: removed `preRelease` guard (v4Source has no such pages) - v5 `page.tsx`: uses `v5Source` for `getPage` / `generateStaticParams` / `generateMetadata`; `v5Link` wrapper rewrites `/docs/…` hrefs to `/v5/docs/…` so inline MDX links stay in the v5 context ## Versioned cookbook - Added `app/[lang]/v5/cookbook/` layout + page (mirrors v4 but uses `v5Source`, `rewriteCookbookUrlForVersion`, and `V5CookbookLink`) - `getCookbookTree` accepts a `versionPrefix` parameter; sidebar URLs are prefixed accordingly (`/v5/cookbook/…`) - `cookbook-tree.ts`: added `skipVersions?: string[]` per-recipe field for version-specific exclusions; `distributed-abort-controller` is marked `skipVersions: ['v5']` ## Version switcher — state & navigation - New `VersionProvider` context (`hooks/geistdocs/use-version.tsx`) backed by `localStorage`: URL is source of truth on versioned pages, `localStorage` carries the preference across non-versioned pages (cookbook overview, worlds, etc.) - `VersionSwitcher` uses `useVersion()` context instead of URL-only detection; now visible on all pages including cookbook - `DesktopMenu` and `MobileMenu` use `activeVersion` from context so the "Docs" and "Cookbook" navbar links resolve to the correct version prefix on every page - `buildVersionUrl` expanded to handle `/cookbook/…` paths alongside `/docs/…`; non-versioned routes (worlds, api) return unchanged - `switchVersion` does a `HEAD` probe before navigating; falls back to the versioned cookbook or docs home if the target page doesn't exist in that version (handles v4-only → v5 and v5-only → v4 cases) ## Cookbook content (v5) - Rewrote `agent-cancellation` recipe using a single `AbortController` pattern; removed Hard Cancellation vs Stop Signal two-approach comparison - Deleted `distributed-abort-controller` recipe from v5 (native `AbortController` serialization makes it unnecessary) - Removed references to distributed-abort-controller from `cookbook/index.mdx` and `common-patterns/timeouts.mdx` Co-authored-by: Cursor <cursoragent@cursor.com> * fix(docs): use abortSignal (not signal) in DurableAgent.stream() options Co-authored-by: Cursor <cursoragent@cursor.com> * fix(docs): update prepack scripts to use versioned content paths Content moved from docs/content/docs/ to docs/content/docs/v5/ on main (pre-release channel). Stable branch will use v4/ after backport. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>github.com-vercel-workflow · ea16d045 · 2026-05-06
- 0.4ETV[core] Thread queue namespace through o11y run actions and bound healthCheck stream reads (#2874) * [core] Thread queue namespace through o11y run actions and bound healthCheck stream reads Deployments that use a queue namespace (e.g. eve's __eve_wkf_workflow_* topics) could not be targeted by cross-context callers: start(), recreateRunFromExisting(), reenqueueRun(), and wakeUpRun() always built queue names from the caller's WORKFLOW_QUEUE_NAMESPACE env, so dashboard replays published to topics the target deployment has no consumer for. Additionally, healthCheck()'s poll loop only checked its timeout between iterations while world.streams.get() itself was unbounded — against workflow-server, which holds unwritten streams open for ~2 minutes, a 2s capability probe hung until the caller's function timed out (the observed 30s 504s on dashboard replay). - Add a `namespace` option to StartOptionsBase, RecreateRunOptions, StopSleepOptions, and new ReenqueueRunOptions; thread it into getWorkflowQueueName() and the cross-deployment capability probe. - Fold the inline `namespace` param into HealthCheckOptions. - Race streams.get() against the remaining health-check budget. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Shorten changeset Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>github.com-vercel-workflow · 48fcc4ef · 2026-07-10
- 0.4ETVfeat(world-vercel): client-observed stream write/read e2e latency spans (#2857) * feat(world-vercel): name stream client spans + add stream attributes Stream write/read requests already share the instrumented HTTP envelope (a CLIENT span + W3C trace-context injection), but the spans were named for the bare HTTP verb (`http PUT`/`http GET`) and carried only generic HTTP attributes — so stream latency couldn't be sliced per run/stream. Name these spans for their operation (`workflow.stream.write` / `workflow.stream.read`) and tag them with `workflow.run.id`, `workflow.stream.name`, `workflow.stream.operation` (write | write_multi | close | read), and `workflow.stream.start_index` (read). Implemented via new optional `spanName`/`attributes` fields on `instrumentedFetch`, so other callers are unaffected. Additive OTEL only: no behavior change when no OpenTelemetry SDK is registered (the span is undefined and attributes are dropped). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Shorten changeset summary Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Add client-observed end-to-end read TTFC span The read GET can't report a client-measured latency back to the server (the value only exists after the response starts streaming), so capture it purely in the SDK's own OTEL: watch response.body for the first non-empty chunk and emit a workflow.stream.read span back-dated to read dispatch, whose duration is the end-to-end time-to-first-chunk (incl. the network hop) via workflow.stream.read.ttfc_ms. Rename the fetch/connect span to workflow.stream.read.connect. No-op without an OTEL SDK registered. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Add client-observed e2e write latency attribute The write PUT is request/response and the server acks only after capturing the chunk, so the workflow.stream.write span duration already equals the client->server write latency. Expose it as a named attribute workflow.stream.write.e2e_ms (via a durationAttribute option on instrumentedFetch) for direct querying, parallel to the read ttfc_ms. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs: document stream spans and latency attributes Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Emit read TTFC span from the core reader instead of a world-vercel transform Per review: move the client-observed time-to-first-chunk measurement out of a TransformStream wrapper in world-vercel and into WorkflowServerReadableStream in core, emitting workflow.stream.read on the first non-empty chunk reaching the consumer. Removes the passthrough, measures at the reader abstraction, and is backend-agnostic. world-vercel keeps the workflow.stream.read.connect HTTP span; the recordElapsedSpan helper now lives in @workflow/core. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Apply suggestion from @VaguelySerious Signed-off-by: Peter Wielander <mittgfu@gmail.com> * Update docs/content/docs/v5/observability/tracing.mdx Co-authored-by: Peter Wielander <mittgfu@gmail.com> Signed-off-by: Karthik Kalyan <105607645+karthikscale3@users.noreply.github.com> * Address review: rename write.e2e_ms -> write.chunk_rtt; docs + changeset wording Per review, rename the write attribute to workflow.stream.write.chunk_rtt (it's a per-chunk client<->server round-trip, not a full e2e), update the docs row wording for both write and read attributes, and shorten the changeset. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Signed-off-by: Peter Wielander <mittgfu@gmail.com> Signed-off-by: Karthik Kalyan <105607645+karthikscale3@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Peter Wielander <mittgfu@gmail.com>github.com-vercel-workflow · 2c6ee614 · 2026-07-10
- 0.4ETV[web-shared][web] Fix events tab search (#2107) * Add server-backed exact ID search to the Events tab. Replace client-side substring filtering with API lookups for full correlation and event IDs so searches work beyond the first loaded page. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix exact ID search dimming and support wrun_ correlation IDs. Disable group dimming for server search results and accept run IDs in the exact ID parser so run-level correlation search works. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix dimmed row when searching by event ID for run-level events. Map selectedGroupKey to __run__ for run-level search results so the matched row is treated as related instead of dimmed. Co-authored-by: Cursor <cursoragent@cursor.com> * Remove run ID search from Events tab exact ID lookup. Workflow-server only accepts step, wait, and hook correlation IDs — not wrun_. Update the search placeholder and validation toast accordingly. Co-authored-by: Cursor <cursoragent@cursor.com> * Harden exact ID search UX and correlation fetch limits. Normalize lowercase ULIDs, scope Enter toasts to ID-like input, abort stale searches, disable search when unavailable, expand parser tests, and cap correlation pagination in workflow web. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix search clear race and surface truncated correlation results. Guard successful exact-ID search against aborted requests, invalidate in-flight work when the input clears, and return truncation metadata from correlation pagination. Co-authored-by: Cursor <cursoragent@cursor.com> * Differentiate exact ID search errors from not-found results. Return a discriminated union from onExactIdSearch and show search errors in the Events tab instead of mislabeling them as missing IDs. Co-authored-by: Cursor <cursoragent@cursor.com> * Apply suggestion from @VaguelySerious Signed-off-by: Peter Wielander <mittgfu@gmail.com> --------- Signed-off-by: Peter Wielander <mittgfu@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Peter Wielander <mittgfu@gmail.com>github.com-vercel-workflow · ad5c068d · 2026-05-29
- 0.4ETV[web-shared] Fix message-only error rendering (#2251) * Fix message-only error rendering * Handle non-string error stacksgithub.com-vercel-workflow · 63f1a990 · 2026-06-04
- 0.3ETVtelemetry: stream flush span + otel load diagnostics (#2891) * telemetry: emit client-observed workflow.stream.write span per flush batch Complements the existing workflow.stream.read TTFC span: each flushed batch emits a back-dated CLIENT span covering the app-perceived write latency (buffer dwell + RPC), with buffer_dwell_ms / chunks / bytes attributes so client-side batching cost (flush timer, turbo run-ready barrier) can be told apart from network/server time. Failed flushes keep the batch's original t0 so a retried batch reports its full dwell. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: tighten changeset Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * telemetry: rename flush span to workflow.stream.flush; DEBUG-log world-vercel OTEL load failure - workflow.stream.write is taken by world-vercel's per-request RPC span (#2857, chunk_rtt); the per-batch flush span gets its own name so the two stay distinguishable in trace queries. Attributes move to workflow.stream.flush.{buffer_dwell_ms,chunks,bytes}, operation=flush. - world-vercel's @opentelemetry/api load failure was silently latched as null, which also swallows bundler/resolution failures in apps that DO register a tracer (observed in production: workbench apps emit core spans but none of world-vercel's). Log the reason under DEBUG=workflow:* so the failure mode is diagnosable. - Document workflow.stream.flush in the tracing docs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: tighten changeset Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>github.com-vercel-workflow · 4a43e39f · 2026-07-13
- 0.3ETVfix(world-vercel): add default request timeout to workflow-server HTTP calls (#1807) * fix(core): add hard exit deadline to replay timeout handler When the replay timeout fires on attempt 4+ and tries to write a run_failed event, the process.exit(1) call is gated behind an await on world.events.create(). If workflow-server is unresponsive and the response never arrives, process.exit(1) is never reached, causing the function to idle until the platform kills it at maxDuration (e.g. 15m). Add a 30s hard exit deadline (setTimeout → process.exit) that fires independently of the run_failed write, guaranteeing the process exits promptly even when the server hangs. Made-with: Cursor * Update packages/core/src/runtime.ts Co-authored-by: Peter Wielander <mittgfu@gmail.com> Signed-off-by: Karthik Kalyan <105607645+karthikscale3@users.noreply.github.com> * Update .changeset/fix-replay-timeout-exit.md Co-authored-by: Peter Wielander <mittgfu@gmail.com> Signed-off-by: Karthik Kalyan <105607645+karthikscale3@users.noreply.github.com> * Update packages/core/src/runtime/constants.ts Co-authored-by: Peter Wielander <mittgfu@gmail.com> Signed-off-by: Karthik Kalyan <105607645+karthikscale3@users.noreply.github.com> * fix(world-vercel): add default request timeout to workflow-server HTTP calls Move the hang mitigation from the core replay-timeout handler to the world-vercel transport layer, per review feedback. A default AbortSignal.timeout(60s) now applies to every makeRequest() call, covering all world.* API methods (not just the replay timeout path). Timeouts are converted to WorkflowWorldError so existing catch sites handle them uniformly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(world-vercel): drop incident-specific latency note from timeout comment Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(world-vercel): address review nits on makeRequest timeout - Compose per-request timeout with caller-provided AbortSignal via AbortSignal.any() so a future caller passing options.signal doesn't silently lose the hang protection (and vice versa). - Move the floating eslint-disable-next-line for the undici dispatcher cast back next to the actual `fetch(... as any)` call where the suppression applies, instead of pointing at `const fetchStart`. Both nits flagged by @VaguelySerious in the AI review on PR #1807. Made-with: Cursor * Apply suggestions from code review Co-authored-by: Peter Wielander <mittgfu@gmail.com> Signed-off-by: Karthik Kalyan <105607645+karthikscale3@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Peter Wielander <mittgfu@gmail.com> Signed-off-by: Karthik Kalyan <105607645+karthikscale3@users.noreply.github.com> --------- Signed-off-by: Karthik Kalyan <105607645+karthikscale3@users.noreply.github.com> Co-authored-by: Peter Wielander <mittgfu@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com>github.com-vercel-workflow · 5eb0b792 · 2026-05-04
- 0.3ETVweb: list hooks from analytics, fetch token on demand (#2652) * Add workflow analytics world APIs * web: read observability list views from world.analytics when available Route the runs/steps/events/hooks list server actions through the optional world.analytics namespace when the backend provides one, falling back to the runtime storage APIs otherwise. Events listing only uses the analytics path when no payload data is requested. Detail/get actions, streams, and mutations are unchanged. * web: keep events and hooks list reads on the runtime storage API The Events tab and trace viewer derive step names and wait resumeAt from resolved event payloads, and the hooks table needs the secret token and ownerId for its resume/copy-token actions. The metadata-only analytics rows do not carry these, so only the runs and steps list views use world.analytics. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * web: read events list from world.analytics with a runtime-shape remap The events list/trace consumers read only top-level eventType, correlationId, and createdAt; event payloads are loaded lazily per event via fetchEvent(..., 'all') on the runtime path. Map the flat analytics event rows into the runtime Event shape (reconstructing eventData.stepName) so fetchEvents and fetchEventsByCorrelationId can use the analytics read path when available. Hooks remain on the runtime path (they need the secret token + ownerId). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * web: list hooks from world.analytics, fetch token on demand The hooks list now reads from the metadata-only world.analytics namespace when the backend provides one (falling back to the runtime storage APIs otherwise). A hook's secret token is no longer shipped in list rows — it is fetched one hook at a time via world.hooks.get only when the user copies the token or resumes the hook, keeping the secret out of bulk list responses. Adds a fetchHookToken server action + RPC, a HookListItem type (Hook without token), and a lazy HookTokenCell for the copy-token affordance. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Handle analytics access metadata in web --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>github.com-vercel-workflow · ae51f451 · 2026-07-07
- 0.3ETVTrace /flow route initialization (#2592) * Trace flow route initialization * Add flow route module timing markers * Address flow route tracing review feedbackgithub.com-vercel-workflow · 2bf5257f · 2026-06-23
- 0.3ETVweb: construct worlds explicitly instead of via static-injection stub (#2804) * web: construct worlds explicitly instead of via static-injection stub Since #2752, createWorld() from @workflow/core/runtime is a stub that throws unless a framework build plugin aliases it to a world package. The CLI was migrated to construct worlds directly, but @workflow/web still called the stub, so 'workflow web' crashed with 'Workflow target world was not statically injected' for local and postgres backends (the vercel path was unaffected since it constructs the world directly). Mirror the CLI: import the local world statically and resolve any other configured world package from the inspected project's directory. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * web: fall back to default.createWorld for CJS world packages Review feedback: 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. Mirrored in the CLI in #2806. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>github.com-vercel-workflow · cdfac39e · 2026-07-07
- 0.3ETVThread occurredAt into web-shared entities (#2608)github.com-vercel-workflow · b9a10d87 · 2026-06-24