Skip to content

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

Closed
pranaygp wants to merge 1 commit into
pranaygp/friendlier-errors-phase-6-consistencyfrom
pranaygp/friendlier-errors-phase-7-observability
Closed

Data-driven describeRunError + expose via @workflow/core/describe-error#1839
pranaygp wants to merge 1 commit into
pranaygp/friendlier-errors-phase-6-consistencyfrom
pranaygp/friendlier-errors-phase-7-observability

Conversation

@pranaygp

@pranaygppranaygp commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Phase 7 foundation of the friendlier-errors stack. Adds a data-driven describeRunError alongside the live-ErrordescribeError, and exposes both under a public @workflow/core/describe-error subpath so observability renderers (CLI, web UI) can use them without taking on the entire core runtime.

  • describeRunError({ errorCode, errorName }) — derives the same { attribution, hint, errorCode } shape as describeError(err), but from persisted run_failed / step_failed event fields. After hydration, renderers only have these primitives — not the original class instance — so attribution needs a data-driven path.
  • Shared hint strings — all six hint strings are now module-level constants, reused by both helpers, so terminal logs and UI banners render identical wording.
  • New ./describe-error subpath on @workflow/core so the CLI / web packages can consume these helpers without depending on the runtime entry point.
  • Tests: existing 6 live-Error tests kept; 8 new tests cover the persisted-data matrix (plain user error, SerializationError, context-violation, WorkflowRuntimeError, StepNotRegisteredError-by-name, REPLAY_TIMEOUT, MAX_DELIVERIES_EXCEEDED, missing errorCode fallback).

Scope note: deliberately scoped to the shared helper + public surface so it can merge cleanly. The UI wiring (split SDK / user banners, humanized step names, screenshot regression case) will follow separately on top of this.

Manual test plan

This PR is mostly a foundation that other renderers will consume. Test it by importing the subpath in a scratch script — no workbench needed.

Create a scratch file at the repo root:

// scratch.tsimport{describeRunError,describeError}from'@workflow/core/describe-error';import{SerializationError,WorkflowRuntimeError}from'@workflow/errors';// describeRunError — data-driven path (from persisted fields)console.log(describeRunError({errorCode: 'USER_ERROR',errorName: 'SerializationError'}));console.log(describeRunError({errorCode: 'USER_ERROR',errorName: 'NotInWorkflowContextError'}));console.log(describeRunError({errorCode: 'RUNTIME_ERROR'}));console.log(describeRunError({errorCode: 'REPLAY_TIMEOUT'}));console.log(describeRunError({errorCode: 'MAX_DELIVERIES_EXCEEDED'}));console.log(describeRunError({errorCode: 'USER_ERROR'}));// plain user, no hintconsole.log(describeRunError({errorCode: 'SOMETHING_WEIRD'}));// unknown → fallback// describeError — live-Error path (existing API)console.log(describeError(newSerializationError('boom')));console.log(describeError(newWorkflowRuntimeError('invariant')));console.log(describeError(newError('plain')));

Run with pnpm tsx scratch.ts.

  • SerializationError by name{ attribution: 'user', errorCode: 'USER_ERROR', hint: 'A value…serialized…' }
  • Context-violation by name{ attribution: 'user', hint: 'A workflow-only or step-only API…' }
  • RUNTIME_ERROR code{ attribution: 'sdk', hint: 'This is an internal workflow SDK error…' }
  • REPLAY_TIMEOUT code{ attribution: 'sdk', hint: 'The workflow replay took too long…' }
  • MAX_DELIVERIES_EXCEEDED code{ attribution: 'sdk', hint: 'The workflow queue exceeded its max-delivery budget…' }
  • Plain USER_ERROR (no errorName) → { attribution: 'user', errorCode: 'USER_ERROR' } with nohint field.
  • Unknown errorCode falls through{ attribution: 'user', errorCode: 'USER_ERROR' } (normalized).
  • Live-error path paritydescribeError(new SerializationError('x')) and describeRunError({ errorCode: 'USER_ERROR', errorName: 'SerializationError' }) return the same shape and same hint string (module-level constants should be shared).
  • Subpath import works — TypeScript resolves @workflow/core/describe-error and pnpm tsx runs without module-resolution errors. This verifies the new export in package.json / dist/ layout.

Unit tests

  • pnpm --filter @workflow/core exec vitest run src/describe-error.test.ts (14 tests)
  • dist/describe-error.js / dist/describe-error.d.ts emitted by existing build pipeline

📚 Friendlier errors stack

Multi-PR initiative inspired by @Schniz's stalled #706:

