Skip to content

feat(workflow-engine): treat undefined loop return as continue for stateless loops#4294

Merged
NathanFlurry merged 1 commit intomainfrom
02-24-feat_workflow-engine_treat_undefined_loop_return_as_continue_for_stateless_loops
Feb 24, 2026
Merged

feat(workflow-engine): treat undefined loop return as continue for stateless loops#4294
NathanFlurry merged 1 commit intomainfrom
02-24-feat_workflow-engine_treat_undefined_loop_return_as_continue_for_stateless_loops

Conversation

@NathanFlurry
Copy link
Member

Description

Please include a summary of the changes and the related issue. Please also include relevant motivation and context.

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

How Has This Been Tested?

Please describe the tests that you ran to verify your changes.

Checklist:

  • My code follows the style guidelines of this project
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes

@railway-app railway-app bot temporarily deployed to rivet-frontend / rivet-pr-4294 February 24, 2026 10:56 Destroyed
@railway-app
Copy link

railway-app bot commented Feb 24, 2026

🚅 Deployed to the rivet-pr-4294 environment in rivet-frontend

Service Status Web Updated (UTC)
mcp-hub ✅ Success (View Logs) Web Feb 24, 2026 at 10:57 am
ladle ❌ Build Failed (View Logs) Web Feb 24, 2026 at 10:56 am
frontend-cloud 🕒 Building (View Logs) Web Feb 24, 2026 at 10:56 am
frontend-inspector 🕒 Building (View Logs) Web Feb 24, 2026 at 10:56 am
website 🕒 Building (View Logs) Web Feb 24, 2026 at 10:56 am

Copy link
Member Author

NathanFlurry commented Feb 24, 2026


How to use the Graphite Merge Queue

Add the label merge-queue to this PR to add it to the merge queue.

You must have a Graphite account in order to use the merge queue. Sign up using this link.

An organization admin has enabled the Graphite Merge Queue in this repository.

Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue.

This stack of pull requests is managed by Graphite. Learn more about stacking.

@claude
Copy link

claude bot commented Feb 24, 2026

PR Review: feat(workflow-engine): treat undefined loop return as continue for stateless loops

The overall approach is solid and well-motivated — returning undefined from a stateless loop callback is a natural ergonomic shorthand for Loop.continue(). The implementation is focused and clean.

Issues

1. Distributive conditional type allows undefined in union state types (type-level gap)

export type LoopIterationResult<S, T> = Promise<
    LoopResult<S, T> | (S extends undefined ? undefined : never)
>;

TypeScript's conditional types distribute over unions, so when S = string | undefined, the expression evaluates as:

  • (string extends undefined ? undefined : never) | (undefined extends undefined ? undefined : never)
  • = never | undefined
  • = undefined

This means LoopIterationResult<string | undefined, T> incorrectly permits returning undefined at the TypeScript level, even though the intent is that only fully stateless (S = undefined) loops should allow it. The runtime check state \!== undefined will correctly throw at runtime, but TypeScript won't surface this as a compile-time error.

To be precise at the type level, this could use a non-distributive form:

export type LoopIterationResult<S, T> = Promise<
    LoopResult<S, T> | ([S] extends [undefined] ? undefined : never)
>;

Wrapping in a tuple [S] prevents distribution over unions. This is probably an esoteric edge case, but it's worth getting right.

2. Missing negative test case for the runtime guard

The PR adds a guard that throws when a stateful loop returns undefined:

if (iterationResult === undefined && state \!== undefined) {
    throw new Error(...)
}

There's no test that verifies this path. Adding a test confirming this error is thrown for stateful loops (e.g., a loop with state: { count: 0 } that returns undefined) would improve confidence in the guard.

3. Minor: inconsistent type usage in context.ts

In the loop method overload implementation, the run parameter uses the expanded form:

run?: (ctx: WorkflowContextInterface) => Promise<LoopResult<undefined, T> | undefined>,

While the interface in types.ts uses LoopIterationResult<undefined, T> directly. They're equivalent, but using LoopIterationResult here would be more consistent and easier to maintain if the type changes in the future.

What looks good

  • The runtime guard correctly protects against stateful loops returning undefined regardless of TypeScript types
  • The as LoopResult<S, T> cast when constructing the implicit continue is safe given the preceding guard
  • The JSDoc on LoopIterationResult clearly explains the behavior
  • The test covers the primary use case (stateless loop with undefined return completing successfully)
  • The error message in the guard is descriptive and actionable

@NathanFlurry NathanFlurry merged commit 0471fc1 into main Feb 24, 2026
8 of 22 checks passed
@NathanFlurry NathanFlurry deleted the 02-24-feat_workflow-engine_treat_undefined_loop_return_as_continue_for_stateless_loops branch February 24, 2026 10:59
Sign up for free to 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.

1 participant