From e357d65c75b08d31d0ac392a2f5d38471da98e89 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 26 Nov 2025 13:46:49 +0000 Subject: [PATCH 1/6] Initial plan From 947f55c1eb234a913bbb7cab77331e07ce599015 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 26 Nov 2025 13:55:23 +0000 Subject: [PATCH 2/6] Fix workflow not being marked as failed when step exhausts max retries When a step reaches max retries or encounters a FatalError, the workflow run is now properly updated to 'failed' status instead of staying in 'running' state. This prevents the workflow from being re-queued and hitting the same failed step indefinitely. Added test case for stepMaxRetriesWorkflow to verify the fix. Co-authored-by: pranaygp <1797812+pranaygp@users.noreply.github.com> --- packages/core/e2e/e2e.test.ts | 22 ++++++++++++++++++++ packages/core/src/runtime.ts | 29 ++++++++++++++++++++++++--- workbench/example/workflows/99_e2e.ts | 15 ++++++++++++++ 3 files changed, 63 insertions(+), 3 deletions(-) diff --git a/packages/core/e2e/e2e.test.ts b/packages/core/e2e/e2e.test.ts index 92336b08e..41d3d65c1 100644 --- a/packages/core/e2e/e2e.test.ts +++ b/packages/core/e2e/e2e.test.ts @@ -776,4 +776,26 @@ describe('e2e', () => { expect(returnValue).toBe('Result: 21'); } ); + + test( + 'stepMaxRetriesWorkflow - workflow should be marked as failed after step exhausts max retries', + { timeout: 60_000 }, + async () => { + // This workflow has a step that always fails, so after max retries (3), + // the workflow should be marked as failed, not stuck in running state + const run = await triggerWorkflow('stepMaxRetriesWorkflow', []); + + // Wait for the workflow to complete (should fail) + await waitForRun(run.runId); + + // Get the workflow run data + const { json: runData } = await cliInspectJson(`runs ${run.runId}`); + + // Verify the workflow is marked as failed + expect(runData.status).toBe('failed'); + expect(runData.error).toBeDefined(); + expect(runData.error.message).toContain('failed after max retries'); + expect(runData.error.message).toContain('This step always fails'); + } + ); }); diff --git a/packages/core/src/runtime.ts b/packages/core/src/runtime.ts index edf5e8a8e..4edfc38f5 100644 --- a/packages/core/src/runtime.ts +++ b/packages/core/src/runtime.ts @@ -775,7 +775,8 @@ export const stepEntrypoint = console.error( `[Workflows] "${workflowRunId}" - Encountered \`FatalError\` while executing step "${stepName}":\n > ${stackLines.join('\n > ')}\n\nBubbling up error to parent workflow` ); - // Fatal error - store the error in the event log and re-invoke the workflow + const errorMessage = err.message || String(err); + // Fatal error - store the error in the event log and mark workflow as failed await world.events.create(workflowRunId, { eventType: 'step_failed', correlationId: stepId, @@ -788,16 +789,27 @@ export const stepEntrypoint = await world.steps.update(workflowRunId, stepId, { status: 'failed', error: { - message: err.message || String(err), + message: errorMessage, stack: errorStack, // TODO: include error codes when we define them }, }); + await world.runs.update(workflowRunId, { + status: 'failed', + error: { + message: errorMessage, + stack: errorStack, + }, + }); span?.setAttributes({ ...Attribute.StepStatus('failed'), ...Attribute.StepFatalError(true), + ...Attribute.WorkflowRunStatus('failed'), }); + + // Don't queue the workflow again - it has failed + return; } else { const maxRetries = stepFn.maxRetries ?? 3; @@ -807,7 +819,7 @@ export const stepEntrypoint = }); if (attempt >= maxRetries) { - // Max retries reached + // Max retries reached - mark workflow as failed const errorStack = getErrorStack(err); const stackLines = errorStack.split('\n').slice(0, 4); console.error( @@ -830,11 +842,22 @@ export const stepEntrypoint = stack: errorStack, }, }); + await world.runs.update(workflowRunId, { + status: 'failed', + error: { + message: errorMessage, + stack: errorStack, + }, + }); span?.setAttributes({ ...Attribute.StepStatus('failed'), ...Attribute.StepRetryExhausted(true), + ...Attribute.WorkflowRunStatus('failed'), }); + + // Don't queue the workflow again - it has failed + return; } else { // Not at max retries yet - log as a retryable error if (RetryableError.is(err)) { diff --git a/workbench/example/workflows/99_e2e.ts b/workbench/example/workflows/99_e2e.ts index df5e0b22c..ff3c6c9b9 100644 --- a/workbench/example/workflows/99_e2e.ts +++ b/workbench/example/workflows/99_e2e.ts @@ -559,3 +559,18 @@ export async function closureVariableWorkflow(baseValue: number) { const output = await calculate(); return output; } + +////////////////////////////////////////////////////////// + +// Test workflow that fails after max retries +async function stepThatAlwaysFails() { + 'use step'; + throw new Error('This step always fails'); +} + +export async function stepMaxRetriesWorkflow() { + 'use workflow'; + // This step will fail 3 times (default maxRetries) and the workflow should be marked as failed + await stepThatAlwaysFails(); + return 'never reached'; +} From 373a5a761aeadb5add73f75f7f19189ea4a174a8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 26 Nov 2025 14:09:15 +0000 Subject: [PATCH 3/6] Add test for workflow failing after step exhausts max retries The existing code already handles this correctly - when a step fails after max retries, the workflow properly fails. Added explicit test coverage to ensure this behavior is maintained. The workflow flow is: 1. Step fails 3x, gets marked as failed with fatal event 2. Workflow is queued and replays 3. During replay, step throws FatalError 4. If not caught, error bubbles to workflowEntrypoint 5. Workflow run is marked as failed This test ensures local world correctly handles step retry exhaustion. Co-authored-by: pranaygp <1797812+pranaygp@users.noreply.github.com> --- packages/core/e2e/e2e.test.ts | 11 ++++++++--- packages/core/src/runtime.ts | 29 +++-------------------------- 2 files changed, 11 insertions(+), 29 deletions(-) diff --git a/packages/core/e2e/e2e.test.ts b/packages/core/e2e/e2e.test.ts index 41d3d65c1..c3390c48a 100644 --- a/packages/core/e2e/e2e.test.ts +++ b/packages/core/e2e/e2e.test.ts @@ -785,10 +785,15 @@ describe('e2e', () => { // the workflow should be marked as failed, not stuck in running state const run = await triggerWorkflow('stepMaxRetriesWorkflow', []); - // Wait for the workflow to complete (should fail) - await waitForRun(run.runId); + // Try to get the workflow return value, which should throw/return an error + // since the workflow fails + const returnValue = await getWorkflowReturnValue(run.runId); + + // The workflow should return a WorkflowRunFailedError + expect(returnValue).toHaveProperty('name'); + expect(returnValue.name).toBe('WorkflowRunFailedError'); - // Get the workflow run data + // Get the workflow run data using CLI const { json: runData } = await cliInspectJson(`runs ${run.runId}`); // Verify the workflow is marked as failed diff --git a/packages/core/src/runtime.ts b/packages/core/src/runtime.ts index 4edfc38f5..edf5e8a8e 100644 --- a/packages/core/src/runtime.ts +++ b/packages/core/src/runtime.ts @@ -775,8 +775,7 @@ export const stepEntrypoint = console.error( `[Workflows] "${workflowRunId}" - Encountered \`FatalError\` while executing step "${stepName}":\n > ${stackLines.join('\n > ')}\n\nBubbling up error to parent workflow` ); - const errorMessage = err.message || String(err); - // Fatal error - store the error in the event log and mark workflow as failed + // Fatal error - store the error in the event log and re-invoke the workflow await world.events.create(workflowRunId, { eventType: 'step_failed', correlationId: stepId, @@ -789,27 +788,16 @@ export const stepEntrypoint = await world.steps.update(workflowRunId, stepId, { status: 'failed', error: { - message: errorMessage, + message: err.message || String(err), stack: errorStack, // TODO: include error codes when we define them }, }); - await world.runs.update(workflowRunId, { - status: 'failed', - error: { - message: errorMessage, - stack: errorStack, - }, - }); span?.setAttributes({ ...Attribute.StepStatus('failed'), ...Attribute.StepFatalError(true), - ...Attribute.WorkflowRunStatus('failed'), }); - - // Don't queue the workflow again - it has failed - return; } else { const maxRetries = stepFn.maxRetries ?? 3; @@ -819,7 +807,7 @@ export const stepEntrypoint = }); if (attempt >= maxRetries) { - // Max retries reached - mark workflow as failed + // Max retries reached const errorStack = getErrorStack(err); const stackLines = errorStack.split('\n').slice(0, 4); console.error( @@ -842,22 +830,11 @@ export const stepEntrypoint = stack: errorStack, }, }); - await world.runs.update(workflowRunId, { - status: 'failed', - error: { - message: errorMessage, - stack: errorStack, - }, - }); span?.setAttributes({ ...Attribute.StepStatus('failed'), ...Attribute.StepRetryExhausted(true), - ...Attribute.WorkflowRunStatus('failed'), }); - - // Don't queue the workflow again - it has failed - return; } else { // Not at max retries yet - log as a retryable error if (RetryableError.is(err)) { From 8e7a4ae4ce5752c1bf69a3726273354fa61ee48c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 26 Nov 2025 14:15:12 +0000 Subject: [PATCH 4/6] Fix workflow not being marked as failed when step exhausts max retries When a step reaches max retries, the workflow run is now properly updated to 'failed' status. Previously, the workflow would stay in 'running' state because the failed step would be skipped on replay, preventing the workflow from ever completing or failing. The fix ensures that after max retries: 1. Step is marked as failed with fatal event 2. Workflow run is immediately marked as failed 3. Workflow is NOT queued again (preventing infinite loop) FatalErrors are still handled correctly - they allow the workflow code to catch and handle the error gracefully. Added test coverage for stepMaxRetriesWorkflow scenario. Added changeset for the fix. Co-authored-by: pranaygp <1797812+pranaygp@users.noreply.github.com> --- .../fix-workflow-status-after-max-retries.md | 7 +++++++ packages/core/src/runtime.ts | 15 +++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 .changeset/fix-workflow-status-after-max-retries.md diff --git a/.changeset/fix-workflow-status-after-max-retries.md b/.changeset/fix-workflow-status-after-max-retries.md new file mode 100644 index 000000000..d2fa0857a --- /dev/null +++ b/.changeset/fix-workflow-status-after-max-retries.md @@ -0,0 +1,7 @@ +--- +"@workflow/core": patch +--- + +Fix workflow run not marked as failed when step exhausts max retries + +When a step fails after max retries, the workflow run is now properly marked as 'failed' instead of staying in 'running' state. This prevents workflows from getting stuck when a step cannot recover. diff --git a/packages/core/src/runtime.ts b/packages/core/src/runtime.ts index edf5e8a8e..c2c1194fe 100644 --- a/packages/core/src/runtime.ts +++ b/packages/core/src/runtime.ts @@ -807,11 +807,11 @@ export const stepEntrypoint = }); if (attempt >= maxRetries) { - // Max retries reached + // Max retries reached - mark workflow as failed const errorStack = getErrorStack(err); const stackLines = errorStack.split('\n').slice(0, 4); console.error( - `[Workflows] "${workflowRunId}" - Encountered \`Error\` while executing step "${stepName}" (attempt ${attempt}):\n > ${stackLines.join('\n > ')}\n\n Max retries reached\n Bubbling error to parent workflow` + `[Workflows] "${workflowRunId}" - Encountered \`Error\` while executing step "${stepName}" (attempt ${attempt}):\n > ${stackLines.join('\n > ')}\n\n Max retries reached\n Marking workflow as failed` ); const errorMessage = `Step "${stepName}" failed after max retries: ${String(err)}`; await world.events.create(workflowRunId, { @@ -830,11 +830,22 @@ export const stepEntrypoint = stack: errorStack, }, }); + await world.runs.update(workflowRunId, { + status: 'failed', + error: { + message: errorMessage, + stack: errorStack, + }, + }); span?.setAttributes({ ...Attribute.StepStatus('failed'), ...Attribute.StepRetryExhausted(true), + ...Attribute.WorkflowRunStatus('failed'), }); + + // Don't queue the workflow again - it has failed + return; } else { // Not at max retries yet - log as a retryable error if (RetryableError.is(err)) { From 7990de162806e8d789a246ec3c95cca38abc60a0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 26 Nov 2025 15:47:29 +0000 Subject: [PATCH 5/6] Revert incorrect fix: let step errors bubble up to workflow function Based on feedback, the approach was wrong. When a step exhausts max retries, the error should propagate up to the workflow function first, not immediately mark the workflow as failed. Only if the workflow function has an unhandled error should the workflow run enter a failed state. The original behavior was correct - the step is marked as failed and the workflow is queued for replay, allowing the error to bubble up to the workflow code where it can be handled (via try/catch) or left unhandled (which then fails the workflow run through the normal error handling path). Co-authored-by: TooTallNate <71256+TooTallNate@users.noreply.github.com> --- .../fix-workflow-status-after-max-retries.md | 7 ----- packages/core/e2e/e2e.test.ts | 27 ------------------- packages/core/src/runtime.ts | 15 ++--------- workbench/example/workflows/99_e2e.ts | 15 ----------- 4 files changed, 2 insertions(+), 62 deletions(-) delete mode 100644 .changeset/fix-workflow-status-after-max-retries.md diff --git a/.changeset/fix-workflow-status-after-max-retries.md b/.changeset/fix-workflow-status-after-max-retries.md deleted file mode 100644 index d2fa0857a..000000000 --- a/.changeset/fix-workflow-status-after-max-retries.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@workflow/core": patch ---- - -Fix workflow run not marked as failed when step exhausts max retries - -When a step fails after max retries, the workflow run is now properly marked as 'failed' instead of staying in 'running' state. This prevents workflows from getting stuck when a step cannot recover. diff --git a/packages/core/e2e/e2e.test.ts b/packages/core/e2e/e2e.test.ts index c3390c48a..92336b08e 100644 --- a/packages/core/e2e/e2e.test.ts +++ b/packages/core/e2e/e2e.test.ts @@ -776,31 +776,4 @@ describe('e2e', () => { expect(returnValue).toBe('Result: 21'); } ); - - test( - 'stepMaxRetriesWorkflow - workflow should be marked as failed after step exhausts max retries', - { timeout: 60_000 }, - async () => { - // This workflow has a step that always fails, so after max retries (3), - // the workflow should be marked as failed, not stuck in running state - const run = await triggerWorkflow('stepMaxRetriesWorkflow', []); - - // Try to get the workflow return value, which should throw/return an error - // since the workflow fails - const returnValue = await getWorkflowReturnValue(run.runId); - - // The workflow should return a WorkflowRunFailedError - expect(returnValue).toHaveProperty('name'); - expect(returnValue.name).toBe('WorkflowRunFailedError'); - - // Get the workflow run data using CLI - const { json: runData } = await cliInspectJson(`runs ${run.runId}`); - - // Verify the workflow is marked as failed - expect(runData.status).toBe('failed'); - expect(runData.error).toBeDefined(); - expect(runData.error.message).toContain('failed after max retries'); - expect(runData.error.message).toContain('This step always fails'); - } - ); }); diff --git a/packages/core/src/runtime.ts b/packages/core/src/runtime.ts index c2c1194fe..edf5e8a8e 100644 --- a/packages/core/src/runtime.ts +++ b/packages/core/src/runtime.ts @@ -807,11 +807,11 @@ export const stepEntrypoint = }); if (attempt >= maxRetries) { - // Max retries reached - mark workflow as failed + // Max retries reached const errorStack = getErrorStack(err); const stackLines = errorStack.split('\n').slice(0, 4); console.error( - `[Workflows] "${workflowRunId}" - Encountered \`Error\` while executing step "${stepName}" (attempt ${attempt}):\n > ${stackLines.join('\n > ')}\n\n Max retries reached\n Marking workflow as failed` + `[Workflows] "${workflowRunId}" - Encountered \`Error\` while executing step "${stepName}" (attempt ${attempt}):\n > ${stackLines.join('\n > ')}\n\n Max retries reached\n Bubbling error to parent workflow` ); const errorMessage = `Step "${stepName}" failed after max retries: ${String(err)}`; await world.events.create(workflowRunId, { @@ -830,22 +830,11 @@ export const stepEntrypoint = stack: errorStack, }, }); - await world.runs.update(workflowRunId, { - status: 'failed', - error: { - message: errorMessage, - stack: errorStack, - }, - }); span?.setAttributes({ ...Attribute.StepStatus('failed'), ...Attribute.StepRetryExhausted(true), - ...Attribute.WorkflowRunStatus('failed'), }); - - // Don't queue the workflow again - it has failed - return; } else { // Not at max retries yet - log as a retryable error if (RetryableError.is(err)) { diff --git a/workbench/example/workflows/99_e2e.ts b/workbench/example/workflows/99_e2e.ts index ff3c6c9b9..df5e0b22c 100644 --- a/workbench/example/workflows/99_e2e.ts +++ b/workbench/example/workflows/99_e2e.ts @@ -559,18 +559,3 @@ export async function closureVariableWorkflow(baseValue: number) { const output = await calculate(); return output; } - -////////////////////////////////////////////////////////// - -// Test workflow that fails after max retries -async function stepThatAlwaysFails() { - 'use step'; - throw new Error('This step always fails'); -} - -export async function stepMaxRetriesWorkflow() { - 'use workflow'; - // This step will fail 3 times (default maxRetries) and the workflow should be marked as failed - await stepThatAlwaysFails(); - return 'never reached'; -} From 2964d3baa42a5084f96de64899644aa67c63d28a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 26 Nov 2025 17:08:23 +0000 Subject: [PATCH 6/6] Add e2e test for workflow failing when step exhausts max retries Added a test workflow (stepExhaustsRetriesWorkflow) that has a step which always throws a regular error. This verifies that when a step exhausts its max retries (default 3), the error bubbles up to the workflow function, and if unhandled, the workflow run enters a failed state with the appropriate error message. Co-authored-by: TooTallNate <71256+TooTallNate@users.noreply.github.com> --- docs/app/(home)/components/frameworks.tsx | 74 +++++++++++------------ packages/core/e2e/e2e.test.ts | 23 +++++++ workbench/example/workflows/99_e2e.ts | 18 ++++++ 3 files changed, 78 insertions(+), 37 deletions(-) diff --git a/docs/app/(home)/components/frameworks.tsx b/docs/app/(home)/components/frameworks.tsx index 1d21d9755..041871417 100644 --- a/docs/app/(home)/components/frameworks.tsx +++ b/docs/app/(home)/components/frameworks.tsx @@ -1,12 +1,12 @@ -"use client"; +'use client'; -import { track } from "@vercel/analytics"; -import Link from "next/link"; -import type { ComponentProps } from "react"; -import { toast } from "sonner"; -import { Badge } from "@/components/ui/badge"; +import { track } from '@vercel/analytics'; +import Link from 'next/link'; +import type { ComponentProps } from 'react'; +import { toast } from 'sonner'; +import { Badge } from '@/components/ui/badge'; -export const Express = (props: ComponentProps<"svg">) => ( +export const Express = (props: ComponentProps<'svg'>) => ( ) => ( ); -export const AstroDark = (props: ComponentProps<"svg">) => ( +export const AstroDark = (props: ComponentProps<'svg'>) => ( ) => ( ); -export const AstroLight = (props: ComponentProps<"svg">) => ( +export const AstroLight = (props: ComponentProps<'svg'>) => ( ) => ( ); -export const AstroGray = (props: ComponentProps<"svg">) => ( +export const AstroGray = (props: ComponentProps<'svg'>) => ( ) => ( ); -export const TanStack = (props: ComponentProps<"svg">) => ( +export const TanStack = (props: ComponentProps<'svg'>) => ( ) => ( ); -export const TanStackGray = (props: ComponentProps<"svg">) => ( +export const TanStackGray = (props: ComponentProps<'svg'>) => ( ) => ( ); -export const Vite = (props: ComponentProps<"svg">) => ( +export const Vite = (props: ComponentProps<'svg'>) => ( ) => ( ); -export const Nitro = (props: ComponentProps<"svg">) => ( +export const Nitro = (props: ComponentProps<'svg'>) => ( ) => ( /> ) => ( ); -export const SvelteKit = (props: ComponentProps<"svg">) => ( +export const SvelteKit = (props: ComponentProps<'svg'>) => ( ) => ( ); -export const SvelteKitGray = (props: ComponentProps<"svg">) => ( +export const SvelteKitGray = (props: ComponentProps<'svg'>) => ( ) => ( ); -export const Nuxt = (props: ComponentProps<"svg">) => ( +export const Nuxt = (props: ComponentProps<'svg'>) => ( ) => ( ); -export const NuxtGray = (props: ComponentProps<"svg">) => ( +export const NuxtGray = (props: ComponentProps<'svg'>) => ( ) => ( ); -export const Hono = (props: ComponentProps<"svg">) => ( +export const Hono = (props: ComponentProps<'svg'>) => ( Hono ) => ( ); -export const HonoGray = (props: ComponentProps<"svg">) => ( +export const HonoGray = (props: ComponentProps<'svg'>) => ( Hono ) => ( ); -export const Bun = (props: ComponentProps<"svg">) => ( +export const Bun = (props: ComponentProps<'svg'>) => ( ) => ( id="Top" d="M35.12,5.53A16.41,16.41,0,0,1,29.49,18c-.28.25-.06.73.3.59,3.37-1.31,7.92-5.23,6-13.14C35.71,5,35.12,5.12,35.12,5.53Zm2.27,0A16.24,16.24,0,0,1,39,19c-.12.35.31.65.55.36C41.74,16.56,43.65,11,37.93,5,37.64,4.74,37.19,5.14,37.39,5.49Zm2.76-.17A16.42,16.42,0,0,1,47,17.12a.33.33,0,0,0,.65.11c.92-3.49.4-9.44-7.17-12.53C40.08,4.54,39.82,5.08,40.15,5.32ZM21.69,15.76a16.94,16.94,0,0,0,10.47-9c.18-.36.75-.22.66.18-1.73,8-7.52,9.67-11.12,9.45C21.32,16.4,21.33,15.87,21.69,15.76Z" fill="#ccbea7" - style={{ fillRule: "evenodd" }} + style={{ fillRule: 'evenodd' }} /> ) => ( ); -export const BunGray = (props: ComponentProps<"svg">) => ( +export const BunGray = (props: ComponentProps<'svg'>) => ( ) => ( id="Top" d="M35.12,5.53A16.41,16.41,0,0,1,29.49,18c-.28.25-.06.73.3.59,3.37-1.31,7.92-5.23,6-13.14C35.71,5,35.12,5.12,35.12,5.53Zm2.27,0A16.24,16.24,0,0,1,39,19c-.12.35.31.65.55.36C41.74,16.56,43.65,11,37.93,5,37.64,4.74,37.19,5.14,37.39,5.49Zm2.76-.17A16.42,16.42,0,0,1,47,17.12a.33.33,0,0,0,.65.11c.92-3.49.4-9.44-7.17-12.53C40.08,4.54,39.82,5.08,40.15,5.32ZM21.69,15.76a16.94,16.94,0,0,0,10.47-9c.18-.36.75-.22.66.18-1.73,8-7.52,9.67-11.12,9.45C21.32,16.4,21.33,15.87,21.69,15.76Z" fill="var(--color-background)" - style={{ fillRule: "evenodd" }} + style={{ fillRule: 'evenodd' }} /> ) => ( ); -export const Nest = (props: ComponentProps<"svg">) => ( +export const Nest = (props: ComponentProps<'svg'>) => ( ) => ( ); -export const NestGray = (props: ComponentProps<"svg">) => ( +export const NestGray = (props: ComponentProps<'svg'>) => ( ) => ( ); -export const Next = (props: ComponentProps<"svg">) => ( +export const Next = (props: ComponentProps<'svg'>) => ( Next.js @@ -681,8 +681,8 @@ export const Next = (props: ComponentProps<"svg">) => ( export const Frameworks = () => { const handleRequest = (framework: string) => { - track("Framework requested", { framework: framework.toLowerCase() }); - toast.success("Request received", { + track('Framework requested', { framework: framework.toLowerCase() }); + toast.success('Request received', { description: `Thanks for expressing interest in ${framework}. We will be adding support for it soon.`, }); }; @@ -742,21 +742,21 @@ export const Frameworks = () => {
handleRequest("NestJS")} + onClick={() => handleRequest('NestJS')} >
handleRequest("TanStack")} + onClick={() => handleRequest('TanStack')} >
handleRequest("Astro")} + onClick={() => handleRequest('Astro')} > diff --git a/packages/core/e2e/e2e.test.ts b/packages/core/e2e/e2e.test.ts index 92336b08e..a1a701d61 100644 --- a/packages/core/e2e/e2e.test.ts +++ b/packages/core/e2e/e2e.test.ts @@ -776,4 +776,27 @@ describe('e2e', () => { expect(returnValue).toBe('Result: 21'); } ); + + test( + 'stepExhaustsRetriesWorkflow - workflow fails when step exhausts max retries', + { timeout: 120_000 }, + async () => { + // This workflow has a step that always throws a regular error. + // After exhausting max retries (default 3), the error should bubble up + // to the workflow function. Since the workflow doesn't handle it, + // the workflow run should enter a failed state. + const run = await triggerWorkflow('stepExhaustsRetriesWorkflow', []); + const returnValue = await getWorkflowReturnValue(run.runId); + + // The workflow should fail with WorkflowRunFailedError + expect(returnValue).toHaveProperty('name'); + expect(returnValue.name).toBe('WorkflowRunFailedError'); + + // Verify the run is in failed status + const { json: runData } = await cliInspectJson(`runs ${run.runId}`); + expect(runData.status).toBe('failed'); + expect(runData.error).toBeDefined(); + expect(runData.error.message).toContain('failed after max retries'); + } + ); }); diff --git a/workbench/example/workflows/99_e2e.ts b/workbench/example/workflows/99_e2e.ts index df5e0b22c..3b2f5e208 100644 --- a/workbench/example/workflows/99_e2e.ts +++ b/workbench/example/workflows/99_e2e.ts @@ -559,3 +559,21 @@ export async function closureVariableWorkflow(baseValue: number) { const output = await calculate(); return output; } + +////////////////////////////////////////////////////////// + +// Step that always throws a regular error (not FatalError) +// This will exhaust max retries and the error should bubble up to the workflow +async function stepThatAlwaysFailsWithRegularError() { + 'use step'; + throw new Error('This step always fails with a regular error'); +} + +// Workflow that calls a step that always fails +// The step will exhaust max retries and the error should bubble up, +// causing the workflow run to enter a failed state +export async function stepExhaustsRetriesWorkflow() { + 'use workflow'; + await stepThatAlwaysFailsWithRegularError(); + return 'never reached'; +}