#PRPhaseSummary
1#1831Phase 1 + 2Ansi rendering primitives + context-violation errors
2#1832Phase 3Structured logger metadata; folds in #1812
3#1836Phase 4SerializationError at serialization / stream / encryption boundaries
4#1837Phase 5Presentation-only user vs SDK attribution (describeError)
5#1838Phase 6Consistency pass on remaining bare throw new Error(...) sites
6→ this PR (#1839)Phase 7 foundationData-driven describeRunError + public subpath
7#1840Phase 8WorkflowBuildError + applications in @workflow/builders
8#1849FollowupsDrop functionName leak, simplify docs framing, redirect stack to user code

Each PR is stacked on the previous one; merge in order.

🤖 Generated with Claude Code

@vercel

vercelBot commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
❌ 💻 Local Development10522861140
✅ 📦 Local Production10540861140
✅ 🐘 Local Postgres10540861140
✅ 🪟 Windows950095
✅ 📋 Other267018285
Total352222763800

❌ Failed Tests

💻 Local Development (2 failed)

vite-stable (2 failed):

  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack

Details by Category

❌ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable8906
✅ express-stable8906
✅ fastify-stable8906
✅ hono-stable8906
✅ nextjs-turbopack-canary76019
✅ nextjs-turbopack-stable9500
✅ nextjs-webpack-canary76019
✅ nextjs-webpack-stable9500
✅ nitro-stable8906
✅ nuxt-stable8906
✅ sveltekit-stable8906
❌ vite-stable8726
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable8906
✅ express-stable8906
✅ fastify-stable8906
✅ hono-stable8906
✅ nextjs-turbopack-canary76019
✅ nextjs-turbopack-stable9500
✅ nextjs-webpack-canary76019
✅ nextjs-webpack-stable9500
✅ nitro-stable8906
✅ nuxt-stable8906
✅ sveltekit-stable8906
✅ vite-stable8906
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable8906
✅ express-stable8906
✅ fastify-stable8906
✅ hono-stable8906
✅ nextjs-turbopack-canary76019
✅ nextjs-turbopack-stable9500
✅ nextjs-webpack-canary76019
✅ nextjs-webpack-stable9500
✅ nitro-stable8906
✅ nuxt-stable8906
✅ sveltekit-stable8906
✅ vite-stable8906
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack9500
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable8906
✅ e2e-local-postgres-nest-stable8906
✅ e2e-local-prod-nest-stable8906

📋 View full workflow run


Some E2E test jobs failed:

  • Vercel Prod: failure
  • Local Dev: failure
  • Local Prod: success
  • Local Postgres: success
  • Windows: success

Check the workflow run for details.

@changeset-bot

changeset-botBot commented Apr 23, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: e6b8e31

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 17 packages
NameType
@workflow/corePatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch
@workflow/aiPatch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a data-driven error description helper to complement the existing live-ErrordescribeError, and exposes these helpers via a new public @workflow/core/describe-error subpath for observability consumers (CLI/UI) to render consistent attribution/hints without depending on the full runtime entrypoint.

Changes:

  • Introduces describeRunError({ errorCode, errorName }) and shared hint string constants in packages/core/src/describe-error.ts.
  • Expands unit tests to cover the persisted-signal matrix for describeRunError.
  • Exposes a new ./describe-error export in @workflow/core and adds a changeset.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

FileDescription
packages/core/src/describe-error.tsAdds describeRunError and refactors shared hint strings for consistent attribution/hints.
packages/core/src/describe-error.test.tsAdds test coverage for the new data-driven helper.
packages/core/package.jsonExposes @workflow/core/describe-error subpath export for consumers.
.changeset/describe-error-subpath.mdDeclares a patch release documenting the new public subpath and helper.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadpackages/core/src/describe-error.ts Outdated
Comment on lines +38 to +40
*/
export interface PersistedErrorSignal {
errorCode?: RunErrorCode;

CopilotAIApr 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PersistedErrorSignal.errorCode is typed as RunErrorCode, but persisted run_failed.eventData.errorCode is currently just string | undefined (world schema allows any string for backwards compatibility). Making this field string | undefined (and coercing/validating to known RUN_ERROR_CODES internally) would avoid forcing downstream renderers to cast and prevents returning an invalid code under the RunErrorCode type.

Suggested change
*/
exportinterfacePersistedErrorSignal{
errorCode?: RunErrorCode;
*
*Notethatpersisted`errorCode`isstoredasaplainstringforbackward
*compatibilityandmustbevalidated/coercedagainst`RUN_ERROR_CODES`
*beforebeingtreatedasa`RunErrorCode`.
*/
exportinterfacePersistedErrorSignal{
errorCode?: string;

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e6b8e31PersistedErrorSignal.errorCode is now typed as string | undefined rather than RunErrorCode | undefined, matching the actual loose shape of the stored value. describeRunError runs it through a normalizeErrorCode helper that only trusts entries in RUN_ERROR_CODES and falls back to USER_ERROR otherwise.

Comment thread.changeset/describe-error-subpath.md Outdated
@@ -0,0 +1,10 @@
---
'@workflow/core': patch

CopilotAIApr 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changeset frontmatter uses single quotes around the package name, but other changesets in this repo use double quotes. Please update the frontmatter to match existing changeset formatting so automated tooling remains consistent.

Suggested change
'@workflow/core': patch
"@workflow/core": patch

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e6b8e31 — single-quoted frontmatter switched to double-quoted.

Comment threadpackages/core/src/describe-error.ts Outdated
Comment on lines +34 to +40
* event data: the classification `errorCode` plus an optional `errorName`
* (which maps to `Error#name` on the original throw). The UI does not
* have the original `Error` instance after hydration, so attribution has
* to be derived from these primitive fields.
*/
export interface PersistedErrorSignal {
errorCode?: RunErrorCode;

CopilotAIApr 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docstring claims errorName is persisted in run_failed / step_failed event data, but the current event schemas/runtime only persist errorCode plus an error payload (no separate errorName). As a result, consumers reading hydrated events won’t be able to provide errorName to describeRunError unless the runtime/world schema is updated to persist it, or this helper is adjusted to derive the name from the persisted error payload instead.

Suggested change
*event data: theclassification`errorCode`plusanoptional`errorName`
*(whichmapsto`Error#name`ontheoriginalthrow).TheUIdoesnot
*havetheoriginal`Error`instanceafterhydration,soattributionhas
*tobederivedfromtheseprimitivefields.
*/
exportinterfacePersistedErrorSignal{
errorCode?: RunErrorCode;
*event data: theclassification`errorCode`plustheserialized`error`
*payloademittedbytheruntime.Somecallersmayalsoprovide
*`errorName`asacompatibilityfield,butit is notaseparately
*persistedprimitiveinthecurrenteventschema.TheUIdoesnothave
*theoriginal`Error`instanceafterhydration,soattributionshouldbe
*derivedfrom`errorCode`plusthehydratederrorpayloadwhenneeded.
*/
exportinterfacePersistedErrorSignal{
errorCode?: RunErrorCode;
error?: {name?: string}|null;

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in e6b8e31 — the docstring now notes that errorName is not universally persisted today, and explains that callers that have it can pass it in to sharpen the attribution. describeRunError still returns a sensible result from errorCode alone when errorName is absent.

@pranaygp

Copy link
Copy Markdown
ContributorAuthor

Stacked: #1840 — Phase 8 (final): WorkflowBuildError class + applications in @workflow/builders.

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>
@pranaygp
pranaygpforce-pushed the pranaygp/friendlier-errors-phase-7-observability branch from ff48e25 to e6b8e31CompareApril 24, 2026 01:23
@pranaygp

Copy link
Copy Markdown
ContributorAuthor

Superseded by #1849 — consolidated friendlier-errors PR with all 8 phases + follow-up fixes (ANSI leak, non-retry semantics, shared captureStackTrace helper).

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@pranaygp
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Data-driven describeRunError + expose via @workflow/core/describe-error by pranaygp · Pull Request #1839 · vercel/workflow · GitHub
Skip to content

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

Closed
pranaygp wants to merge 1 commit into
pranaygp/friendlier-errors-phase-6-consistencyfrom
pranaygp/friendlier-errors-phase-7-observability
Closed

Data-driven describeRunError + expose via @workflow/core/describe-error#1839
pranaygp wants to merge 1 commit into
pranaygp/friendlier-errors-phase-6-consistencyfrom
pranaygp/friendlier-errors-phase-7-observability

Conversation

@pranaygp

@pranaygppranaygp commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Phase 7 foundation of the friendlier-errors stack. Adds a data-driven describeRunError alongside the live-ErrordescribeError, and exposes both under a public @workflow/core/describe-error subpath so observability renderers (CLI, web UI) can use them without taking on the entire core runtime.

  • describeRunError({ errorCode, errorName }) — derives the same { attribution, hint, errorCode } shape as describeError(err), but from persisted run_failed / step_failed event fields. After hydration, renderers only have these primitives — not the original class instance — so attribution needs a data-driven path.
  • Shared hint strings — all six hint strings are now module-level constants, reused by both helpers, so terminal logs and UI banners render identical wording.
  • New ./describe-error subpath on @workflow/core so the CLI / web packages can consume these helpers without depending on the runtime entry point.
  • Tests: existing 6 live-Error tests kept; 8 new tests cover the persisted-data matrix (plain user error, SerializationError, context-violation, WorkflowRuntimeError, StepNotRegisteredError-by-name, REPLAY_TIMEOUT, MAX_DELIVERIES_EXCEEDED, missing errorCode fallback).

Scope note: deliberately scoped to the shared helper + public surface so it can merge cleanly. The UI wiring (split SDK / user banners, humanized step names, screenshot regression case) will follow separately on top of this.

Manual test plan

This PR is mostly a foundation that other renderers will consume. Test it by importing the subpath in a scratch script — no workbench needed.

Create a scratch file at the repo root:

// scratch.tsimport{describeRunError,describeError}from'@workflow/core/describe-error';import{SerializationError,WorkflowRuntimeError}from'@workflow/errors';// describeRunError — data-driven path (from persisted fields)console.log(describeRunError({errorCode: 'USER_ERROR',errorName: 'SerializationError'}));console.log(describeRunError({errorCode: 'USER_ERROR',errorName: 'NotInWorkflowContextError'}));console.log(describeRunError({errorCode: 'RUNTIME_ERROR'}));console.log(describeRunError({errorCode: 'REPLAY_TIMEOUT'}));console.log(describeRunError({errorCode: 'MAX_DELIVERIES_EXCEEDED'}));console.log(describeRunError({errorCode: 'USER_ERROR'}));// plain user, no hintconsole.log(describeRunError({errorCode: 'SOMETHING_WEIRD'}));// unknown → fallback// describeError — live-Error path (existing API)console.log(describeError(newSerializationError('boom')));console.log(describeError(newWorkflowRuntimeError('invariant')));console.log(describeError(newError('plain')));

Run with pnpm tsx scratch.ts.

  • SerializationError by name{ attribution: 'user', errorCode: 'USER_ERROR', hint: 'A value…serialized…' }
  • Context-violation by name{ attribution: 'user', hint: 'A workflow-only or step-only API…' }
  • RUNTIME_ERROR code{ attribution: 'sdk', hint: 'This is an internal workflow SDK error…' }
  • REPLAY_TIMEOUT code{ attribution: 'sdk', hint: 'The workflow replay took too long…' }
  • MAX_DELIVERIES_EXCEEDED code{ attribution: 'sdk', hint: 'The workflow queue exceeded its max-delivery budget…' }
  • Plain USER_ERROR (no errorName) → { attribution: 'user', errorCode: 'USER_ERROR' } with nohint field.
  • Unknown errorCode falls through{ attribution: 'user', errorCode: 'USER_ERROR' } (normalized).
  • Live-error path paritydescribeError(new SerializationError('x')) and describeRunError({ errorCode: 'USER_ERROR', errorName: 'SerializationError' }) return the same shape and same hint string (module-level constants should be shared).
  • Subpath import works — TypeScript resolves @workflow/core/describe-error and pnpm tsx runs without module-resolution errors. This verifies the new export in package.json / dist/ layout.

Unit tests

  • pnpm --filter @workflow/core exec vitest run src/describe-error.test.ts (14 tests)
  • dist/describe-error.js / dist/describe-error.d.ts emitted by existing build pipeline

📚 Friendlier errors stack

Multi-PR initiative inspired by @Schniz's stalled #706:

#PRPhaseSummary
1#1831Phase 1 + 2Ansi rendering primitives + context-violation errors
2#1832Phase 3Structured logger metadata; folds in #1812
3#1836Phase 4SerializationError at serialization / stream / encryption boundaries
4#1837Phase 5Presentation-only user vs SDK attribution (describeError)
5#1838Phase 6Consistency pass on remaining bare throw new Error(...) sites
6→ this PR (#1839)Phase 7 foundationData-driven describeRunError + public subpath
7#1840Phase 8WorkflowBuildError + applications in @workflow/builders
8#1849FollowupsDrop functionName leak, simplify docs framing, redirect stack to user code

Each PR is stacked on the previous one; merge in order.

🤖 Generated with Claude Code

@vercel

vercelBot commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
❌ 💻 Local Development10522861140
✅ 📦 Local Production10540861140
✅ 🐘 Local Postgres10540861140
✅ 🪟 Windows950095
✅ 📋 Other267018285
Total352222763800

❌ Failed Tests

💻 Local Development (2 failed)

vite-stable (2 failed):

  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack

Details by Category

❌ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable8906
✅ express-stable8906
✅ fastify-stable8906
✅ hono-stable8906
✅ nextjs-turbopack-canary76019
✅ nextjs-turbopack-stable9500
✅ nextjs-webpack-canary76019
✅ nextjs-webpack-stable9500
✅ nitro-stable8906
✅ nuxt-stable8906
✅ sveltekit-stable8906
❌ vite-stable8726
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable8906
✅ express-stable8906
✅ fastify-stable8906
✅ hono-stable8906
✅ nextjs-turbopack-canary76019
✅ nextjs-turbopack-stable9500
✅ nextjs-webpack-canary76019
✅ nextjs-webpack-stable9500
✅ nitro-stable8906
✅ nuxt-stable8906
✅ sveltekit-stable8906
✅ vite-stable8906
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable8906
✅ express-stable8906
✅ fastify-stable8906
✅ hono-stable8906
✅ nextjs-turbopack-canary76019
✅ nextjs-turbopack-stable9500
✅ nextjs-webpack-canary76019
✅ nextjs-webpack-stable9500
✅ nitro-stable8906
✅ nuxt-stable8906
✅ sveltekit-stable8906
✅ vite-stable8906
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack9500
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable8906
✅ e2e-local-postgres-nest-stable8906
✅ e2e-local-prod-nest-stable8906

📋 View full workflow run


Some E2E test jobs failed:

  • Vercel Prod: failure
  • Local Dev: failure
  • Local Prod: success
  • Local Postgres: success
  • Windows: success

Check the workflow run for details.

@changeset-bot

changeset-botBot commented Apr 23, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: e6b8e31

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 17 packages
NameType
@workflow/corePatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch
@workflow/aiPatch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a data-driven error description helper to complement the existing live-ErrordescribeError, and exposes these helpers via a new public @workflow/core/describe-error subpath for observability consumers (CLI/UI) to render consistent attribution/hints without depending on the full runtime entrypoint.

Changes:

  • Introduces describeRunError({ errorCode, errorName }) and shared hint string constants in packages/core/src/describe-error.ts.
  • Expands unit tests to cover the persisted-signal matrix for describeRunError.
  • Exposes a new ./describe-error export in @workflow/core and adds a changeset.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

FileDescription
packages/core/src/describe-error.tsAdds describeRunError and refactors shared hint strings for consistent attribution/hints.
packages/core/src/describe-error.test.tsAdds test coverage for the new data-driven helper.
packages/core/package.jsonExposes @workflow/core/describe-error subpath export for consumers.
.changeset/describe-error-subpath.mdDeclares a patch release documenting the new public subpath and helper.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadpackages/core/src/describe-error.ts Outdated
Comment on lines +38 to +40
*/
export interface PersistedErrorSignal {
errorCode?: RunErrorCode;

CopilotAIApr 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PersistedErrorSignal.errorCode is typed as RunErrorCode, but persisted run_failed.eventData.errorCode is currently just string | undefined (world schema allows any string for backwards compatibility). Making this field string | undefined (and coercing/validating to known RUN_ERROR_CODES internally) would avoid forcing downstream renderers to cast and prevents returning an invalid code under the RunErrorCode type.

Suggested change
*/
exportinterfacePersistedErrorSignal{
errorCode?: RunErrorCode;
*
*Notethatpersisted`errorCode`isstoredasaplainstringforbackward
*compatibilityandmustbevalidated/coercedagainst`RUN_ERROR_CODES`
*beforebeingtreatedasa`RunErrorCode`.
*/
exportinterfacePersistedErrorSignal{
errorCode?: string;

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e6b8e31PersistedErrorSignal.errorCode is now typed as string | undefined rather than RunErrorCode | undefined, matching the actual loose shape of the stored value. describeRunError runs it through a normalizeErrorCode helper that only trusts entries in RUN_ERROR_CODES and falls back to USER_ERROR otherwise.

Comment thread.changeset/describe-error-subpath.md Outdated
@@ -0,0 +1,10 @@
---
'@workflow/core': patch

CopilotAIApr 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changeset frontmatter uses single quotes around the package name, but other changesets in this repo use double quotes. Please update the frontmatter to match existing changeset formatting so automated tooling remains consistent.

Suggested change
'@workflow/core': patch
"@workflow/core": patch

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e6b8e31 — single-quoted frontmatter switched to double-quoted.

Comment threadpackages/core/src/describe-error.ts Outdated
Comment on lines +34 to +40
* event data: the classification `errorCode` plus an optional `errorName`
* (which maps to `Error#name` on the original throw). The UI does not
* have the original `Error` instance after hydration, so attribution has
* to be derived from these primitive fields.
*/
export interface PersistedErrorSignal {
errorCode?: RunErrorCode;

CopilotAIApr 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docstring claims errorName is persisted in run_failed / step_failed event data, but the current event schemas/runtime only persist errorCode plus an error payload (no separate errorName). As a result, consumers reading hydrated events won’t be able to provide errorName to describeRunError unless the runtime/world schema is updated to persist it, or this helper is adjusted to derive the name from the persisted error payload instead.

Suggested change
*event data: theclassification`errorCode`plusanoptional`errorName`
*(whichmapsto`Error#name`ontheoriginalthrow).TheUIdoesnot
*havetheoriginal`Error`instanceafterhydration,soattributionhas
*tobederivedfromtheseprimitivefields.
*/
exportinterfacePersistedErrorSignal{
errorCode?: RunErrorCode;
*event data: theclassification`errorCode`plustheserialized`error`
*payloademittedbytheruntime.Somecallersmayalsoprovide
*`errorName`asacompatibilityfield,butit is notaseparately
*persistedprimitiveinthecurrenteventschema.TheUIdoesnothave
*theoriginal`Error`instanceafterhydration,soattributionshouldbe
*derivedfrom`errorCode`plusthehydratederrorpayloadwhenneeded.
*/
exportinterfacePersistedErrorSignal{
errorCode?: RunErrorCode;
error?: {name?: string}|null;

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in e6b8e31 — the docstring now notes that errorName is not universally persisted today, and explains that callers that have it can pass it in to sharpen the attribution. describeRunError still returns a sensible result from errorCode alone when errorName is absent.

@pranaygp

Copy link
Copy Markdown
ContributorAuthor

Stacked: #1840 — Phase 8 (final): WorkflowBuildError class + applications in @workflow/builders.

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>
@pranaygp
pranaygpforce-pushed the pranaygp/friendlier-errors-phase-7-observability branch from ff48e25 to e6b8e31CompareApril 24, 2026 01:23
@pranaygp

Copy link
Copy Markdown
ContributorAuthor

Superseded by #1849 — consolidated friendlier-errors PR with all 8 phases + follow-up fixes (ANSI leak, non-retry semantics, shared captureStackTrace helper).

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@pranaygp
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Data-driven describeRunError + expose via @workflow/core/describe-error by pranaygp · Pull Request #1839 · vercel/workflow · GitHub
Skip to content

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

Closed
pranaygp wants to merge 1 commit into
pranaygp/friendlier-errors-phase-6-consistencyfrom
pranaygp/friendlier-errors-phase-7-observability
Closed

Data-driven describeRunError + expose via @workflow/core/describe-error#1839
pranaygp wants to merge 1 commit into
pranaygp/friendlier-errors-phase-6-consistencyfrom
pranaygp/friendlier-errors-phase-7-observability

Conversation

@pranaygp

@pranaygppranaygp commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Phase 7 foundation of the friendlier-errors stack. Adds a data-driven describeRunError alongside the live-ErrordescribeError, and exposes both under a public @workflow/core/describe-error subpath so observability renderers (CLI, web UI) can use them without taking on the entire core runtime.

  • describeRunError({ errorCode, errorName }) — derives the same { attribution, hint, errorCode } shape as describeError(err), but from persisted run_failed / step_failed event fields. After hydration, renderers only have these primitives — not the original class instance — so attribution needs a data-driven path.
  • Shared hint strings — all six hint strings are now module-level constants, reused by both helpers, so terminal logs and UI banners render identical wording.
  • New ./describe-error subpath on @workflow/core so the CLI / web packages can consume these helpers without depending on the runtime entry point.
  • Tests: existing 6 live-Error tests kept; 8 new tests cover the persisted-data matrix (plain user error, SerializationError, context-violation, WorkflowRuntimeError, StepNotRegisteredError-by-name, REPLAY_TIMEOUT, MAX_DELIVERIES_EXCEEDED, missing errorCode fallback).

Scope note: deliberately scoped to the shared helper + public surface so it can merge cleanly. The UI wiring (split SDK / user banners, humanized step names, screenshot regression case) will follow separately on top of this.

Manual test plan

This PR is mostly a foundation that other renderers will consume. Test it by importing the subpath in a scratch script — no workbench needed.

Create a scratch file at the repo root:

// scratch.tsimport{describeRunError,describeError}from'@workflow/core/describe-error';import{SerializationError,WorkflowRuntimeError}from'@workflow/errors';// describeRunError — data-driven path (from persisted fields)console.log(describeRunError({errorCode: 'USER_ERROR',errorName: 'SerializationError'}));console.log(describeRunError({errorCode: 'USER_ERROR',errorName: 'NotInWorkflowContextError'}));console.log(describeRunError({errorCode: 'RUNTIME_ERROR'}));console.log(describeRunError({errorCode: 'REPLAY_TIMEOUT'}));console.log(describeRunError({errorCode: 'MAX_DELIVERIES_EXCEEDED'}));console.log(describeRunError({errorCode: 'USER_ERROR'}));// plain user, no hintconsole.log(describeRunError({errorCode: 'SOMETHING_WEIRD'}));// unknown → fallback// describeError — live-Error path (existing API)console.log(describeError(newSerializationError('boom')));console.log(describeError(newWorkflowRuntimeError('invariant')));console.log(describeError(newError('plain')));

Run with pnpm tsx scratch.ts.

  • SerializationError by name{ attribution: 'user', errorCode: 'USER_ERROR', hint: 'A value…serialized…' }
  • Context-violation by name{ attribution: 'user', hint: 'A workflow-only or step-only API…' }
  • RUNTIME_ERROR code{ attribution: 'sdk', hint: 'This is an internal workflow SDK error…' }
  • REPLAY_TIMEOUT code{ attribution: 'sdk', hint: 'The workflow replay took too long…' }
  • MAX_DELIVERIES_EXCEEDED code{ attribution: 'sdk', hint: 'The workflow queue exceeded its max-delivery budget…' }
  • Plain USER_ERROR (no errorName) → { attribution: 'user', errorCode: 'USER_ERROR' } with nohint field.
  • Unknown errorCode falls through{ attribution: 'user', errorCode: 'USER_ERROR' } (normalized).
  • Live-error path paritydescribeError(new SerializationError('x')) and describeRunError({ errorCode: 'USER_ERROR', errorName: 'SerializationError' }) return the same shape and same hint string (module-level constants should be shared).
  • Subpath import works — TypeScript resolves @workflow/core/describe-error and pnpm tsx runs without module-resolution errors. This verifies the new export in package.json / dist/ layout.

Unit tests

  • pnpm --filter @workflow/core exec vitest run src/describe-error.test.ts (14 tests)
  • dist/describe-error.js / dist/describe-error.d.ts emitted by existing build pipeline

📚 Friendlier errors stack

Multi-PR initiative inspired by @Schniz's stalled #706:

#PRPhaseSummary
1#1831Phase 1 + 2Ansi rendering primitives + context-violation errors
2#1832Phase 3Structured logger metadata; folds in #1812
3#1836Phase 4SerializationError at serialization / stream / encryption boundaries
4#1837Phase 5Presentation-only user vs SDK attribution (describeError)
5#1838Phase 6Consistency pass on remaining bare throw new Error(...) sites
6→ this PR (#1839)Phase 7 foundationData-driven describeRunError + public subpath
7#1840Phase 8WorkflowBuildError + applications in @workflow/builders
8#1849FollowupsDrop functionName leak, simplify docs framing, redirect stack to user code

Each PR is stacked on the previous one; merge in order.

🤖 Generated with Claude Code

@vercel

vercelBot commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
❌ 💻 Local Development10522861140
✅ 📦 Local Production10540861140
✅ 🐘 Local Postgres10540861140
✅ 🪟 Windows950095
✅ 📋 Other267018285
Total352222763800

❌ Failed Tests

💻 Local Development (2 failed)

vite-stable (2 failed):

  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack

Details by Category

❌ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable8906
✅ express-stable8906
✅ fastify-stable8906
✅ hono-stable8906
✅ nextjs-turbopack-canary76019
✅ nextjs-turbopack-stable9500
✅ nextjs-webpack-canary76019
✅ nextjs-webpack-stable9500
✅ nitro-stable8906
✅ nuxt-stable8906
✅ sveltekit-stable8906
❌ vite-stable8726
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable8906
✅ express-stable8906
✅ fastify-stable8906
✅ hono-stable8906
✅ nextjs-turbopack-canary76019
✅ nextjs-turbopack-stable9500
✅ nextjs-webpack-canary76019
✅ nextjs-webpack-stable9500
✅ nitro-stable8906
✅ nuxt-stable8906
✅ sveltekit-stable8906
✅ vite-stable8906
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable8906
✅ express-stable8906
✅ fastify-stable8906
✅ hono-stable8906
✅ nextjs-turbopack-canary76019
✅ nextjs-turbopack-stable9500
✅ nextjs-webpack-canary76019
✅ nextjs-webpack-stable9500
✅ nitro-stable8906
✅ nuxt-stable8906
✅ sveltekit-stable8906
✅ vite-stable8906
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack9500
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable8906
✅ e2e-local-postgres-nest-stable8906
✅ e2e-local-prod-nest-stable8906

📋 View full workflow run


Some E2E test jobs failed:

  • Vercel Prod: failure
  • Local Dev: failure
  • Local Prod: success
  • Local Postgres: success
  • Windows: success

Check the workflow run for details.

@changeset-bot

changeset-botBot commented Apr 23, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: e6b8e31

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 17 packages
NameType
@workflow/corePatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch
@workflow/aiPatch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a data-driven error description helper to complement the existing live-ErrordescribeError, and exposes these helpers via a new public @workflow/core/describe-error subpath for observability consumers (CLI/UI) to render consistent attribution/hints without depending on the full runtime entrypoint.

Changes:

  • Introduces describeRunError({ errorCode, errorName }) and shared hint string constants in packages/core/src/describe-error.ts.
  • Expands unit tests to cover the persisted-signal matrix for describeRunError.
  • Exposes a new ./describe-error export in @workflow/core and adds a changeset.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

FileDescription
packages/core/src/describe-error.tsAdds describeRunError and refactors shared hint strings for consistent attribution/hints.
packages/core/src/describe-error.test.tsAdds test coverage for the new data-driven helper.
packages/core/package.jsonExposes @workflow/core/describe-error subpath export for consumers.
.changeset/describe-error-subpath.mdDeclares a patch release documenting the new public subpath and helper.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadpackages/core/src/describe-error.ts Outdated
Comment on lines +38 to +40
*/
export interface PersistedErrorSignal {
errorCode?: RunErrorCode;

CopilotAIApr 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PersistedErrorSignal.errorCode is typed as RunErrorCode, but persisted run_failed.eventData.errorCode is currently just string | undefined (world schema allows any string for backwards compatibility). Making this field string | undefined (and coercing/validating to known RUN_ERROR_CODES internally) would avoid forcing downstream renderers to cast and prevents returning an invalid code under the RunErrorCode type.

Suggested change
*/
exportinterfacePersistedErrorSignal{
errorCode?: RunErrorCode;
*
*Notethatpersisted`errorCode`isstoredasaplainstringforbackward
*compatibilityandmustbevalidated/coercedagainst`RUN_ERROR_CODES`
*beforebeingtreatedasa`RunErrorCode`.
*/
exportinterfacePersistedErrorSignal{
errorCode?: string;

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e6b8e31PersistedErrorSignal.errorCode is now typed as string | undefined rather than RunErrorCode | undefined, matching the actual loose shape of the stored value. describeRunError runs it through a normalizeErrorCode helper that only trusts entries in RUN_ERROR_CODES and falls back to USER_ERROR otherwise.

Comment thread.changeset/describe-error-subpath.md Outdated
@@ -0,0 +1,10 @@
---
'@workflow/core': patch

CopilotAIApr 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changeset frontmatter uses single quotes around the package name, but other changesets in this repo use double quotes. Please update the frontmatter to match existing changeset formatting so automated tooling remains consistent.

Suggested change
'@workflow/core': patch
"@workflow/core": patch

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e6b8e31 — single-quoted frontmatter switched to double-quoted.

Comment threadpackages/core/src/describe-error.ts Outdated
Comment on lines +34 to +40
* event data: the classification `errorCode` plus an optional `errorName`
* (which maps to `Error#name` on the original throw). The UI does not
* have the original `Error` instance after hydration, so attribution has
* to be derived from these primitive fields.
*/
export interface PersistedErrorSignal {
errorCode?: RunErrorCode;

CopilotAIApr 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docstring claims errorName is persisted in run_failed / step_failed event data, but the current event schemas/runtime only persist errorCode plus an error payload (no separate errorName). As a result, consumers reading hydrated events won’t be able to provide errorName to describeRunError unless the runtime/world schema is updated to persist it, or this helper is adjusted to derive the name from the persisted error payload instead.

Suggested change
*event data: theclassification`errorCode`plusanoptional`errorName`
*(whichmapsto`Error#name`ontheoriginalthrow).TheUIdoesnot
*havetheoriginal`Error`instanceafterhydration,soattributionhas
*tobederivedfromtheseprimitivefields.
*/
exportinterfacePersistedErrorSignal{
errorCode?: RunErrorCode;
*event data: theclassification`errorCode`plustheserialized`error`
*payloademittedbytheruntime.Somecallersmayalsoprovide
*`errorName`asacompatibilityfield,butit is notaseparately
*persistedprimitiveinthecurrenteventschema.TheUIdoesnothave
*theoriginal`Error`instanceafterhydration,soattributionshouldbe
*derivedfrom`errorCode`plusthehydratederrorpayloadwhenneeded.
*/
exportinterfacePersistedErrorSignal{
errorCode?: RunErrorCode;
error?: {name?: string}|null;

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in e6b8e31 — the docstring now notes that errorName is not universally persisted today, and explains that callers that have it can pass it in to sharpen the attribution. describeRunError still returns a sensible result from errorCode alone when errorName is absent.

@pranaygp

Copy link
Copy Markdown
ContributorAuthor

Stacked: #1840 — Phase 8 (final): WorkflowBuildError class + applications in @workflow/builders.

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>
@pranaygp
pranaygpforce-pushed the pranaygp/friendlier-errors-phase-7-observability branch from ff48e25 to e6b8e31CompareApril 24, 2026 01:23
@pranaygp

Copy link
Copy Markdown
ContributorAuthor

Superseded by #1849 — consolidated friendlier-errors PR with all 8 phases + follow-up fixes (ANSI leak, non-retry semantics, shared captureStackTrace helper).

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@pranaygp
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Data-driven describeRunError + expose via @workflow/core/describe-error by pranaygp · Pull Request #1839 · vercel/workflow · GitHub
Skip to content

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

Closed
pranaygp wants to merge 1 commit into
pranaygp/friendlier-errors-phase-6-consistencyfrom
pranaygp/friendlier-errors-phase-7-observability
Closed

Data-driven describeRunError + expose via @workflow/core/describe-error#1839
pranaygp wants to merge 1 commit into
pranaygp/friendlier-errors-phase-6-consistencyfrom
pranaygp/friendlier-errors-phase-7-observability

Conversation

@pranaygp

@pranaygppranaygp commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Phase 7 foundation of the friendlier-errors stack. Adds a data-driven describeRunError alongside the live-ErrordescribeError, and exposes both under a public @workflow/core/describe-error subpath so observability renderers (CLI, web UI) can use them without taking on the entire core runtime.

  • describeRunError({ errorCode, errorName }) — derives the same { attribution, hint, errorCode } shape as describeError(err), but from persisted run_failed / step_failed event fields. After hydration, renderers only have these primitives — not the original class instance — so attribution needs a data-driven path.
  • Shared hint strings — all six hint strings are now module-level constants, reused by both helpers, so terminal logs and UI banners render identical wording.
  • New ./describe-error subpath on @workflow/core so the CLI / web packages can consume these helpers without depending on the runtime entry point.
  • Tests: existing 6 live-Error tests kept; 8 new tests cover the persisted-data matrix (plain user error, SerializationError, context-violation, WorkflowRuntimeError, StepNotRegisteredError-by-name, REPLAY_TIMEOUT, MAX_DELIVERIES_EXCEEDED, missing errorCode fallback).

Scope note: deliberately scoped to the shared helper + public surface so it can merge cleanly. The UI wiring (split SDK / user banners, humanized step names, screenshot regression case) will follow separately on top of this.

Manual test plan

This PR is mostly a foundation that other renderers will consume. Test it by importing the subpath in a scratch script — no workbench needed.

Create a scratch file at the repo root:

// scratch.tsimport{describeRunError,describeError}from'@workflow/core/describe-error';import{SerializationError,WorkflowRuntimeError}from'@workflow/errors';// describeRunError — data-driven path (from persisted fields)console.log(describeRunError({errorCode: 'USER_ERROR',errorName: 'SerializationError'}));console.log(describeRunError({errorCode: 'USER_ERROR',errorName: 'NotInWorkflowContextError'}));console.log(describeRunError({errorCode: 'RUNTIME_ERROR'}));console.log(describeRunError({errorCode: 'REPLAY_TIMEOUT'}));console.log(describeRunError({errorCode: 'MAX_DELIVERIES_EXCEEDED'}));console.log(describeRunError({errorCode: 'USER_ERROR'}));// plain user, no hintconsole.log(describeRunError({errorCode: 'SOMETHING_WEIRD'}));// unknown → fallback// describeError — live-Error path (existing API)console.log(describeError(newSerializationError('boom')));console.log(describeError(newWorkflowRuntimeError('invariant')));console.log(describeError(newError('plain')));

Run with pnpm tsx scratch.ts.

  • SerializationError by name{ attribution: 'user', errorCode: 'USER_ERROR', hint: 'A value…serialized…' }
  • Context-violation by name{ attribution: 'user', hint: 'A workflow-only or step-only API…' }
  • RUNTIME_ERROR code{ attribution: 'sdk', hint: 'This is an internal workflow SDK error…' }
  • REPLAY_TIMEOUT code{ attribution: 'sdk', hint: 'The workflow replay took too long…' }
  • MAX_DELIVERIES_EXCEEDED code{ attribution: 'sdk', hint: 'The workflow queue exceeded its max-delivery budget…' }
  • Plain USER_ERROR (no errorName) → { attribution: 'user', errorCode: 'USER_ERROR' } with nohint field.
  • Unknown errorCode falls through{ attribution: 'user', errorCode: 'USER_ERROR' } (normalized).
  • Live-error path paritydescribeError(new SerializationError('x')) and describeRunError({ errorCode: 'USER_ERROR', errorName: 'SerializationError' }) return the same shape and same hint string (module-level constants should be shared).
  • Subpath import works — TypeScript resolves @workflow/core/describe-error and pnpm tsx runs without module-resolution errors. This verifies the new export in package.json / dist/ layout.

Unit tests

  • pnpm --filter @workflow/core exec vitest run src/describe-error.test.ts (14 tests)
  • dist/describe-error.js / dist/describe-error.d.ts emitted by existing build pipeline

📚 Friendlier errors stack

Multi-PR initiative inspired by @Schniz's stalled #706:

#PRPhaseSummary
1#1831Phase 1 + 2Ansi rendering primitives + context-violation errors
2#1832Phase 3Structured logger metadata; folds in #1812
3#1836Phase 4SerializationError at serialization / stream / encryption boundaries
4#1837Phase 5Presentation-only user vs SDK attribution (describeError)
5#1838Phase 6Consistency pass on remaining bare throw new Error(...) sites
6→ this PR (#1839)Phase 7 foundationData-driven describeRunError + public subpath
7#1840Phase 8WorkflowBuildError + applications in @workflow/builders
8#1849FollowupsDrop functionName leak, simplify docs framing, redirect stack to user code

Each PR is stacked on the previous one; merge in order.

🤖 Generated with Claude Code

@vercel

vercelBot commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
❌ 💻 Local Development10522861140
✅ 📦 Local Production10540861140
✅ 🐘 Local Postgres10540861140
✅ 🪟 Windows950095
✅ 📋 Other267018285
Total352222763800

❌ Failed Tests

💻 Local Development (2 failed)

vite-stable (2 failed):

  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack

Details by Category

❌ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable8906
✅ express-stable8906
✅ fastify-stable8906
✅ hono-stable8906
✅ nextjs-turbopack-canary76019
✅ nextjs-turbopack-stable9500
✅ nextjs-webpack-canary76019
✅ nextjs-webpack-stable9500
✅ nitro-stable8906
✅ nuxt-stable8906
✅ sveltekit-stable8906
❌ vite-stable8726
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable8906
✅ express-stable8906
✅ fastify-stable8906
✅ hono-stable8906
✅ nextjs-turbopack-canary76019
✅ nextjs-turbopack-stable9500
✅ nextjs-webpack-canary76019
✅ nextjs-webpack-stable9500
✅ nitro-stable8906
✅ nuxt-stable8906
✅ sveltekit-stable8906
✅ vite-stable8906
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable8906
✅ express-stable8906
✅ fastify-stable8906
✅ hono-stable8906
✅ nextjs-turbopack-canary76019
✅ nextjs-turbopack-stable9500
✅ nextjs-webpack-canary76019
✅ nextjs-webpack-stable9500
✅ nitro-stable8906
✅ nuxt-stable8906
✅ sveltekit-stable8906
✅ vite-stable8906
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack9500
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable8906
✅ e2e-local-postgres-nest-stable8906
✅ e2e-local-prod-nest-stable8906

📋 View full workflow run


Some E2E test jobs failed:

  • Vercel Prod: failure
  • Local Dev: failure
  • Local Prod: success
  • Local Postgres: success
  • Windows: success

Check the workflow run for details.

@changeset-bot

changeset-botBot commented Apr 23, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: e6b8e31

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 17 packages
NameType
@workflow/corePatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch
@workflow/aiPatch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a data-driven error description helper to complement the existing live-ErrordescribeError, and exposes these helpers via a new public @workflow/core/describe-error subpath for observability consumers (CLI/UI) to render consistent attribution/hints without depending on the full runtime entrypoint.

Changes:

  • Introduces describeRunError({ errorCode, errorName }) and shared hint string constants in packages/core/src/describe-error.ts.
  • Expands unit tests to cover the persisted-signal matrix for describeRunError.
  • Exposes a new ./describe-error export in @workflow/core and adds a changeset.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

FileDescription
packages/core/src/describe-error.tsAdds describeRunError and refactors shared hint strings for consistent attribution/hints.
packages/core/src/describe-error.test.tsAdds test coverage for the new data-driven helper.
packages/core/package.jsonExposes @workflow/core/describe-error subpath export for consumers.
.changeset/describe-error-subpath.mdDeclares a patch release documenting the new public subpath and helper.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadpackages/core/src/describe-error.ts Outdated
Comment on lines +38 to +40
*/
export interface PersistedErrorSignal {
errorCode?: RunErrorCode;

CopilotAIApr 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PersistedErrorSignal.errorCode is typed as RunErrorCode, but persisted run_failed.eventData.errorCode is currently just string | undefined (world schema allows any string for backwards compatibility). Making this field string | undefined (and coercing/validating to known RUN_ERROR_CODES internally) would avoid forcing downstream renderers to cast and prevents returning an invalid code under the RunErrorCode type.

Suggested change
*/
exportinterfacePersistedErrorSignal{
errorCode?: RunErrorCode;
*
*Notethatpersisted`errorCode`isstoredasaplainstringforbackward
*compatibilityandmustbevalidated/coercedagainst`RUN_ERROR_CODES`
*beforebeingtreatedasa`RunErrorCode`.
*/
exportinterfacePersistedErrorSignal{
errorCode?: string;

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e6b8e31PersistedErrorSignal.errorCode is now typed as string | undefined rather than RunErrorCode | undefined, matching the actual loose shape of the stored value. describeRunError runs it through a normalizeErrorCode helper that only trusts entries in RUN_ERROR_CODES and falls back to USER_ERROR otherwise.

Comment thread.changeset/describe-error-subpath.md Outdated
@@ -0,0 +1,10 @@
---
'@workflow/core': patch

CopilotAIApr 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changeset frontmatter uses single quotes around the package name, but other changesets in this repo use double quotes. Please update the frontmatter to match existing changeset formatting so automated tooling remains consistent.

Suggested change
'@workflow/core': patch
"@workflow/core": patch

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e6b8e31 — single-quoted frontmatter switched to double-quoted.

Comment threadpackages/core/src/describe-error.ts Outdated
Comment on lines +34 to +40
* event data: the classification `errorCode` plus an optional `errorName`
* (which maps to `Error#name` on the original throw). The UI does not
* have the original `Error` instance after hydration, so attribution has
* to be derived from these primitive fields.
*/
export interface PersistedErrorSignal {
errorCode?: RunErrorCode;

CopilotAIApr 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docstring claims errorName is persisted in run_failed / step_failed event data, but the current event schemas/runtime only persist errorCode plus an error payload (no separate errorName). As a result, consumers reading hydrated events won’t be able to provide errorName to describeRunError unless the runtime/world schema is updated to persist it, or this helper is adjusted to derive the name from the persisted error payload instead.

Suggested change
*event data: theclassification`errorCode`plusanoptional`errorName`
*(whichmapsto`Error#name`ontheoriginalthrow).TheUIdoesnot
*havetheoriginal`Error`instanceafterhydration,soattributionhas
*tobederivedfromtheseprimitivefields.
*/
exportinterfacePersistedErrorSignal{
errorCode?: RunErrorCode;
*event data: theclassification`errorCode`plustheserialized`error`
*payloademittedbytheruntime.Somecallersmayalsoprovide
*`errorName`asacompatibilityfield,butit is notaseparately
*persistedprimitiveinthecurrenteventschema.TheUIdoesnothave
*theoriginal`Error`instanceafterhydration,soattributionshouldbe
*derivedfrom`errorCode`plusthehydratederrorpayloadwhenneeded.
*/
exportinterfacePersistedErrorSignal{
errorCode?: RunErrorCode;
error?: {name?: string}|null;

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in e6b8e31 — the docstring now notes that errorName is not universally persisted today, and explains that callers that have it can pass it in to sharpen the attribution. describeRunError still returns a sensible result from errorCode alone when errorName is absent.

@pranaygp

Copy link
Copy Markdown
ContributorAuthor

Stacked: #1840 — Phase 8 (final): WorkflowBuildError class + applications in @workflow/builders.

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>
@pranaygp
pranaygpforce-pushed the pranaygp/friendlier-errors-phase-7-observability branch from ff48e25 to e6b8e31CompareApril 24, 2026 01:23
@pranaygp

Copy link
Copy Markdown
ContributorAuthor

Superseded by #1849 — consolidated friendlier-errors PR with all 8 phases + follow-up fixes (ANSI leak, non-retry semantics, shared captureStackTrace helper).

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@pranaygp
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Data-driven describeRunError + expose via @workflow/core/describe-error by pranaygp · Pull Request #1839 · vercel/workflow · GitHub
Skip to content

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

Closed
pranaygp wants to merge 1 commit into
pranaygp/friendlier-errors-phase-6-consistencyfrom
pranaygp/friendlier-errors-phase-7-observability
Closed

Data-driven describeRunError + expose via @workflow/core/describe-error#1839
pranaygp wants to merge 1 commit into
pranaygp/friendlier-errors-phase-6-consistencyfrom
pranaygp/friendlier-errors-phase-7-observability

Conversation

@pranaygp

@pranaygppranaygp commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Phase 7 foundation of the friendlier-errors stack. Adds a data-driven describeRunError alongside the live-ErrordescribeError, and exposes both under a public @workflow/core/describe-error subpath so observability renderers (CLI, web UI) can use them without taking on the entire core runtime.

  • describeRunError({ errorCode, errorName }) — derives the same { attribution, hint, errorCode } shape as describeError(err), but from persisted run_failed / step_failed event fields. After hydration, renderers only have these primitives — not the original class instance — so attribution needs a data-driven path.
  • Shared hint strings — all six hint strings are now module-level constants, reused by both helpers, so terminal logs and UI banners render identical wording.
  • New ./describe-error subpath on @workflow/core so the CLI / web packages can consume these helpers without depending on the runtime entry point.
  • Tests: existing 6 live-Error tests kept; 8 new tests cover the persisted-data matrix (plain user error, SerializationError, context-violation, WorkflowRuntimeError, StepNotRegisteredError-by-name, REPLAY_TIMEOUT, MAX_DELIVERIES_EXCEEDED, missing errorCode fallback).

Scope note: deliberately scoped to the shared helper + public surface so it can merge cleanly. The UI wiring (split SDK / user banners, humanized step names, screenshot regression case) will follow separately on top of this.

Manual test plan

This PR is mostly a foundation that other renderers will consume. Test it by importing the subpath in a scratch script — no workbench needed.

Create a scratch file at the repo root:

// scratch.tsimport{describeRunError,describeError}from'@workflow/core/describe-error';import{SerializationError,WorkflowRuntimeError}from'@workflow/errors';// describeRunError — data-driven path (from persisted fields)console.log(describeRunError({errorCode: 'USER_ERROR',errorName: 'SerializationError'}));console.log(describeRunError({errorCode: 'USER_ERROR',errorName: 'NotInWorkflowContextError'}));console.log(describeRunError({errorCode: 'RUNTIME_ERROR'}));console.log(describeRunError({errorCode: 'REPLAY_TIMEOUT'}));console.log(describeRunError({errorCode: 'MAX_DELIVERIES_EXCEEDED'}));console.log(describeRunError({errorCode: 'USER_ERROR'}));// plain user, no hintconsole.log(describeRunError({errorCode: 'SOMETHING_WEIRD'}));// unknown → fallback// describeError — live-Error path (existing API)console.log(describeError(newSerializationError('boom')));console.log(describeError(newWorkflowRuntimeError('invariant')));console.log(describeError(newError('plain')));

Run with pnpm tsx scratch.ts.

  • SerializationError by name{ attribution: 'user', errorCode: 'USER_ERROR', hint: 'A value…serialized…' }
  • Context-violation by name{ attribution: 'user', hint: 'A workflow-only or step-only API…' }
  • RUNTIME_ERROR code{ attribution: 'sdk', hint: 'This is an internal workflow SDK error…' }
  • REPLAY_TIMEOUT code{ attribution: 'sdk', hint: 'The workflow replay took too long…' }
  • MAX_DELIVERIES_EXCEEDED code{ attribution: 'sdk', hint: 'The workflow queue exceeded its max-delivery budget…' }
  • Plain USER_ERROR (no errorName) → { attribution: 'user', errorCode: 'USER_ERROR' } with nohint field.
  • Unknown errorCode falls through{ attribution: 'user', errorCode: 'USER_ERROR' } (normalized).
  • Live-error path paritydescribeError(new SerializationError('x')) and describeRunError({ errorCode: 'USER_ERROR', errorName: 'SerializationError' }) return the same shape and same hint string (module-level constants should be shared).
  • Subpath import works — TypeScript resolves @workflow/core/describe-error and pnpm tsx runs without module-resolution errors. This verifies the new export in package.json / dist/ layout.

Unit tests

  • pnpm --filter @workflow/core exec vitest run src/describe-error.test.ts (14 tests)
  • dist/describe-error.js / dist/describe-error.d.ts emitted by existing build pipeline

📚 Friendlier errors stack

Multi-PR initiative inspired by @Schniz's stalled #706:

#PRPhaseSummary
1#1831Phase 1 + 2Ansi rendering primitives + context-violation errors
2#1832Phase 3Structured logger metadata; folds in #1812
3#1836Phase 4SerializationError at serialization / stream / encryption boundaries
4#1837Phase 5Presentation-only user vs SDK attribution (describeError)
5#1838Phase 6Consistency pass on remaining bare throw new Error(...) sites
6→ this PR (#1839)Phase 7 foundationData-driven describeRunError + public subpath
7#1840Phase 8WorkflowBuildError + applications in @workflow/builders
8#1849FollowupsDrop functionName leak, simplify docs framing, redirect stack to user code

Each PR is stacked on the previous one; merge in order.

🤖 Generated with Claude Code

@vercel

vercelBot commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
❌ 💻 Local Development10522861140
✅ 📦 Local Production10540861140
✅ 🐘 Local Postgres10540861140
✅ 🪟 Windows950095
✅ 📋 Other267018285
Total352222763800

❌ Failed Tests

💻 Local Development (2 failed)

vite-stable (2 failed):

  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack

Details by Category

❌ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable8906
✅ express-stable8906
✅ fastify-stable8906
✅ hono-stable8906
✅ nextjs-turbopack-canary76019
✅ nextjs-turbopack-stable9500
✅ nextjs-webpack-canary76019
✅ nextjs-webpack-stable9500
✅ nitro-stable8906
✅ nuxt-stable8906
✅ sveltekit-stable8906
❌ vite-stable8726
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable8906
✅ express-stable8906
✅ fastify-stable8906
✅ hono-stable8906
✅ nextjs-turbopack-canary76019
✅ nextjs-turbopack-stable9500
✅ nextjs-webpack-canary76019
✅ nextjs-webpack-stable9500
✅ nitro-stable8906
✅ nuxt-stable8906
✅ sveltekit-stable8906
✅ vite-stable8906
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable8906
✅ express-stable8906
✅ fastify-stable8906
✅ hono-stable8906
✅ nextjs-turbopack-canary76019
✅ nextjs-turbopack-stable9500
✅ nextjs-webpack-canary76019
✅ nextjs-webpack-stable9500
✅ nitro-stable8906
✅ nuxt-stable8906
✅ sveltekit-stable8906
✅ vite-stable8906
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack9500
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable8906
✅ e2e-local-postgres-nest-stable8906
✅ e2e-local-prod-nest-stable8906

📋 View full workflow run


Some E2E test jobs failed:

  • Vercel Prod: failure
  • Local Dev: failure
  • Local Prod: success
  • Local Postgres: success
  • Windows: success

Check the workflow run for details.

@changeset-bot

changeset-botBot commented Apr 23, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: e6b8e31

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 17 packages
NameType
@workflow/corePatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch
@workflow/aiPatch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a data-driven error description helper to complement the existing live-ErrordescribeError, and exposes these helpers via a new public @workflow/core/describe-error subpath for observability consumers (CLI/UI) to render consistent attribution/hints without depending on the full runtime entrypoint.

Changes:

  • Introduces describeRunError({ errorCode, errorName }) and shared hint string constants in packages/core/src/describe-error.ts.
  • Expands unit tests to cover the persisted-signal matrix for describeRunError.
  • Exposes a new ./describe-error export in @workflow/core and adds a changeset.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

FileDescription
packages/core/src/describe-error.tsAdds describeRunError and refactors shared hint strings for consistent attribution/hints.
packages/core/src/describe-error.test.tsAdds test coverage for the new data-driven helper.
packages/core/package.jsonExposes @workflow/core/describe-error subpath export for consumers.
.changeset/describe-error-subpath.mdDeclares a patch release documenting the new public subpath and helper.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadpackages/core/src/describe-error.ts Outdated
Comment on lines +38 to +40
*/
export interface PersistedErrorSignal {
errorCode?: RunErrorCode;

CopilotAIApr 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PersistedErrorSignal.errorCode is typed as RunErrorCode, but persisted run_failed.eventData.errorCode is currently just string | undefined (world schema allows any string for backwards compatibility). Making this field string | undefined (and coercing/validating to known RUN_ERROR_CODES internally) would avoid forcing downstream renderers to cast and prevents returning an invalid code under the RunErrorCode type.

Suggested change
*/
exportinterfacePersistedErrorSignal{
errorCode?: RunErrorCode;
*
*Notethatpersisted`errorCode`isstoredasaplainstringforbackward
*compatibilityandmustbevalidated/coercedagainst`RUN_ERROR_CODES`
*beforebeingtreatedasa`RunErrorCode`.
*/
exportinterfacePersistedErrorSignal{
errorCode?: string;

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e6b8e31PersistedErrorSignal.errorCode is now typed as string | undefined rather than RunErrorCode | undefined, matching the actual loose shape of the stored value. describeRunError runs it through a normalizeErrorCode helper that only trusts entries in RUN_ERROR_CODES and falls back to USER_ERROR otherwise.

Comment thread.changeset/describe-error-subpath.md Outdated
@@ -0,0 +1,10 @@
---
'@workflow/core': patch

CopilotAIApr 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changeset frontmatter uses single quotes around the package name, but other changesets in this repo use double quotes. Please update the frontmatter to match existing changeset formatting so automated tooling remains consistent.

Suggested change
'@workflow/core': patch
"@workflow/core": patch

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e6b8e31 — single-quoted frontmatter switched to double-quoted.

Comment threadpackages/core/src/describe-error.ts Outdated
Comment on lines +34 to +40
* event data: the classification `errorCode` plus an optional `errorName`
* (which maps to `Error#name` on the original throw). The UI does not
* have the original `Error` instance after hydration, so attribution has
* to be derived from these primitive fields.
*/
export interface PersistedErrorSignal {
errorCode?: RunErrorCode;

CopilotAIApr 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docstring claims errorName is persisted in run_failed / step_failed event data, but the current event schemas/runtime only persist errorCode plus an error payload (no separate errorName). As a result, consumers reading hydrated events won’t be able to provide errorName to describeRunError unless the runtime/world schema is updated to persist it, or this helper is adjusted to derive the name from the persisted error payload instead.

Suggested change
*event data: theclassification`errorCode`plusanoptional`errorName`
*(whichmapsto`Error#name`ontheoriginalthrow).TheUIdoesnot
*havetheoriginal`Error`instanceafterhydration,soattributionhas
*tobederivedfromtheseprimitivefields.
*/
exportinterfacePersistedErrorSignal{
errorCode?: RunErrorCode;
*event data: theclassification`errorCode`plustheserialized`error`
*payloademittedbytheruntime.Somecallersmayalsoprovide
*`errorName`asacompatibilityfield,butit is notaseparately
*persistedprimitiveinthecurrenteventschema.TheUIdoesnothave
*theoriginal`Error`instanceafterhydration,soattributionshouldbe
*derivedfrom`errorCode`plusthehydratederrorpayloadwhenneeded.
*/
exportinterfacePersistedErrorSignal{
errorCode?: RunErrorCode;
error?: {name?: string}|null;

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in e6b8e31 — the docstring now notes that errorName is not universally persisted today, and explains that callers that have it can pass it in to sharpen the attribution. describeRunError still returns a sensible result from errorCode alone when errorName is absent.

@pranaygp

Copy link
Copy Markdown
ContributorAuthor

Stacked: #1840 — Phase 8 (final): WorkflowBuildError class + applications in @workflow/builders.

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>
@pranaygp
pranaygpforce-pushed the pranaygp/friendlier-errors-phase-7-observability branch from ff48e25 to e6b8e31CompareApril 24, 2026 01:23
@pranaygp

Copy link
Copy Markdown
ContributorAuthor

Superseded by #1849 — consolidated friendlier-errors PR with all 8 phases + follow-up fixes (ANSI leak, non-retry semantics, shared captureStackTrace helper).

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@pranaygp
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Data-driven describeRunError + expose via @workflow/core/describe-error by pranaygp · Pull Request #1839 · vercel/workflow · GitHub
Skip to content

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

Closed
pranaygp wants to merge 1 commit into
pranaygp/friendlier-errors-phase-6-consistencyfrom
pranaygp/friendlier-errors-phase-7-observability
Closed

Data-driven describeRunError + expose via @workflow/core/describe-error#1839
pranaygp wants to merge 1 commit into
pranaygp/friendlier-errors-phase-6-consistencyfrom
pranaygp/friendlier-errors-phase-7-observability

Conversation

@pranaygp

@pranaygppranaygp commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Phase 7 foundation of the friendlier-errors stack. Adds a data-driven describeRunError alongside the live-ErrordescribeError, and exposes both under a public @workflow/core/describe-error subpath so observability renderers (CLI, web UI) can use them without taking on the entire core runtime.

  • describeRunError({ errorCode, errorName }) — derives the same { attribution, hint, errorCode } shape as describeError(err), but from persisted run_failed / step_failed event fields. After hydration, renderers only have these primitives — not the original class instance — so attribution needs a data-driven path.
  • Shared hint strings — all six hint strings are now module-level constants, reused by both helpers, so terminal logs and UI banners render identical wording.
  • New ./describe-error subpath on @workflow/core so the CLI / web packages can consume these helpers without depending on the runtime entry point.
  • Tests: existing 6 live-Error tests kept; 8 new tests cover the persisted-data matrix (plain user error, SerializationError, context-violation, WorkflowRuntimeError, StepNotRegisteredError-by-name, REPLAY_TIMEOUT, MAX_DELIVERIES_EXCEEDED, missing errorCode fallback).

Scope note: deliberately scoped to the shared helper + public surface so it can merge cleanly. The UI wiring (split SDK / user banners, humanized step names, screenshot regression case) will follow separately on top of this.

Manual test plan

This PR is mostly a foundation that other renderers will consume. Test it by importing the subpath in a scratch script — no workbench needed.

Create a scratch file at the repo root:

// scratch.tsimport{describeRunError,describeError}from'@workflow/core/describe-error';import{SerializationError,WorkflowRuntimeError}from'@workflow/errors';// describeRunError — data-driven path (from persisted fields)console.log(describeRunError({errorCode: 'USER_ERROR',errorName: 'SerializationError'}));console.log(describeRunError({errorCode: 'USER_ERROR',errorName: 'NotInWorkflowContextError'}));console.log(describeRunError({errorCode: 'RUNTIME_ERROR'}));console.log(describeRunError({errorCode: 'REPLAY_TIMEOUT'}));console.log(describeRunError({errorCode: 'MAX_DELIVERIES_EXCEEDED'}));console.log(describeRunError({errorCode: 'USER_ERROR'}));// plain user, no hintconsole.log(describeRunError({errorCode: 'SOMETHING_WEIRD'}));// unknown → fallback// describeError — live-Error path (existing API)console.log(describeError(newSerializationError('boom')));console.log(describeError(newWorkflowRuntimeError('invariant')));console.log(describeError(newError('plain')));

Run with pnpm tsx scratch.ts.

  • SerializationError by name{ attribution: 'user', errorCode: 'USER_ERROR', hint: 'A value…serialized…' }
  • Context-violation by name{ attribution: 'user', hint: 'A workflow-only or step-only API…' }
  • RUNTIME_ERROR code{ attribution: 'sdk', hint: 'This is an internal workflow SDK error…' }
  • REPLAY_TIMEOUT code{ attribution: 'sdk', hint: 'The workflow replay took too long…' }
  • MAX_DELIVERIES_EXCEEDED code{ attribution: 'sdk', hint: 'The workflow queue exceeded its max-delivery budget…' }
  • Plain USER_ERROR (no errorName) → { attribution: 'user', errorCode: 'USER_ERROR' } with nohint field.
  • Unknown errorCode falls through{ attribution: 'user', errorCode: 'USER_ERROR' } (normalized).
  • Live-error path paritydescribeError(new SerializationError('x')) and describeRunError({ errorCode: 'USER_ERROR', errorName: 'SerializationError' }) return the same shape and same hint string (module-level constants should be shared).
  • Subpath import works — TypeScript resolves @workflow/core/describe-error and pnpm tsx runs without module-resolution errors. This verifies the new export in package.json / dist/ layout.

Unit tests

  • pnpm --filter @workflow/core exec vitest run src/describe-error.test.ts (14 tests)
  • dist/describe-error.js / dist/describe-error.d.ts emitted by existing build pipeline

📚 Friendlier errors stack

Multi-PR initiative inspired by @Schniz's stalled #706:

#PRPhaseSummary
1#1831Phase 1 + 2Ansi rendering primitives + context-violation errors
2#1832Phase 3Structured logger metadata; folds in #1812
3#1836Phase 4SerializationError at serialization / stream / encryption boundaries
4#1837Phase 5Presentation-only user vs SDK attribution (describeError)
5#1838Phase 6Consistency pass on remaining bare throw new Error(...) sites
6→ this PR (#1839)Phase 7 foundationData-driven describeRunError + public subpath
7#1840Phase 8WorkflowBuildError + applications in @workflow/builders
8#1849FollowupsDrop functionName leak, simplify docs framing, redirect stack to user code

Each PR is stacked on the previous one; merge in order.

🤖 Generated with Claude Code

@vercel

vercelBot commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
❌ 💻 Local Development10522861140
✅ 📦 Local Production10540861140
✅ 🐘 Local Postgres10540861140
✅ 🪟 Windows950095
✅ 📋 Other267018285
Total352222763800

❌ Failed Tests

💻 Local Development (2 failed)

vite-stable (2 failed):

  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack

Details by Category

❌ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable8906
✅ express-stable8906
✅ fastify-stable8906
✅ hono-stable8906
✅ nextjs-turbopack-canary76019
✅ nextjs-turbopack-stable9500
✅ nextjs-webpack-canary76019
✅ nextjs-webpack-stable9500
✅ nitro-stable8906
✅ nuxt-stable8906
✅ sveltekit-stable8906
❌ vite-stable8726
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable8906
✅ express-stable8906
✅ fastify-stable8906
✅ hono-stable8906
✅ nextjs-turbopack-canary76019
✅ nextjs-turbopack-stable9500
✅ nextjs-webpack-canary76019
✅ nextjs-webpack-stable9500
✅ nitro-stable8906
✅ nuxt-stable8906
✅ sveltekit-stable8906
✅ vite-stable8906
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable8906
✅ express-stable8906
✅ fastify-stable8906
✅ hono-stable8906
✅ nextjs-turbopack-canary76019
✅ nextjs-turbopack-stable9500
✅ nextjs-webpack-canary76019
✅ nextjs-webpack-stable9500
✅ nitro-stable8906
✅ nuxt-stable8906
✅ sveltekit-stable8906
✅ vite-stable8906
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack9500
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable8906
✅ e2e-local-postgres-nest-stable8906
✅ e2e-local-prod-nest-stable8906

📋 View full workflow run


Some E2E test jobs failed:

  • Vercel Prod: failure
  • Local Dev: failure
  • Local Prod: success
  • Local Postgres: success
  • Windows: success

Check the workflow run for details.

@changeset-bot

changeset-botBot commented Apr 23, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: e6b8e31

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 17 packages
NameType
@workflow/corePatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch
@workflow/aiPatch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a data-driven error description helper to complement the existing live-ErrordescribeError, and exposes these helpers via a new public @workflow/core/describe-error subpath for observability consumers (CLI/UI) to render consistent attribution/hints without depending on the full runtime entrypoint.

Changes:

  • Introduces describeRunError({ errorCode, errorName }) and shared hint string constants in packages/core/src/describe-error.ts.
  • Expands unit tests to cover the persisted-signal matrix for describeRunError.
  • Exposes a new ./describe-error export in @workflow/core and adds a changeset.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

FileDescription
packages/core/src/describe-error.tsAdds describeRunError and refactors shared hint strings for consistent attribution/hints.
packages/core/src/describe-error.test.tsAdds test coverage for the new data-driven helper.
packages/core/package.jsonExposes @workflow/core/describe-error subpath export for consumers.
.changeset/describe-error-subpath.mdDeclares a patch release documenting the new public subpath and helper.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadpackages/core/src/describe-error.ts Outdated
Comment on lines +38 to +40
*/
export interface PersistedErrorSignal {
errorCode?: RunErrorCode;

CopilotAIApr 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PersistedErrorSignal.errorCode is typed as RunErrorCode, but persisted run_failed.eventData.errorCode is currently just string | undefined (world schema allows any string for backwards compatibility). Making this field string | undefined (and coercing/validating to known RUN_ERROR_CODES internally) would avoid forcing downstream renderers to cast and prevents returning an invalid code under the RunErrorCode type.

Suggested change
*/
exportinterfacePersistedErrorSignal{
errorCode?: RunErrorCode;
*
*Notethatpersisted`errorCode`isstoredasaplainstringforbackward
*compatibilityandmustbevalidated/coercedagainst`RUN_ERROR_CODES`
*beforebeingtreatedasa`RunErrorCode`.
*/
exportinterfacePersistedErrorSignal{
errorCode?: string;

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e6b8e31PersistedErrorSignal.errorCode is now typed as string | undefined rather than RunErrorCode | undefined, matching the actual loose shape of the stored value. describeRunError runs it through a normalizeErrorCode helper that only trusts entries in RUN_ERROR_CODES and falls back to USER_ERROR otherwise.

Comment thread.changeset/describe-error-subpath.md Outdated
@@ -0,0 +1,10 @@
---
'@workflow/core': patch

CopilotAIApr 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changeset frontmatter uses single quotes around the package name, but other changesets in this repo use double quotes. Please update the frontmatter to match existing changeset formatting so automated tooling remains consistent.

Suggested change
'@workflow/core': patch
"@workflow/core": patch

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e6b8e31 — single-quoted frontmatter switched to double-quoted.

Comment threadpackages/core/src/describe-error.ts Outdated
Comment on lines +34 to +40
* event data: the classification `errorCode` plus an optional `errorName`
* (which maps to `Error#name` on the original throw). The UI does not
* have the original `Error` instance after hydration, so attribution has
* to be derived from these primitive fields.
*/
export interface PersistedErrorSignal {
errorCode?: RunErrorCode;

CopilotAIApr 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docstring claims errorName is persisted in run_failed / step_failed event data, but the current event schemas/runtime only persist errorCode plus an error payload (no separate errorName). As a result, consumers reading hydrated events won’t be able to provide errorName to describeRunError unless the runtime/world schema is updated to persist it, or this helper is adjusted to derive the name from the persisted error payload instead.

Suggested change
*event data: theclassification`errorCode`plusanoptional`errorName`
*(whichmapsto`Error#name`ontheoriginalthrow).TheUIdoesnot
*havetheoriginal`Error`instanceafterhydration,soattributionhas
*tobederivedfromtheseprimitivefields.
*/
exportinterfacePersistedErrorSignal{
errorCode?: RunErrorCode;
*event data: theclassification`errorCode`plustheserialized`error`
*payloademittedbytheruntime.Somecallersmayalsoprovide
*`errorName`asacompatibilityfield,butit is notaseparately
*persistedprimitiveinthecurrenteventschema.TheUIdoesnothave
*theoriginal`Error`instanceafterhydration,soattributionshouldbe
*derivedfrom`errorCode`plusthehydratederrorpayloadwhenneeded.
*/
exportinterfacePersistedErrorSignal{
errorCode?: RunErrorCode;
error?: {name?: string}|null;

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in e6b8e31 — the docstring now notes that errorName is not universally persisted today, and explains that callers that have it can pass it in to sharpen the attribution. describeRunError still returns a sensible result from errorCode alone when errorName is absent.

@pranaygp

Copy link
Copy Markdown
ContributorAuthor

Stacked: #1840 — Phase 8 (final): WorkflowBuildError class + applications in @workflow/builders.

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>
@pranaygp
pranaygpforce-pushed the pranaygp/friendlier-errors-phase-7-observability branch from ff48e25 to e6b8e31CompareApril 24, 2026 01:23
@pranaygp

Copy link
Copy Markdown
ContributorAuthor

Superseded by #1849 — consolidated friendlier-errors PR with all 8 phases + follow-up fixes (ANSI leak, non-retry semantics, shared captureStackTrace helper).

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@pranaygp
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Data-driven describeRunError + expose via @workflow/core/describe-error by pranaygp · Pull Request #1839 · vercel/workflow · GitHub
Skip to content

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

Closed
pranaygp wants to merge 1 commit into
pranaygp/friendlier-errors-phase-6-consistencyfrom
pranaygp/friendlier-errors-phase-7-observability
Closed

Data-driven describeRunError + expose via @workflow/core/describe-error#1839
pranaygp wants to merge 1 commit into
pranaygp/friendlier-errors-phase-6-consistencyfrom
pranaygp/friendlier-errors-phase-7-observability

Conversation

@pranaygp

@pranaygppranaygp commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Phase 7 foundation of the friendlier-errors stack. Adds a data-driven describeRunError alongside the live-ErrordescribeError, and exposes both under a public @workflow/core/describe-error subpath so observability renderers (CLI, web UI) can use them without taking on the entire core runtime.

  • describeRunError({ errorCode, errorName }) — derives the same { attribution, hint, errorCode } shape as describeError(err), but from persisted run_failed / step_failed event fields. After hydration, renderers only have these primitives — not the original class instance — so attribution needs a data-driven path.
  • Shared hint strings — all six hint strings are now module-level constants, reused by both helpers, so terminal logs and UI banners render identical wording.
  • New ./describe-error subpath on @workflow/core so the CLI / web packages can consume these helpers without depending on the runtime entry point.
  • Tests: existing 6 live-Error tests kept; 8 new tests cover the persisted-data matrix (plain user error, SerializationError, context-violation, WorkflowRuntimeError, StepNotRegisteredError-by-name, REPLAY_TIMEOUT, MAX_DELIVERIES_EXCEEDED, missing errorCode fallback).

Scope note: deliberately scoped to the shared helper + public surface so it can merge cleanly. The UI wiring (split SDK / user banners, humanized step names, screenshot regression case) will follow separately on top of this.

Manual test plan

This PR is mostly a foundation that other renderers will consume. Test it by importing the subpath in a scratch script — no workbench needed.

Create a scratch file at the repo root:

// scratch.tsimport{describeRunError,describeError}from'@workflow/core/describe-error';import{SerializationError,WorkflowRuntimeError}from'@workflow/errors';// describeRunError — data-driven path (from persisted fields)console.log(describeRunError({errorCode: 'USER_ERROR',errorName: 'SerializationError'}));console.log(describeRunError({errorCode: 'USER_ERROR',errorName: 'NotInWorkflowContextError'}));console.log(describeRunError({errorCode: 'RUNTIME_ERROR'}));console.log(describeRunError({errorCode: 'REPLAY_TIMEOUT'}));console.log(describeRunError({errorCode: 'MAX_DELIVERIES_EXCEEDED'}));console.log(describeRunError({errorCode: 'USER_ERROR'}));// plain user, no hintconsole.log(describeRunError({errorCode: 'SOMETHING_WEIRD'}));// unknown → fallback// describeError — live-Error path (existing API)console.log(describeError(newSerializationError('boom')));console.log(describeError(newWorkflowRuntimeError('invariant')));console.log(describeError(newError('plain')));

Run with pnpm tsx scratch.ts.

  • SerializationError by name{ attribution: 'user', errorCode: 'USER_ERROR', hint: 'A value…serialized…' }
  • Context-violation by name{ attribution: 'user', hint: 'A workflow-only or step-only API…' }
  • RUNTIME_ERROR code{ attribution: 'sdk', hint: 'This is an internal workflow SDK error…' }
  • REPLAY_TIMEOUT code{ attribution: 'sdk', hint: 'The workflow replay took too long…' }
  • MAX_DELIVERIES_EXCEEDED code{ attribution: 'sdk', hint: 'The workflow queue exceeded its max-delivery budget…' }
  • Plain USER_ERROR (no errorName) → { attribution: 'user', errorCode: 'USER_ERROR' } with nohint field.
  • Unknown errorCode falls through{ attribution: 'user', errorCode: 'USER_ERROR' } (normalized).
  • Live-error path paritydescribeError(new SerializationError('x')) and describeRunError({ errorCode: 'USER_ERROR', errorName: 'SerializationError' }) return the same shape and same hint string (module-level constants should be shared).
  • Subpath import works — TypeScript resolves @workflow/core/describe-error and pnpm tsx runs without module-resolution errors. This verifies the new export in package.json / dist/ layout.

Unit tests

  • pnpm --filter @workflow/core exec vitest run src/describe-error.test.ts (14 tests)
  • dist/describe-error.js / dist/describe-error.d.ts emitted by existing build pipeline

📚 Friendlier errors stack

Multi-PR initiative inspired by @Schniz's stalled #706:

#PRPhaseSummary
1#1831Phase 1 + 2Ansi rendering primitives + context-violation errors
2#1832Phase 3Structured logger metadata; folds in #1812
3#1836Phase 4SerializationError at serialization / stream / encryption boundaries
4#1837Phase 5Presentation-only user vs SDK attribution (describeError)
5#1838Phase 6Consistency pass on remaining bare throw new Error(...) sites
6→ this PR (#1839)Phase 7 foundationData-driven describeRunError + public subpath
7#1840Phase 8WorkflowBuildError + applications in @workflow/builders
8#1849FollowupsDrop functionName leak, simplify docs framing, redirect stack to user code

Each PR is stacked on the previous one; merge in order.

🤖 Generated with Claude Code

@vercel

vercelBot commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
❌ 💻 Local Development10522861140
✅ 📦 Local Production10540861140
✅ 🐘 Local Postgres10540861140
✅ 🪟 Windows950095
✅ 📋 Other267018285
Total352222763800

❌ Failed Tests

💻 Local Development (2 failed)

vite-stable (2 failed):

  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack

Details by Category

❌ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable8906
✅ express-stable8906
✅ fastify-stable8906
✅ hono-stable8906
✅ nextjs-turbopack-canary76019
✅ nextjs-turbopack-stable9500
✅ nextjs-webpack-canary76019
✅ nextjs-webpack-stable9500
✅ nitro-stable8906
✅ nuxt-stable8906
✅ sveltekit-stable8906
❌ vite-stable8726
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable8906
✅ express-stable8906
✅ fastify-stable8906
✅ hono-stable8906
✅ nextjs-turbopack-canary76019
✅ nextjs-turbopack-stable9500
✅ nextjs-webpack-canary76019
✅ nextjs-webpack-stable9500
✅ nitro-stable8906
✅ nuxt-stable8906
✅ sveltekit-stable8906
✅ vite-stable8906
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable8906
✅ express-stable8906
✅ fastify-stable8906
✅ hono-stable8906
✅ nextjs-turbopack-canary76019
✅ nextjs-turbopack-stable9500
✅ nextjs-webpack-canary76019
✅ nextjs-webpack-stable9500
✅ nitro-stable8906
✅ nuxt-stable8906
✅ sveltekit-stable8906
✅ vite-stable8906
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack9500
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable8906
✅ e2e-local-postgres-nest-stable8906
✅ e2e-local-prod-nest-stable8906

📋 View full workflow run


Some E2E test jobs failed:

  • Vercel Prod: failure
  • Local Dev: failure
  • Local Prod: success
  • Local Postgres: success
  • Windows: success

Check the workflow run for details.

@changeset-bot

changeset-botBot commented Apr 23, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: e6b8e31

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 17 packages
NameType
@workflow/corePatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch
@workflow/aiPatch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a data-driven error description helper to complement the existing live-ErrordescribeError, and exposes these helpers via a new public @workflow/core/describe-error subpath for observability consumers (CLI/UI) to render consistent attribution/hints without depending on the full runtime entrypoint.

Changes:

  • Introduces describeRunError({ errorCode, errorName }) and shared hint string constants in packages/core/src/describe-error.ts.
  • Expands unit tests to cover the persisted-signal matrix for describeRunError.
  • Exposes a new ./describe-error export in @workflow/core and adds a changeset.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

FileDescription
packages/core/src/describe-error.tsAdds describeRunError and refactors shared hint strings for consistent attribution/hints.
packages/core/src/describe-error.test.tsAdds test coverage for the new data-driven helper.
packages/core/package.jsonExposes @workflow/core/describe-error subpath export for consumers.
.changeset/describe-error-subpath.mdDeclares a patch release documenting the new public subpath and helper.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadpackages/core/src/describe-error.ts Outdated
Comment on lines +38 to +40
*/
export interface PersistedErrorSignal {
errorCode?: RunErrorCode;

CopilotAIApr 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PersistedErrorSignal.errorCode is typed as RunErrorCode, but persisted run_failed.eventData.errorCode is currently just string | undefined (world schema allows any string for backwards compatibility). Making this field string | undefined (and coercing/validating to known RUN_ERROR_CODES internally) would avoid forcing downstream renderers to cast and prevents returning an invalid code under the RunErrorCode type.

Suggested change
*/
exportinterfacePersistedErrorSignal{
errorCode?: RunErrorCode;
*
*Notethatpersisted`errorCode`isstoredasaplainstringforbackward
*compatibilityandmustbevalidated/coercedagainst`RUN_ERROR_CODES`
*beforebeingtreatedasa`RunErrorCode`.
*/
exportinterfacePersistedErrorSignal{
errorCode?: string;

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e6b8e31PersistedErrorSignal.errorCode is now typed as string | undefined rather than RunErrorCode | undefined, matching the actual loose shape of the stored value. describeRunError runs it through a normalizeErrorCode helper that only trusts entries in RUN_ERROR_CODES and falls back to USER_ERROR otherwise.

Comment thread.changeset/describe-error-subpath.md Outdated
@@ -0,0 +1,10 @@
---
'@workflow/core': patch

CopilotAIApr 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changeset frontmatter uses single quotes around the package name, but other changesets in this repo use double quotes. Please update the frontmatter to match existing changeset formatting so automated tooling remains consistent.

Suggested change
'@workflow/core': patch
"@workflow/core": patch

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e6b8e31 — single-quoted frontmatter switched to double-quoted.

Comment threadpackages/core/src/describe-error.ts Outdated
Comment on lines +34 to +40
* event data: the classification `errorCode` plus an optional `errorName`
* (which maps to `Error#name` on the original throw). The UI does not
* have the original `Error` instance after hydration, so attribution has
* to be derived from these primitive fields.
*/
export interface PersistedErrorSignal {
errorCode?: RunErrorCode;

CopilotAIApr 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docstring claims errorName is persisted in run_failed / step_failed event data, but the current event schemas/runtime only persist errorCode plus an error payload (no separate errorName). As a result, consumers reading hydrated events won’t be able to provide errorName to describeRunError unless the runtime/world schema is updated to persist it, or this helper is adjusted to derive the name from the persisted error payload instead.

Suggested change
*event data: theclassification`errorCode`plusanoptional`errorName`
*(whichmapsto`Error#name`ontheoriginalthrow).TheUIdoesnot
*havetheoriginal`Error`instanceafterhydration,soattributionhas
*tobederivedfromtheseprimitivefields.
*/
exportinterfacePersistedErrorSignal{
errorCode?: RunErrorCode;
*event data: theclassification`errorCode`plustheserialized`error`
*payloademittedbytheruntime.Somecallersmayalsoprovide
*`errorName`asacompatibilityfield,butit is notaseparately
*persistedprimitiveinthecurrenteventschema.TheUIdoesnothave
*theoriginal`Error`instanceafterhydration,soattributionshouldbe
*derivedfrom`errorCode`plusthehydratederrorpayloadwhenneeded.
*/
exportinterfacePersistedErrorSignal{
errorCode?: RunErrorCode;
error?: {name?: string}|null;

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in e6b8e31 — the docstring now notes that errorName is not universally persisted today, and explains that callers that have it can pass it in to sharpen the attribution. describeRunError still returns a sensible result from errorCode alone when errorName is absent.

@pranaygp

Copy link
Copy Markdown
ContributorAuthor

Stacked: #1840 — Phase 8 (final): WorkflowBuildError class + applications in @workflow/builders.

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>
@pranaygp
pranaygpforce-pushed the pranaygp/friendlier-errors-phase-7-observability branch from ff48e25 to e6b8e31CompareApril 24, 2026 01:23
@pranaygp

Copy link
Copy Markdown
ContributorAuthor

Superseded by #1849 — consolidated friendlier-errors PR with all 8 phases + follow-up fixes (ANSI leak, non-retry semantics, shared captureStackTrace helper).

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@pranaygp
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); Data-driven describeRunError + expose via @workflow/core/describe-error by pranaygp · Pull Request #1839 · vercel/workflow · GitHub
Skip to content

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

Closed
pranaygp wants to merge 1 commit into
pranaygp/friendlier-errors-phase-6-consistencyfrom
pranaygp/friendlier-errors-phase-7-observability
Closed

Data-driven describeRunError + expose via @workflow/core/describe-error#1839
pranaygp wants to merge 1 commit into
pranaygp/friendlier-errors-phase-6-consistencyfrom
pranaygp/friendlier-errors-phase-7-observability

Conversation

@pranaygp

@pranaygppranaygp commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Phase 7 foundation of the friendlier-errors stack. Adds a data-driven describeRunError alongside the live-ErrordescribeError, and exposes both under a public @workflow/core/describe-error subpath so observability renderers (CLI, web UI) can use them without taking on the entire core runtime.

  • describeRunError({ errorCode, errorName }) — derives the same { attribution, hint, errorCode } shape as describeError(err), but from persisted run_failed / step_failed event fields. After hydration, renderers only have these primitives — not the original class instance — so attribution needs a data-driven path.
  • Shared hint strings — all six hint strings are now module-level constants, reused by both helpers, so terminal logs and UI banners render identical wording.
  • New ./describe-error subpath on @workflow/core so the CLI / web packages can consume these helpers without depending on the runtime entry point.
  • Tests: existing 6 live-Error tests kept; 8 new tests cover the persisted-data matrix (plain user error, SerializationError, context-violation, WorkflowRuntimeError, StepNotRegisteredError-by-name, REPLAY_TIMEOUT, MAX_DELIVERIES_EXCEEDED, missing errorCode fallback).

Scope note: deliberately scoped to the shared helper + public surface so it can merge cleanly. The UI wiring (split SDK / user banners, humanized step names, screenshot regression case) will follow separately on top of this.

Manual test plan

This PR is mostly a foundation that other renderers will consume. Test it by importing the subpath in a scratch script — no workbench needed.

Create a scratch file at the repo root:

// scratch.tsimport{describeRunError,describeError}from'@workflow/core/describe-error';import{SerializationError,WorkflowRuntimeError}from'@workflow/errors';// describeRunError — data-driven path (from persisted fields)console.log(describeRunError({errorCode: 'USER_ERROR',errorName: 'SerializationError'}));console.log(describeRunError({errorCode: 'USER_ERROR',errorName: 'NotInWorkflowContextError'}));console.log(describeRunError({errorCode: 'RUNTIME_ERROR'}));console.log(describeRunError({errorCode: 'REPLAY_TIMEOUT'}));console.log(describeRunError({errorCode: 'MAX_DELIVERIES_EXCEEDED'}));console.log(describeRunError({errorCode: 'USER_ERROR'}));// plain user, no hintconsole.log(describeRunError({errorCode: 'SOMETHING_WEIRD'}));// unknown → fallback// describeError — live-Error path (existing API)console.log(describeError(newSerializationError('boom')));console.log(describeError(newWorkflowRuntimeError('invariant')));console.log(describeError(newError('plain')));

Run with pnpm tsx scratch.ts.

  • SerializationError by name{ attribution: 'user', errorCode: 'USER_ERROR', hint: 'A value…serialized…' }
  • Context-violation by name{ attribution: 'user', hint: 'A workflow-only or step-only API…' }
  • RUNTIME_ERROR code{ attribution: 'sdk', hint: 'This is an internal workflow SDK error…' }
  • REPLAY_TIMEOUT code{ attribution: 'sdk', hint: 'The workflow replay took too long…' }
  • MAX_DELIVERIES_EXCEEDED code{ attribution: 'sdk', hint: 'The workflow queue exceeded its max-delivery budget…' }
  • Plain USER_ERROR (no errorName) → { attribution: 'user', errorCode: 'USER_ERROR' } with nohint field.
  • Unknown errorCode falls through{ attribution: 'user', errorCode: 'USER_ERROR' } (normalized).
  • Live-error path paritydescribeError(new SerializationError('x')) and describeRunError({ errorCode: 'USER_ERROR', errorName: 'SerializationError' }) return the same shape and same hint string (module-level constants should be shared).
  • Subpath import works — TypeScript resolves @workflow/core/describe-error and pnpm tsx runs without module-resolution errors. This verifies the new export in package.json / dist/ layout.

Unit tests

  • pnpm --filter @workflow/core exec vitest run src/describe-error.test.ts (14 tests)
  • dist/describe-error.js / dist/describe-error.d.ts emitted by existing build pipeline

📚 Friendlier errors stack

Multi-PR initiative inspired by @Schniz's stalled #706:

#PRPhaseSummary
1#1831Phase 1 + 2Ansi rendering primitives + context-violation errors
2#1832Phase 3Structured logger metadata; folds in #1812
3#1836Phase 4SerializationError at serialization / stream / encryption boundaries
4#1837Phase 5Presentation-only user vs SDK attribution (describeError)
5#1838Phase 6Consistency pass on remaining bare throw new Error(...) sites
6→ this PR (#1839)Phase 7 foundationData-driven describeRunError + public subpath
7#1840Phase 8WorkflowBuildError + applications in @workflow/builders
8#1849FollowupsDrop functionName leak, simplify docs framing, redirect stack to user code

Each PR is stacked on the previous one; merge in order.

🤖 Generated with Claude Code

@vercel

vercelBot commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
❌ 💻 Local Development10522861140
✅ 📦 Local Production10540861140
✅ 🐘 Local Postgres10540861140
✅ 🪟 Windows950095
✅ 📋 Other267018285
Total352222763800

❌ Failed Tests

💻 Local Development (2 failed)

vite-stable (2 failed):

  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack

Details by Category

❌ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable8906
✅ express-stable8906
✅ fastify-stable8906
✅ hono-stable8906
✅ nextjs-turbopack-canary76019
✅ nextjs-turbopack-stable9500
✅ nextjs-webpack-canary76019
✅ nextjs-webpack-stable9500
✅ nitro-stable8906
✅ nuxt-stable8906
✅ sveltekit-stable8906
❌ vite-stable8726
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable8906
✅ express-stable8906
✅ fastify-stable8906
✅ hono-stable8906
✅ nextjs-turbopack-canary76019
✅ nextjs-turbopack-stable9500
✅ nextjs-webpack-canary76019
✅ nextjs-webpack-stable9500
✅ nitro-stable8906
✅ nuxt-stable8906
✅ sveltekit-stable8906
✅ vite-stable8906
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable8906
✅ express-stable8906
✅ fastify-stable8906
✅ hono-stable8906
✅ nextjs-turbopack-canary76019
✅ nextjs-turbopack-stable9500
✅ nextjs-webpack-canary76019
✅ nextjs-webpack-stable9500
✅ nitro-stable8906
✅ nuxt-stable8906
✅ sveltekit-stable8906
✅ vite-stable8906
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack9500
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable8906
✅ e2e-local-postgres-nest-stable8906
✅ e2e-local-prod-nest-stable8906

📋 View full workflow run


Some E2E test jobs failed:

  • Vercel Prod: failure
  • Local Dev: failure
  • Local Prod: success
  • Local Postgres: success
  • Windows: success

Check the workflow run for details.

@changeset-bot

changeset-botBot commented Apr 23, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: e6b8e31

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 17 packages
NameType
@workflow/corePatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch
@workflow/aiPatch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a data-driven error description helper to complement the existing live-ErrordescribeError, and exposes these helpers via a new public @workflow/core/describe-error subpath for observability consumers (CLI/UI) to render consistent attribution/hints without depending on the full runtime entrypoint.

Changes:

  • Introduces describeRunError({ errorCode, errorName }) and shared hint string constants in packages/core/src/describe-error.ts.
  • Expands unit tests to cover the persisted-signal matrix for describeRunError.
  • Exposes a new ./describe-error export in @workflow/core and adds a changeset.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

FileDescription
packages/core/src/describe-error.tsAdds describeRunError and refactors shared hint strings for consistent attribution/hints.
packages/core/src/describe-error.test.tsAdds test coverage for the new data-driven helper.
packages/core/package.jsonExposes @workflow/core/describe-error subpath export for consumers.
.changeset/describe-error-subpath.mdDeclares a patch release documenting the new public subpath and helper.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadpackages/core/src/describe-error.ts Outdated
Comment on lines +38 to +40
*/
export interface PersistedErrorSignal {
errorCode?: RunErrorCode;

CopilotAIApr 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PersistedErrorSignal.errorCode is typed as RunErrorCode, but persisted run_failed.eventData.errorCode is currently just string | undefined (world schema allows any string for backwards compatibility). Making this field string | undefined (and coercing/validating to known RUN_ERROR_CODES internally) would avoid forcing downstream renderers to cast and prevents returning an invalid code under the RunErrorCode type.

Suggested change
*/
exportinterfacePersistedErrorSignal{
errorCode?: RunErrorCode;
*
*Notethatpersisted`errorCode`isstoredasaplainstringforbackward
*compatibilityandmustbevalidated/coercedagainst`RUN_ERROR_CODES`
*beforebeingtreatedasa`RunErrorCode`.
*/
exportinterfacePersistedErrorSignal{
errorCode?: string;

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e6b8e31PersistedErrorSignal.errorCode is now typed as string | undefined rather than RunErrorCode | undefined, matching the actual loose shape of the stored value. describeRunError runs it through a normalizeErrorCode helper that only trusts entries in RUN_ERROR_CODES and falls back to USER_ERROR otherwise.

Comment thread.changeset/describe-error-subpath.md Outdated
@@ -0,0 +1,10 @@
---
'@workflow/core': patch

CopilotAIApr 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changeset frontmatter uses single quotes around the package name, but other changesets in this repo use double quotes. Please update the frontmatter to match existing changeset formatting so automated tooling remains consistent.

Suggested change
'@workflow/core': patch
"@workflow/core": patch

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e6b8e31 — single-quoted frontmatter switched to double-quoted.

Comment threadpackages/core/src/describe-error.ts Outdated
Comment on lines +34 to +40
* event data: the classification `errorCode` plus an optional `errorName`
* (which maps to `Error#name` on the original throw). The UI does not
* have the original `Error` instance after hydration, so attribution has
* to be derived from these primitive fields.
*/
export interface PersistedErrorSignal {
errorCode?: RunErrorCode;

CopilotAIApr 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docstring claims errorName is persisted in run_failed / step_failed event data, but the current event schemas/runtime only persist errorCode plus an error payload (no separate errorName). As a result, consumers reading hydrated events won’t be able to provide errorName to describeRunError unless the runtime/world schema is updated to persist it, or this helper is adjusted to derive the name from the persisted error payload instead.

Suggested change
*event data: theclassification`errorCode`plusanoptional`errorName`
*(whichmapsto`Error#name`ontheoriginalthrow).TheUIdoesnot
*havetheoriginal`Error`instanceafterhydration,soattributionhas
*tobederivedfromtheseprimitivefields.
*/
exportinterfacePersistedErrorSignal{
errorCode?: RunErrorCode;
*event data: theclassification`errorCode`plustheserialized`error`
*payloademittedbytheruntime.Somecallersmayalsoprovide
*`errorName`asacompatibilityfield,butit is notaseparately
*persistedprimitiveinthecurrenteventschema.TheUIdoesnothave
*theoriginal`Error`instanceafterhydration,soattributionshouldbe
*derivedfrom`errorCode`plusthehydratederrorpayloadwhenneeded.
*/
exportinterfacePersistedErrorSignal{
errorCode?: RunErrorCode;
error?: {name?: string}|null;

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in e6b8e31 — the docstring now notes that errorName is not universally persisted today, and explains that callers that have it can pass it in to sharpen the attribution. describeRunError still returns a sensible result from errorCode alone when errorName is absent.

@pranaygp

Copy link
Copy Markdown
ContributorAuthor

Stacked: #1840 — Phase 8 (final): WorkflowBuildError class + applications in @workflow/builders.

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>
@pranaygp
pranaygpforce-pushed the pranaygp/friendlier-errors-phase-7-observability branch from ff48e25 to e6b8e31CompareApril 24, 2026 01:23
@pranaygp

Copy link
Copy Markdown
ContributorAuthor

Superseded by #1849 — consolidated friendlier-errors PR with all 8 phases + follow-up fixes (ANSI leak, non-retry semantics, shared captureStackTrace helper).

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@pranaygp