Materialize waits as entities to prevent duplicate wait_completed events - #1057

Merged
pranaygp merged 10 commits into
mainfrom
pgp/wait-complete-guard
Feb 17, 2026
Merged

Materialize waits as entities to prevent duplicate wait_completed events#1057
pranaygp merged 10 commits into
mainfrom
pgp/wait-complete-guard

Conversation

@pranaygp

@pranaygppranaygp commented Feb 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds wait entity materialization to the local and postgres world implementations, matching the server-side DynamoDB behavior in workflow-server PR #265. This prevents duplicate wait_completed events by creating wait entities with status tracking and conditional writes.

Changes by package

@workflow/world

  • New waits.ts with Wait type, WaitSchema, and WaitStatusSchema
  • Added optional wait field to EventResult interface

@workflow/world-local

  • wait_created creates a wait entity file with status: 'waiting'; throws 409 if already exists
  • wait_completed transitions to status: 'completed'; throws 404 if not found, 409 if already completed
  • Waits are cleaned up on terminal run states (run_completed, run_failed, run_cancelled) via deleteAllWaitsForRun
  • wait_created blocked on terminal runs (can't create new entities)

@workflow/world-postgres

  • New workflow_waits table with wait_status enum (migration 0007_add_waits_table)
  • wait_created uses INSERT with onConflictDoNothing + check for 409
  • wait_completed uses conditional UPDATE (WHERE status = 'waiting') with fallback SELECT to distinguish 404 vs 409
  • Waits cleaned up on terminal run states and in legacy handler

@workflow/core

  • Handles 409 conflict gracefully in wait_completed event creation (concurrent VQS invocations)

Server-side counterpart: https://github.com/vercel/workflow-server/pull/265

Test plan

  • Local world unit tests pass (pnpm test --filter world-local)
  • Postgres world unit tests pass (pnpm test --filter world-postgres)
  • TypeScript compiles cleanly
  • E2E Vercel Prod tests pass against server preview branch
  • Clear WORKFLOW_SERVER_URL_OVERRIDE before merging

🤖 Generated with Claude Code

CopilotAI review requested due to automatic review settings February 14, 2026 01:25
@vercel

vercelBot commented Feb 14, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented Feb 14, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 04f58a3

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

This PR includes changesets to release 18 packages
NameType
@workflow/corePatch
@workflow/worldPatch
@workflow/world-localPatch
@workflow/world-postgresPatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/web-sharedPatch
workflowPatch
@workflow/world-testingPatch
@workflow/world-vercelPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

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

@github-actions

github-actionsBot commented Feb 14, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production512038550
✅ 💻 Local Development532068600
✅ 📦 Local Production532068600
✅ 🐘 Local Postgres532068600
✅ 🪟 Windows470350
❌ 🌍 Community Worlds106449159
✅ 📋 Other129021150
Total2390442752709

❌ Failed Tests

🌍 Community Worlds (44 failed)

mongodb (1 failed):

  • webhookWorkflow

turso (43 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • hookWorkflow
  • webhookWorkflow
  • sleepingWorkflow
  • parallelSleepWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • 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
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling retry behavior workflow completes despite transient 5xx on step_completed
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • health check (queue-based) - workflow and step endpoints respond to health check messages
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • instanceMethodStepWorkflow - instance methods with "use step" directive
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context
  • stepFunctionAsStartArgWorkflow - step function reference passed as start() argument
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro4604
✅ example4604
✅ express4604
✅ fastify4604
✅ hono4604
✅ nextjs-turbopack4901
✅ nextjs-webpack4901
✅ nitro4604
✅ nuxt4604
✅ sveltekit4604
✅ vite4604
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable4307
✅ express-stable4307
✅ fastify-stable4307
✅ hono-stable4307
✅ nextjs-turbopack-canary4703
✅ nextjs-turbopack-stable4703
✅ nextjs-webpack-canary4703
✅ nextjs-webpack-stable4703
✅ nitro-stable4307
✅ nuxt-stable4307
✅ sveltekit-stable4307
✅ vite-stable4307
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable4307
✅ express-stable4307
✅ fastify-stable4307
✅ hono-stable4307
✅ nextjs-turbopack-canary4703
✅ nextjs-turbopack-stable4703
✅ nextjs-webpack-canary4703
✅ nextjs-webpack-stable4703
✅ nitro-stable4307
✅ nuxt-stable4307
✅ sveltekit-stable4307
✅ vite-stable4307
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable4307
✅ express-stable4307
✅ fastify-stable4307
✅ hono-stable4307
✅ nextjs-turbopack-canary4703
✅ nextjs-turbopack-stable4703
✅ nextjs-webpack-canary4703
✅ nextjs-webpack-stable4703
✅ nitro-stable4307
✅ nuxt-stable4307
✅ sveltekit-stable4307
✅ vite-stable4307
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack4703
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev300
❌ mongodb4613
✅ redis-dev300
✅ redis4703
✅ turso-dev300
❌ turso4433
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable4307
✅ e2e-local-postgres-nest-stable4307
✅ e2e-local-prod-nest-stable4307

📋 View full workflow run

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

This PR adds graceful handling for 409 Conflict responses when creating wait_completed events, addressing race conditions when multiple concurrent workflow invocations attempt to complete the same wait. The changes build on PR #1055 which added orphaned event detection, and work in conjunction with server-side changes that materialize waits as entities to prevent duplicate completions.

Changes:

  • Wraps wait_completed event creation in try/catch blocks to handle 409 conflicts gracefully
  • Applies 409 handling in both automatic wait completion (runtime.ts) and manual wake-up (runs.ts)
  • Updates event sourcing documentation to reflect that waits are now materialized entities in storage

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.

Show a summary per file
FileDescription
packages/core/src/runtime/runs.tsAdds 409 conflict handling in wakeUpRun() to count already-completed waits as successful
packages/core/src/runtime/runs.test.tsNew test file with unit tests for 409 handling and error cases in wakeUpRun()
packages/core/src/runtime.tsWraps automatic wait_completed event creation in try/catch to log and skip on 409 conflicts
docs/content/docs/how-it-works/event-sourcing.mdxUpdates documentation to clarify waits are materialized entities with atomic completion guarantees
.changeset/wait-complete-guard.mdAdds changeset for patch release describing the fix

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

pranaygpand others added 4 commits February 15, 2026 17:19
When multiple concurrent workflow invocations race to complete the same
wait, the server returns 409 (conflict) for duplicates. This change
handles the 409 gracefully in both runtime.ts (sleep elapsed check) and
runs.ts (wakeUpRun), preventing crashes and treating already-completed
waits as successful.
Also updates event-sourcing docs to reflect that waits are now
materialized as entities in storage with atomic completion guarantees.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ranch
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… duplicate wait_completed events
Adds Wait type/schema to the shared world package and implements wait entity
materialization in both local (filesystem) and postgres world implementations,
matching the DynamoDB behavior. wait_created creates a wait entity with status
'waiting', and wait_completed transitions it to 'completed' with guards that
reject duplicates (409).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
pranaygpand others added 4 commits February 15, 2026 17:54
…ation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* origin/main:
add closure comment in sleep test helper (#1071)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…rride
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM, but can we add (or modify) one e2e test to include Promise.all(sleep(1s) x 10) or similar? That'd probably ensure general race-condition safety for waits into the future

@pranaygp

pranaygp commented Feb 16, 2026

Copy link
Copy Markdown
ContributorAuthor

LGTM, but can we add (or modify) one e2e test to include Promise.all(sleep(1s) x 10) or similar? That'd probably ensure general race-condition safety for waits into the future

I already did that in the previous PR and it's been merged.

exportasyncfunctionparallelSleepWorkflow(){
'use workflow';
conststartTime=Date.now();
awaitPromise.all(Array.from({length: 10},()=>sleep('1s')));
constendTime=Date.now();
return{ startTime, endTime };
}

@pranaygp

Copy link
Copy Markdown
ContributorAuthor

Replying to @TooTallNate's code review:

Backward Compatibility

Good call — both local and postgres would throw 404 for waits created before this code was deployed. For Vercel production this is handled server-side (the backend backfills the wait entity in completed state on wait_completed if it doesn't exist — see vercel/workflow-server#265). For self-hosted postgres and local dev, I agree we should add backfill logic. I'll add that as a follow-up.

waitId Inconsistency

Fixed in a5e093c — both backends now use the raw correlationId as waitId, consistent with how steps and hooks use their correlation IDs as PKs.

Runtime 409 Handling

Agreed it causes one wasted invocation. The self-healing behavior is correct though — the next invocation picks up the persisted event and proceeds. I'll note this as an optimization for a follow-up (re-fetch the event on 409 and push it to the array to avoid the extra round-trip).

wakeUpRun 409 Handling

👍


Re: "world-postgres doesn't seem too happy" — this was fixed, the migration journal entry was missing. Added in ce2eb72.

pranaygpand others added 2 commits February 16, 2026 15:53
The server-side changes (vercel/workflow-server#265) have been merged to main
and deployed to production, so we no longer need to point at the preview URL.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* origin/main:
Remove "workflow/internal/serialization" export (#1082)
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.

4 participants

@pranaygp@TooTallNate@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n 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;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Materialize waits as entities to prevent duplicate wait_completed events - #1057

Merged
pranaygp merged 10 commits into
mainfrom
pgp/wait-complete-guard
Feb 17, 2026
Merged

Materialize waits as entities to prevent duplicate wait_completed events#1057
pranaygp merged 10 commits into
mainfrom
pgp/wait-complete-guard

Conversation

@pranaygp

@pranaygppranaygp commented Feb 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds wait entity materialization to the local and postgres world implementations, matching the server-side DynamoDB behavior in workflow-server PR #265. This prevents duplicate wait_completed events by creating wait entities with status tracking and conditional writes.

Changes by package

@workflow/world

  • New waits.ts with Wait type, WaitSchema, and WaitStatusSchema
  • Added optional wait field to EventResult interface

@workflow/world-local

  • wait_created creates a wait entity file with status: 'waiting'; throws 409 if already exists
  • wait_completed transitions to status: 'completed'; throws 404 if not found, 409 if already completed
  • Waits are cleaned up on terminal run states (run_completed, run_failed, run_cancelled) via deleteAllWaitsForRun
  • wait_created blocked on terminal runs (can't create new entities)

@workflow/world-postgres

  • New workflow_waits table with wait_status enum (migration 0007_add_waits_table)
  • wait_created uses INSERT with onConflictDoNothing + check for 409
  • wait_completed uses conditional UPDATE (WHERE status = 'waiting') with fallback SELECT to distinguish 404 vs 409
  • Waits cleaned up on terminal run states and in legacy handler

@workflow/core

  • Handles 409 conflict gracefully in wait_completed event creation (concurrent VQS invocations)

Server-side counterpart: https://github.com/vercel/workflow-server/pull/265

Test plan

  • Local world unit tests pass (pnpm test --filter world-local)
  • Postgres world unit tests pass (pnpm test --filter world-postgres)
  • TypeScript compiles cleanly
  • E2E Vercel Prod tests pass against server preview branch
  • Clear WORKFLOW_SERVER_URL_OVERRIDE before merging

🤖 Generated with Claude Code

CopilotAI review requested due to automatic review settings February 14, 2026 01:25
@vercel

vercelBot commented Feb 14, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented Feb 14, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 04f58a3

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

This PR includes changesets to release 18 packages
NameType
@workflow/corePatch
@workflow/worldPatch
@workflow/world-localPatch
@workflow/world-postgresPatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/web-sharedPatch
workflowPatch
@workflow/world-testingPatch
@workflow/world-vercelPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

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

@github-actions

github-actionsBot commented Feb 14, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production512038550
✅ 💻 Local Development532068600
✅ 📦 Local Production532068600
✅ 🐘 Local Postgres532068600
✅ 🪟 Windows470350
❌ 🌍 Community Worlds106449159
✅ 📋 Other129021150
Total2390442752709

❌ Failed Tests

🌍 Community Worlds (44 failed)

mongodb (1 failed):

  • webhookWorkflow

turso (43 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • hookWorkflow
  • webhookWorkflow
  • sleepingWorkflow
  • parallelSleepWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • 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
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling retry behavior workflow completes despite transient 5xx on step_completed
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • health check (queue-based) - workflow and step endpoints respond to health check messages
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • instanceMethodStepWorkflow - instance methods with "use step" directive
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context
  • stepFunctionAsStartArgWorkflow - step function reference passed as start() argument
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro4604
✅ example4604
✅ express4604
✅ fastify4604
✅ hono4604
✅ nextjs-turbopack4901
✅ nextjs-webpack4901
✅ nitro4604
✅ nuxt4604
✅ sveltekit4604
✅ vite4604
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable4307
✅ express-stable4307
✅ fastify-stable4307
✅ hono-stable4307
✅ nextjs-turbopack-canary4703
✅ nextjs-turbopack-stable4703
✅ nextjs-webpack-canary4703
✅ nextjs-webpack-stable4703
✅ nitro-stable4307
✅ nuxt-stable4307
✅ sveltekit-stable4307
✅ vite-stable4307
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable4307
✅ express-stable4307
✅ fastify-stable4307
✅ hono-stable4307
✅ nextjs-turbopack-canary4703
✅ nextjs-turbopack-stable4703
✅ nextjs-webpack-canary4703
✅ nextjs-webpack-stable4703
✅ nitro-stable4307
✅ nuxt-stable4307
✅ sveltekit-stable4307
✅ vite-stable4307
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable4307
✅ express-stable4307
✅ fastify-stable4307
✅ hono-stable4307
✅ nextjs-turbopack-canary4703
✅ nextjs-turbopack-stable4703
✅ nextjs-webpack-canary4703
✅ nextjs-webpack-stable4703
✅ nitro-stable4307
✅ nuxt-stable4307
✅ sveltekit-stable4307
✅ vite-stable4307
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack4703
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev300
❌ mongodb4613
✅ redis-dev300
✅ redis4703
✅ turso-dev300
❌ turso4433
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable4307
✅ e2e-local-postgres-nest-stable4307
✅ e2e-local-prod-nest-stable4307

📋 View full workflow run

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

This PR adds graceful handling for 409 Conflict responses when creating wait_completed events, addressing race conditions when multiple concurrent workflow invocations attempt to complete the same wait. The changes build on PR #1055 which added orphaned event detection, and work in conjunction with server-side changes that materialize waits as entities to prevent duplicate completions.

Changes:

  • Wraps wait_completed event creation in try/catch blocks to handle 409 conflicts gracefully
  • Applies 409 handling in both automatic wait completion (runtime.ts) and manual wake-up (runs.ts)
  • Updates event sourcing documentation to reflect that waits are now materialized entities in storage

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.

Show a summary per file
FileDescription
packages/core/src/runtime/runs.tsAdds 409 conflict handling in wakeUpRun() to count already-completed waits as successful
packages/core/src/runtime/runs.test.tsNew test file with unit tests for 409 handling and error cases in wakeUpRun()
packages/core/src/runtime.tsWraps automatic wait_completed event creation in try/catch to log and skip on 409 conflicts
docs/content/docs/how-it-works/event-sourcing.mdxUpdates documentation to clarify waits are materialized entities with atomic completion guarantees
.changeset/wait-complete-guard.mdAdds changeset for patch release describing the fix

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

pranaygpand others added 4 commits February 15, 2026 17:19
When multiple concurrent workflow invocations race to complete the same
wait, the server returns 409 (conflict) for duplicates. This change
handles the 409 gracefully in both runtime.ts (sleep elapsed check) and
runs.ts (wakeUpRun), preventing crashes and treating already-completed
waits as successful.
Also updates event-sourcing docs to reflect that waits are now
materialized as entities in storage with atomic completion guarantees.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ranch
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… duplicate wait_completed events
Adds Wait type/schema to the shared world package and implements wait entity
materialization in both local (filesystem) and postgres world implementations,
matching the DynamoDB behavior. wait_created creates a wait entity with status
'waiting', and wait_completed transitions it to 'completed' with guards that
reject duplicates (409).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
pranaygpand others added 4 commits February 15, 2026 17:54
…ation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* origin/main:
add closure comment in sleep test helper (#1071)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…rride
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM, but can we add (or modify) one e2e test to include Promise.all(sleep(1s) x 10) or similar? That'd probably ensure general race-condition safety for waits into the future

@pranaygp

pranaygp commented Feb 16, 2026

Copy link
Copy Markdown
ContributorAuthor

LGTM, but can we add (or modify) one e2e test to include Promise.all(sleep(1s) x 10) or similar? That'd probably ensure general race-condition safety for waits into the future

I already did that in the previous PR and it's been merged.

exportasyncfunctionparallelSleepWorkflow(){
'use workflow';
conststartTime=Date.now();
awaitPromise.all(Array.from({length: 10},()=>sleep('1s')));
constendTime=Date.now();
return{ startTime, endTime };
}

@pranaygp

Copy link
Copy Markdown
ContributorAuthor

Replying to @TooTallNate's code review:

Backward Compatibility

Good call — both local and postgres would throw 404 for waits created before this code was deployed. For Vercel production this is handled server-side (the backend backfills the wait entity in completed state on wait_completed if it doesn't exist — see vercel/workflow-server#265). For self-hosted postgres and local dev, I agree we should add backfill logic. I'll add that as a follow-up.

waitId Inconsistency

Fixed in a5e093c — both backends now use the raw correlationId as waitId, consistent with how steps and hooks use their correlation IDs as PKs.

Runtime 409 Handling

Agreed it causes one wasted invocation. The self-healing behavior is correct though — the next invocation picks up the persisted event and proceeds. I'll note this as an optimization for a follow-up (re-fetch the event on 409 and push it to the array to avoid the extra round-trip).

wakeUpRun 409 Handling

👍


Re: "world-postgres doesn't seem too happy" — this was fixed, the migration journal entry was missing. Added in ce2eb72.

pranaygpand others added 2 commits February 16, 2026 15:53
The server-side changes (vercel/workflow-server#265) have been merged to main
and deployed to production, so we no longer need to point at the preview URL.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* origin/main:
Remove "workflow/internal/serialization" export (#1082)
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.

4 participants

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

Materialize waits as entities to prevent duplicate wait_completed events - #1057

Merged
pranaygp merged 10 commits into
mainfrom
pgp/wait-complete-guard
Feb 17, 2026
Merged

Materialize waits as entities to prevent duplicate wait_completed events#1057
pranaygp merged 10 commits into
mainfrom
pgp/wait-complete-guard

Conversation

@pranaygp

@pranaygppranaygp commented Feb 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds wait entity materialization to the local and postgres world implementations, matching the server-side DynamoDB behavior in workflow-server PR #265. This prevents duplicate wait_completed events by creating wait entities with status tracking and conditional writes.

Changes by package

@workflow/world

  • New waits.ts with Wait type, WaitSchema, and WaitStatusSchema
  • Added optional wait field to EventResult interface

@workflow/world-local

  • wait_created creates a wait entity file with status: 'waiting'; throws 409 if already exists
  • wait_completed transitions to status: 'completed'; throws 404 if not found, 409 if already completed
  • Waits are cleaned up on terminal run states (run_completed, run_failed, run_cancelled) via deleteAllWaitsForRun
  • wait_created blocked on terminal runs (can't create new entities)

@workflow/world-postgres

  • New workflow_waits table with wait_status enum (migration 0007_add_waits_table)
  • wait_created uses INSERT with onConflictDoNothing + check for 409
  • wait_completed uses conditional UPDATE (WHERE status = 'waiting') with fallback SELECT to distinguish 404 vs 409
  • Waits cleaned up on terminal run states and in legacy handler

@workflow/core

  • Handles 409 conflict gracefully in wait_completed event creation (concurrent VQS invocations)

Server-side counterpart: https://github.com/vercel/workflow-server/pull/265

Test plan

  • Local world unit tests pass (pnpm test --filter world-local)
  • Postgres world unit tests pass (pnpm test --filter world-postgres)
  • TypeScript compiles cleanly
  • E2E Vercel Prod tests pass against server preview branch
  • Clear WORKFLOW_SERVER_URL_OVERRIDE before merging

🤖 Generated with Claude Code

CopilotAI review requested due to automatic review settings February 14, 2026 01:25
@vercel

vercelBot commented Feb 14, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented Feb 14, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 04f58a3

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

This PR includes changesets to release 18 packages
NameType
@workflow/corePatch
@workflow/worldPatch
@workflow/world-localPatch
@workflow/world-postgresPatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/web-sharedPatch
workflowPatch
@workflow/world-testingPatch
@workflow/world-vercelPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

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

@github-actions

github-actionsBot commented Feb 14, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production512038550
✅ 💻 Local Development532068600
✅ 📦 Local Production532068600
✅ 🐘 Local Postgres532068600
✅ 🪟 Windows470350
❌ 🌍 Community Worlds106449159
✅ 📋 Other129021150
Total2390442752709

❌ Failed Tests

🌍 Community Worlds (44 failed)

mongodb (1 failed):

  • webhookWorkflow

turso (43 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • hookWorkflow
  • webhookWorkflow
  • sleepingWorkflow
  • parallelSleepWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • 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
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling retry behavior workflow completes despite transient 5xx on step_completed
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • health check (queue-based) - workflow and step endpoints respond to health check messages
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • instanceMethodStepWorkflow - instance methods with "use step" directive
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context
  • stepFunctionAsStartArgWorkflow - step function reference passed as start() argument
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro4604
✅ example4604
✅ express4604
✅ fastify4604
✅ hono4604
✅ nextjs-turbopack4901
✅ nextjs-webpack4901
✅ nitro4604
✅ nuxt4604
✅ sveltekit4604
✅ vite4604
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable4307
✅ express-stable4307
✅ fastify-stable4307
✅ hono-stable4307
✅ nextjs-turbopack-canary4703
✅ nextjs-turbopack-stable4703
✅ nextjs-webpack-canary4703
✅ nextjs-webpack-stable4703
✅ nitro-stable4307
✅ nuxt-stable4307
✅ sveltekit-stable4307
✅ vite-stable4307
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable4307
✅ express-stable4307
✅ fastify-stable4307
✅ hono-stable4307
✅ nextjs-turbopack-canary4703
✅ nextjs-turbopack-stable4703
✅ nextjs-webpack-canary4703
✅ nextjs-webpack-stable4703
✅ nitro-stable4307
✅ nuxt-stable4307
✅ sveltekit-stable4307
✅ vite-stable4307
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable4307
✅ express-stable4307
✅ fastify-stable4307
✅ hono-stable4307
✅ nextjs-turbopack-canary4703
✅ nextjs-turbopack-stable4703
✅ nextjs-webpack-canary4703
✅ nextjs-webpack-stable4703
✅ nitro-stable4307
✅ nuxt-stable4307
✅ sveltekit-stable4307
✅ vite-stable4307
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack4703
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev300
❌ mongodb4613
✅ redis-dev300
✅ redis4703
✅ turso-dev300
❌ turso4433
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable4307
✅ e2e-local-postgres-nest-stable4307
✅ e2e-local-prod-nest-stable4307

📋 View full workflow run

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

This PR adds graceful handling for 409 Conflict responses when creating wait_completed events, addressing race conditions when multiple concurrent workflow invocations attempt to complete the same wait. The changes build on PR #1055 which added orphaned event detection, and work in conjunction with server-side changes that materialize waits as entities to prevent duplicate completions.

Changes:

  • Wraps wait_completed event creation in try/catch blocks to handle 409 conflicts gracefully
  • Applies 409 handling in both automatic wait completion (runtime.ts) and manual wake-up (runs.ts)
  • Updates event sourcing documentation to reflect that waits are now materialized entities in storage

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.

Show a summary per file
FileDescription
packages/core/src/runtime/runs.tsAdds 409 conflict handling in wakeUpRun() to count already-completed waits as successful
packages/core/src/runtime/runs.test.tsNew test file with unit tests for 409 handling and error cases in wakeUpRun()
packages/core/src/runtime.tsWraps automatic wait_completed event creation in try/catch to log and skip on 409 conflicts
docs/content/docs/how-it-works/event-sourcing.mdxUpdates documentation to clarify waits are materialized entities with atomic completion guarantees
.changeset/wait-complete-guard.mdAdds changeset for patch release describing the fix

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

pranaygpand others added 4 commits February 15, 2026 17:19
When multiple concurrent workflow invocations race to complete the same
wait, the server returns 409 (conflict) for duplicates. This change
handles the 409 gracefully in both runtime.ts (sleep elapsed check) and
runs.ts (wakeUpRun), preventing crashes and treating already-completed
waits as successful.
Also updates event-sourcing docs to reflect that waits are now
materialized as entities in storage with atomic completion guarantees.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ranch
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… duplicate wait_completed events
Adds Wait type/schema to the shared world package and implements wait entity
materialization in both local (filesystem) and postgres world implementations,
matching the DynamoDB behavior. wait_created creates a wait entity with status
'waiting', and wait_completed transitions it to 'completed' with guards that
reject duplicates (409).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
pranaygpand others added 4 commits February 15, 2026 17:54
…ation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* origin/main:
add closure comment in sleep test helper (#1071)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…rride
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM, but can we add (or modify) one e2e test to include Promise.all(sleep(1s) x 10) or similar? That'd probably ensure general race-condition safety for waits into the future

@pranaygp

pranaygp commented Feb 16, 2026

Copy link
Copy Markdown
ContributorAuthor

LGTM, but can we add (or modify) one e2e test to include Promise.all(sleep(1s) x 10) or similar? That'd probably ensure general race-condition safety for waits into the future

I already did that in the previous PR and it's been merged.

exportasyncfunctionparallelSleepWorkflow(){
'use workflow';
conststartTime=Date.now();
awaitPromise.all(Array.from({length: 10},()=>sleep('1s')));
constendTime=Date.now();
return{ startTime, endTime };
}

@pranaygp

Copy link
Copy Markdown
ContributorAuthor

Replying to @TooTallNate's code review:

Backward Compatibility

Good call — both local and postgres would throw 404 for waits created before this code was deployed. For Vercel production this is handled server-side (the backend backfills the wait entity in completed state on wait_completed if it doesn't exist — see vercel/workflow-server#265). For self-hosted postgres and local dev, I agree we should add backfill logic. I'll add that as a follow-up.

waitId Inconsistency

Fixed in a5e093c — both backends now use the raw correlationId as waitId, consistent with how steps and hooks use their correlation IDs as PKs.

Runtime 409 Handling

Agreed it causes one wasted invocation. The self-healing behavior is correct though — the next invocation picks up the persisted event and proceeds. I'll note this as an optimization for a follow-up (re-fetch the event on 409 and push it to the array to avoid the extra round-trip).

wakeUpRun 409 Handling

👍


Re: "world-postgres doesn't seem too happy" — this was fixed, the migration journal entry was missing. Added in ce2eb72.

pranaygpand others added 2 commits February 16, 2026 15:53
The server-side changes (vercel/workflow-server#265) have been merged to main
and deployed to production, so we no longer need to point at the preview URL.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* origin/main:
Remove "workflow/internal/serialization" export (#1082)
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.

4 participants

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

Materialize waits as entities to prevent duplicate wait_completed events - #1057

Merged
pranaygp merged 10 commits into
mainfrom
pgp/wait-complete-guard
Feb 17, 2026
Merged

Materialize waits as entities to prevent duplicate wait_completed events#1057
pranaygp merged 10 commits into
mainfrom
pgp/wait-complete-guard

Conversation

@pranaygp

@pranaygppranaygp commented Feb 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds wait entity materialization to the local and postgres world implementations, matching the server-side DynamoDB behavior in workflow-server PR #265. This prevents duplicate wait_completed events by creating wait entities with status tracking and conditional writes.

Changes by package

@workflow/world

  • New waits.ts with Wait type, WaitSchema, and WaitStatusSchema
  • Added optional wait field to EventResult interface

@workflow/world-local

  • wait_created creates a wait entity file with status: 'waiting'; throws 409 if already exists
  • wait_completed transitions to status: 'completed'; throws 404 if not found, 409 if already completed
  • Waits are cleaned up on terminal run states (run_completed, run_failed, run_cancelled) via deleteAllWaitsForRun
  • wait_created blocked on terminal runs (can't create new entities)

@workflow/world-postgres

  • New workflow_waits table with wait_status enum (migration 0007_add_waits_table)
  • wait_created uses INSERT with onConflictDoNothing + check for 409
  • wait_completed uses conditional UPDATE (WHERE status = 'waiting') with fallback SELECT to distinguish 404 vs 409
  • Waits cleaned up on terminal run states and in legacy handler

@workflow/core

  • Handles 409 conflict gracefully in wait_completed event creation (concurrent VQS invocations)

Server-side counterpart: https://github.com/vercel/workflow-server/pull/265

Test plan

  • Local world unit tests pass (pnpm test --filter world-local)
  • Postgres world unit tests pass (pnpm test --filter world-postgres)
  • TypeScript compiles cleanly
  • E2E Vercel Prod tests pass against server preview branch
  • Clear WORKFLOW_SERVER_URL_OVERRIDE before merging

🤖 Generated with Claude Code

CopilotAI review requested due to automatic review settings February 14, 2026 01:25
@vercel

vercelBot commented Feb 14, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented Feb 14, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 04f58a3

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

This PR includes changesets to release 18 packages
NameType
@workflow/corePatch
@workflow/worldPatch
@workflow/world-localPatch
@workflow/world-postgresPatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/web-sharedPatch
workflowPatch
@workflow/world-testingPatch
@workflow/world-vercelPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

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

@github-actions

github-actionsBot commented Feb 14, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production512038550
✅ 💻 Local Development532068600
✅ 📦 Local Production532068600
✅ 🐘 Local Postgres532068600
✅ 🪟 Windows470350
❌ 🌍 Community Worlds106449159
✅ 📋 Other129021150
Total2390442752709

❌ Failed Tests

🌍 Community Worlds (44 failed)

mongodb (1 failed):

  • webhookWorkflow

turso (43 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • hookWorkflow
  • webhookWorkflow
  • sleepingWorkflow
  • parallelSleepWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • 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
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling retry behavior workflow completes despite transient 5xx on step_completed
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • health check (queue-based) - workflow and step endpoints respond to health check messages
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • instanceMethodStepWorkflow - instance methods with "use step" directive
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context
  • stepFunctionAsStartArgWorkflow - step function reference passed as start() argument
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro4604
✅ example4604
✅ express4604
✅ fastify4604
✅ hono4604
✅ nextjs-turbopack4901
✅ nextjs-webpack4901
✅ nitro4604
✅ nuxt4604
✅ sveltekit4604
✅ vite4604
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable4307
✅ express-stable4307
✅ fastify-stable4307
✅ hono-stable4307
✅ nextjs-turbopack-canary4703
✅ nextjs-turbopack-stable4703
✅ nextjs-webpack-canary4703
✅ nextjs-webpack-stable4703
✅ nitro-stable4307
✅ nuxt-stable4307
✅ sveltekit-stable4307
✅ vite-stable4307
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable4307
✅ express-stable4307
✅ fastify-stable4307
✅ hono-stable4307
✅ nextjs-turbopack-canary4703
✅ nextjs-turbopack-stable4703
✅ nextjs-webpack-canary4703
✅ nextjs-webpack-stable4703
✅ nitro-stable4307
✅ nuxt-stable4307
✅ sveltekit-stable4307
✅ vite-stable4307
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable4307
✅ express-stable4307
✅ fastify-stable4307
✅ hono-stable4307
✅ nextjs-turbopack-canary4703
✅ nextjs-turbopack-stable4703
✅ nextjs-webpack-canary4703
✅ nextjs-webpack-stable4703
✅ nitro-stable4307
✅ nuxt-stable4307
✅ sveltekit-stable4307
✅ vite-stable4307
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack4703
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev300
❌ mongodb4613
✅ redis-dev300
✅ redis4703
✅ turso-dev300
❌ turso4433
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable4307
✅ e2e-local-postgres-nest-stable4307
✅ e2e-local-prod-nest-stable4307

📋 View full workflow run

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

This PR adds graceful handling for 409 Conflict responses when creating wait_completed events, addressing race conditions when multiple concurrent workflow invocations attempt to complete the same wait. The changes build on PR #1055 which added orphaned event detection, and work in conjunction with server-side changes that materialize waits as entities to prevent duplicate completions.

Changes:

  • Wraps wait_completed event creation in try/catch blocks to handle 409 conflicts gracefully
  • Applies 409 handling in both automatic wait completion (runtime.ts) and manual wake-up (runs.ts)
  • Updates event sourcing documentation to reflect that waits are now materialized entities in storage

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.

Show a summary per file
FileDescription
packages/core/src/runtime/runs.tsAdds 409 conflict handling in wakeUpRun() to count already-completed waits as successful
packages/core/src/runtime/runs.test.tsNew test file with unit tests for 409 handling and error cases in wakeUpRun()
packages/core/src/runtime.tsWraps automatic wait_completed event creation in try/catch to log and skip on 409 conflicts
docs/content/docs/how-it-works/event-sourcing.mdxUpdates documentation to clarify waits are materialized entities with atomic completion guarantees
.changeset/wait-complete-guard.mdAdds changeset for patch release describing the fix

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

pranaygpand others added 4 commits February 15, 2026 17:19
When multiple concurrent workflow invocations race to complete the same
wait, the server returns 409 (conflict) for duplicates. This change
handles the 409 gracefully in both runtime.ts (sleep elapsed check) and
runs.ts (wakeUpRun), preventing crashes and treating already-completed
waits as successful.
Also updates event-sourcing docs to reflect that waits are now
materialized as entities in storage with atomic completion guarantees.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ranch
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… duplicate wait_completed events
Adds Wait type/schema to the shared world package and implements wait entity
materialization in both local (filesystem) and postgres world implementations,
matching the DynamoDB behavior. wait_created creates a wait entity with status
'waiting', and wait_completed transitions it to 'completed' with guards that
reject duplicates (409).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
pranaygpand others added 4 commits February 15, 2026 17:54
…ation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* origin/main:
add closure comment in sleep test helper (#1071)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…rride
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM, but can we add (or modify) one e2e test to include Promise.all(sleep(1s) x 10) or similar? That'd probably ensure general race-condition safety for waits into the future

@pranaygp

pranaygp commented Feb 16, 2026

Copy link
Copy Markdown
ContributorAuthor

LGTM, but can we add (or modify) one e2e test to include Promise.all(sleep(1s) x 10) or similar? That'd probably ensure general race-condition safety for waits into the future

I already did that in the previous PR and it's been merged.

exportasyncfunctionparallelSleepWorkflow(){
'use workflow';
conststartTime=Date.now();
awaitPromise.all(Array.from({length: 10},()=>sleep('1s')));
constendTime=Date.now();
return{ startTime, endTime };
}

@pranaygp

Copy link
Copy Markdown
ContributorAuthor

Replying to @TooTallNate's code review:

Backward Compatibility

Good call — both local and postgres would throw 404 for waits created before this code was deployed. For Vercel production this is handled server-side (the backend backfills the wait entity in completed state on wait_completed if it doesn't exist — see vercel/workflow-server#265). For self-hosted postgres and local dev, I agree we should add backfill logic. I'll add that as a follow-up.

waitId Inconsistency

Fixed in a5e093c — both backends now use the raw correlationId as waitId, consistent with how steps and hooks use their correlation IDs as PKs.

Runtime 409 Handling

Agreed it causes one wasted invocation. The self-healing behavior is correct though — the next invocation picks up the persisted event and proceeds. I'll note this as an optimization for a follow-up (re-fetch the event on 409 and push it to the array to avoid the extra round-trip).

wakeUpRun 409 Handling

👍


Re: "world-postgres doesn't seem too happy" — this was fixed, the migration journal entry was missing. Added in ce2eb72.

pranaygpand others added 2 commits February 16, 2026 15:53
The server-side changes (vercel/workflow-server#265) have been merged to main
and deployed to production, so we no longer need to point at the preview URL.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* origin/main:
Remove "workflow/internal/serialization" export (#1082)
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.

4 participants

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

Materialize waits as entities to prevent duplicate wait_completed events - #1057

Merged
pranaygp merged 10 commits into
mainfrom
pgp/wait-complete-guard
Feb 17, 2026
Merged

Materialize waits as entities to prevent duplicate wait_completed events#1057
pranaygp merged 10 commits into
mainfrom
pgp/wait-complete-guard

Conversation

@pranaygp

@pranaygppranaygp commented Feb 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds wait entity materialization to the local and postgres world implementations, matching the server-side DynamoDB behavior in workflow-server PR #265. This prevents duplicate wait_completed events by creating wait entities with status tracking and conditional writes.

Changes by package

@workflow/world

  • New waits.ts with Wait type, WaitSchema, and WaitStatusSchema
  • Added optional wait field to EventResult interface

@workflow/world-local

  • wait_created creates a wait entity file with status: 'waiting'; throws 409 if already exists
  • wait_completed transitions to status: 'completed'; throws 404 if not found, 409 if already completed
  • Waits are cleaned up on terminal run states (run_completed, run_failed, run_cancelled) via deleteAllWaitsForRun
  • wait_created blocked on terminal runs (can't create new entities)

@workflow/world-postgres

  • New workflow_waits table with wait_status enum (migration 0007_add_waits_table)
  • wait_created uses INSERT with onConflictDoNothing + check for 409
  • wait_completed uses conditional UPDATE (WHERE status = 'waiting') with fallback SELECT to distinguish 404 vs 409
  • Waits cleaned up on terminal run states and in legacy handler

@workflow/core

  • Handles 409 conflict gracefully in wait_completed event creation (concurrent VQS invocations)

Server-side counterpart: https://github.com/vercel/workflow-server/pull/265

Test plan

  • Local world unit tests pass (pnpm test --filter world-local)
  • Postgres world unit tests pass (pnpm test --filter world-postgres)
  • TypeScript compiles cleanly
  • E2E Vercel Prod tests pass against server preview branch
  • Clear WORKFLOW_SERVER_URL_OVERRIDE before merging

🤖 Generated with Claude Code

CopilotAI review requested due to automatic review settings February 14, 2026 01:25
@vercel

vercelBot commented Feb 14, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented Feb 14, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 04f58a3

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

This PR includes changesets to release 18 packages
NameType
@workflow/corePatch
@workflow/worldPatch
@workflow/world-localPatch
@workflow/world-postgresPatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/web-sharedPatch
workflowPatch
@workflow/world-testingPatch
@workflow/world-vercelPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

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

@github-actions

github-actionsBot commented Feb 14, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production512038550
✅ 💻 Local Development532068600
✅ 📦 Local Production532068600
✅ 🐘 Local Postgres532068600
✅ 🪟 Windows470350
❌ 🌍 Community Worlds106449159
✅ 📋 Other129021150
Total2390442752709

❌ Failed Tests

🌍 Community Worlds (44 failed)

mongodb (1 failed):

  • webhookWorkflow

turso (43 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • hookWorkflow
  • webhookWorkflow
  • sleepingWorkflow
  • parallelSleepWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • 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
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling retry behavior workflow completes despite transient 5xx on step_completed
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • health check (queue-based) - workflow and step endpoints respond to health check messages
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • instanceMethodStepWorkflow - instance methods with "use step" directive
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context
  • stepFunctionAsStartArgWorkflow - step function reference passed as start() argument
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro4604
✅ example4604
✅ express4604
✅ fastify4604
✅ hono4604
✅ nextjs-turbopack4901
✅ nextjs-webpack4901
✅ nitro4604
✅ nuxt4604
✅ sveltekit4604
✅ vite4604
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable4307
✅ express-stable4307
✅ fastify-stable4307
✅ hono-stable4307
✅ nextjs-turbopack-canary4703
✅ nextjs-turbopack-stable4703
✅ nextjs-webpack-canary4703
✅ nextjs-webpack-stable4703
✅ nitro-stable4307
✅ nuxt-stable4307
✅ sveltekit-stable4307
✅ vite-stable4307
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable4307
✅ express-stable4307
✅ fastify-stable4307
✅ hono-stable4307
✅ nextjs-turbopack-canary4703
✅ nextjs-turbopack-stable4703
✅ nextjs-webpack-canary4703
✅ nextjs-webpack-stable4703
✅ nitro-stable4307
✅ nuxt-stable4307
✅ sveltekit-stable4307
✅ vite-stable4307
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable4307
✅ express-stable4307
✅ fastify-stable4307
✅ hono-stable4307
✅ nextjs-turbopack-canary4703
✅ nextjs-turbopack-stable4703
✅ nextjs-webpack-canary4703
✅ nextjs-webpack-stable4703
✅ nitro-stable4307
✅ nuxt-stable4307
✅ sveltekit-stable4307
✅ vite-stable4307
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack4703
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev300
❌ mongodb4613
✅ redis-dev300
✅ redis4703
✅ turso-dev300
❌ turso4433
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable4307
✅ e2e-local-postgres-nest-stable4307
✅ e2e-local-prod-nest-stable4307

📋 View full workflow run

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

This PR adds graceful handling for 409 Conflict responses when creating wait_completed events, addressing race conditions when multiple concurrent workflow invocations attempt to complete the same wait. The changes build on PR #1055 which added orphaned event detection, and work in conjunction with server-side changes that materialize waits as entities to prevent duplicate completions.

Changes:

  • Wraps wait_completed event creation in try/catch blocks to handle 409 conflicts gracefully
  • Applies 409 handling in both automatic wait completion (runtime.ts) and manual wake-up (runs.ts)
  • Updates event sourcing documentation to reflect that waits are now materialized entities in storage

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.

Show a summary per file
FileDescription
packages/core/src/runtime/runs.tsAdds 409 conflict handling in wakeUpRun() to count already-completed waits as successful
packages/core/src/runtime/runs.test.tsNew test file with unit tests for 409 handling and error cases in wakeUpRun()
packages/core/src/runtime.tsWraps automatic wait_completed event creation in try/catch to log and skip on 409 conflicts
docs/content/docs/how-it-works/event-sourcing.mdxUpdates documentation to clarify waits are materialized entities with atomic completion guarantees
.changeset/wait-complete-guard.mdAdds changeset for patch release describing the fix

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

pranaygpand others added 4 commits February 15, 2026 17:19
When multiple concurrent workflow invocations race to complete the same
wait, the server returns 409 (conflict) for duplicates. This change
handles the 409 gracefully in both runtime.ts (sleep elapsed check) and
runs.ts (wakeUpRun), preventing crashes and treating already-completed
waits as successful.
Also updates event-sourcing docs to reflect that waits are now
materialized as entities in storage with atomic completion guarantees.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ranch
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… duplicate wait_completed events
Adds Wait type/schema to the shared world package and implements wait entity
materialization in both local (filesystem) and postgres world implementations,
matching the DynamoDB behavior. wait_created creates a wait entity with status
'waiting', and wait_completed transitions it to 'completed' with guards that
reject duplicates (409).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
pranaygpand others added 4 commits February 15, 2026 17:54
…ation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* origin/main:
add closure comment in sleep test helper (#1071)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…rride
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM, but can we add (or modify) one e2e test to include Promise.all(sleep(1s) x 10) or similar? That'd probably ensure general race-condition safety for waits into the future

@pranaygp

pranaygp commented Feb 16, 2026

Copy link
Copy Markdown
ContributorAuthor

LGTM, but can we add (or modify) one e2e test to include Promise.all(sleep(1s) x 10) or similar? That'd probably ensure general race-condition safety for waits into the future

I already did that in the previous PR and it's been merged.

exportasyncfunctionparallelSleepWorkflow(){
'use workflow';
conststartTime=Date.now();
awaitPromise.all(Array.from({length: 10},()=>sleep('1s')));
constendTime=Date.now();
return{ startTime, endTime };
}

@pranaygp

Copy link
Copy Markdown
ContributorAuthor

Replying to @TooTallNate's code review:

Backward Compatibility

Good call — both local and postgres would throw 404 for waits created before this code was deployed. For Vercel production this is handled server-side (the backend backfills the wait entity in completed state on wait_completed if it doesn't exist — see vercel/workflow-server#265). For self-hosted postgres and local dev, I agree we should add backfill logic. I'll add that as a follow-up.

waitId Inconsistency

Fixed in a5e093c — both backends now use the raw correlationId as waitId, consistent with how steps and hooks use their correlation IDs as PKs.

Runtime 409 Handling

Agreed it causes one wasted invocation. The self-healing behavior is correct though — the next invocation picks up the persisted event and proceeds. I'll note this as an optimization for a follow-up (re-fetch the event on 409 and push it to the array to avoid the extra round-trip).

wakeUpRun 409 Handling

👍


Re: "world-postgres doesn't seem too happy" — this was fixed, the migration journal entry was missing. Added in ce2eb72.

pranaygpand others added 2 commits February 16, 2026 15:53
The server-side changes (vercel/workflow-server#265) have been merged to main
and deployed to production, so we no longer need to point at the preview URL.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* origin/main:
Remove "workflow/internal/serialization" export (#1082)
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.

4 participants

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

Materialize waits as entities to prevent duplicate wait_completed events - #1057

Merged
pranaygp merged 10 commits into
mainfrom
pgp/wait-complete-guard
Feb 17, 2026
Merged

Materialize waits as entities to prevent duplicate wait_completed events#1057
pranaygp merged 10 commits into
mainfrom
pgp/wait-complete-guard

Conversation

@pranaygp

@pranaygppranaygp commented Feb 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds wait entity materialization to the local and postgres world implementations, matching the server-side DynamoDB behavior in workflow-server PR #265. This prevents duplicate wait_completed events by creating wait entities with status tracking and conditional writes.

Changes by package

@workflow/world

  • New waits.ts with Wait type, WaitSchema, and WaitStatusSchema
  • Added optional wait field to EventResult interface

@workflow/world-local

  • wait_created creates a wait entity file with status: 'waiting'; throws 409 if already exists
  • wait_completed transitions to status: 'completed'; throws 404 if not found, 409 if already completed
  • Waits are cleaned up on terminal run states (run_completed, run_failed, run_cancelled) via deleteAllWaitsForRun
  • wait_created blocked on terminal runs (can't create new entities)

@workflow/world-postgres

  • New workflow_waits table with wait_status enum (migration 0007_add_waits_table)
  • wait_created uses INSERT with onConflictDoNothing + check for 409
  • wait_completed uses conditional UPDATE (WHERE status = 'waiting') with fallback SELECT to distinguish 404 vs 409
  • Waits cleaned up on terminal run states and in legacy handler

@workflow/core

  • Handles 409 conflict gracefully in wait_completed event creation (concurrent VQS invocations)

Server-side counterpart: https://github.com/vercel/workflow-server/pull/265

Test plan

  • Local world unit tests pass (pnpm test --filter world-local)
  • Postgres world unit tests pass (pnpm test --filter world-postgres)
  • TypeScript compiles cleanly
  • E2E Vercel Prod tests pass against server preview branch
  • Clear WORKFLOW_SERVER_URL_OVERRIDE before merging

🤖 Generated with Claude Code

CopilotAI review requested due to automatic review settings February 14, 2026 01:25
@vercel

vercelBot commented Feb 14, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented Feb 14, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 04f58a3

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

This PR includes changesets to release 18 packages
NameType
@workflow/corePatch
@workflow/worldPatch
@workflow/world-localPatch
@workflow/world-postgresPatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/web-sharedPatch
workflowPatch
@workflow/world-testingPatch
@workflow/world-vercelPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

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

@github-actions

github-actionsBot commented Feb 14, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production512038550
✅ 💻 Local Development532068600
✅ 📦 Local Production532068600
✅ 🐘 Local Postgres532068600
✅ 🪟 Windows470350
❌ 🌍 Community Worlds106449159
✅ 📋 Other129021150
Total2390442752709

❌ Failed Tests

🌍 Community Worlds (44 failed)

mongodb (1 failed):

  • webhookWorkflow

turso (43 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • hookWorkflow
  • webhookWorkflow
  • sleepingWorkflow
  • parallelSleepWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • 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
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling retry behavior workflow completes despite transient 5xx on step_completed
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • health check (queue-based) - workflow and step endpoints respond to health check messages
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • instanceMethodStepWorkflow - instance methods with "use step" directive
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context
  • stepFunctionAsStartArgWorkflow - step function reference passed as start() argument
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro4604
✅ example4604
✅ express4604
✅ fastify4604
✅ hono4604
✅ nextjs-turbopack4901
✅ nextjs-webpack4901
✅ nitro4604
✅ nuxt4604
✅ sveltekit4604
✅ vite4604
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable4307
✅ express-stable4307
✅ fastify-stable4307
✅ hono-stable4307
✅ nextjs-turbopack-canary4703
✅ nextjs-turbopack-stable4703
✅ nextjs-webpack-canary4703
✅ nextjs-webpack-stable4703
✅ nitro-stable4307
✅ nuxt-stable4307
✅ sveltekit-stable4307
✅ vite-stable4307
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable4307
✅ express-stable4307
✅ fastify-stable4307
✅ hono-stable4307
✅ nextjs-turbopack-canary4703
✅ nextjs-turbopack-stable4703
✅ nextjs-webpack-canary4703
✅ nextjs-webpack-stable4703
✅ nitro-stable4307
✅ nuxt-stable4307
✅ sveltekit-stable4307
✅ vite-stable4307
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable4307
✅ express-stable4307
✅ fastify-stable4307
✅ hono-stable4307
✅ nextjs-turbopack-canary4703
✅ nextjs-turbopack-stable4703
✅ nextjs-webpack-canary4703
✅ nextjs-webpack-stable4703
✅ nitro-stable4307
✅ nuxt-stable4307
✅ sveltekit-stable4307
✅ vite-stable4307
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack4703
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev300
❌ mongodb4613
✅ redis-dev300
✅ redis4703
✅ turso-dev300
❌ turso4433
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable4307
✅ e2e-local-postgres-nest-stable4307
✅ e2e-local-prod-nest-stable4307

📋 View full workflow run

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

This PR adds graceful handling for 409 Conflict responses when creating wait_completed events, addressing race conditions when multiple concurrent workflow invocations attempt to complete the same wait. The changes build on PR #1055 which added orphaned event detection, and work in conjunction with server-side changes that materialize waits as entities to prevent duplicate completions.

Changes:

  • Wraps wait_completed event creation in try/catch blocks to handle 409 conflicts gracefully
  • Applies 409 handling in both automatic wait completion (runtime.ts) and manual wake-up (runs.ts)
  • Updates event sourcing documentation to reflect that waits are now materialized entities in storage

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.

Show a summary per file
FileDescription
packages/core/src/runtime/runs.tsAdds 409 conflict handling in wakeUpRun() to count already-completed waits as successful
packages/core/src/runtime/runs.test.tsNew test file with unit tests for 409 handling and error cases in wakeUpRun()
packages/core/src/runtime.tsWraps automatic wait_completed event creation in try/catch to log and skip on 409 conflicts
docs/content/docs/how-it-works/event-sourcing.mdxUpdates documentation to clarify waits are materialized entities with atomic completion guarantees
.changeset/wait-complete-guard.mdAdds changeset for patch release describing the fix

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

pranaygpand others added 4 commits February 15, 2026 17:19
When multiple concurrent workflow invocations race to complete the same
wait, the server returns 409 (conflict) for duplicates. This change
handles the 409 gracefully in both runtime.ts (sleep elapsed check) and
runs.ts (wakeUpRun), preventing crashes and treating already-completed
waits as successful.
Also updates event-sourcing docs to reflect that waits are now
materialized as entities in storage with atomic completion guarantees.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ranch
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… duplicate wait_completed events
Adds Wait type/schema to the shared world package and implements wait entity
materialization in both local (filesystem) and postgres world implementations,
matching the DynamoDB behavior. wait_created creates a wait entity with status
'waiting', and wait_completed transitions it to 'completed' with guards that
reject duplicates (409).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
pranaygpand others added 4 commits February 15, 2026 17:54
…ation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* origin/main:
add closure comment in sleep test helper (#1071)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…rride
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM, but can we add (or modify) one e2e test to include Promise.all(sleep(1s) x 10) or similar? That'd probably ensure general race-condition safety for waits into the future

@pranaygp

pranaygp commented Feb 16, 2026

Copy link
Copy Markdown
ContributorAuthor

LGTM, but can we add (or modify) one e2e test to include Promise.all(sleep(1s) x 10) or similar? That'd probably ensure general race-condition safety for waits into the future

I already did that in the previous PR and it's been merged.

exportasyncfunctionparallelSleepWorkflow(){
'use workflow';
conststartTime=Date.now();
awaitPromise.all(Array.from({length: 10},()=>sleep('1s')));
constendTime=Date.now();
return{ startTime, endTime };
}

@pranaygp

Copy link
Copy Markdown
ContributorAuthor

Replying to @TooTallNate's code review:

Backward Compatibility

Good call — both local and postgres would throw 404 for waits created before this code was deployed. For Vercel production this is handled server-side (the backend backfills the wait entity in completed state on wait_completed if it doesn't exist — see vercel/workflow-server#265). For self-hosted postgres and local dev, I agree we should add backfill logic. I'll add that as a follow-up.

waitId Inconsistency

Fixed in a5e093c — both backends now use the raw correlationId as waitId, consistent with how steps and hooks use their correlation IDs as PKs.

Runtime 409 Handling

Agreed it causes one wasted invocation. The self-healing behavior is correct though — the next invocation picks up the persisted event and proceeds. I'll note this as an optimization for a follow-up (re-fetch the event on 409 and push it to the array to avoid the extra round-trip).

wakeUpRun 409 Handling

👍


Re: "world-postgres doesn't seem too happy" — this was fixed, the migration journal entry was missing. Added in ce2eb72.

pranaygpand others added 2 commits February 16, 2026 15:53
The server-side changes (vercel/workflow-server#265) have been merged to main
and deployed to production, so we no longer need to point at the preview URL.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* origin/main:
Remove "workflow/internal/serialization" export (#1082)
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.

4 participants

@pranaygp@TooTallNate@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Materialize waits as entities to prevent duplicate wait_completed events - #1057

Merged
pranaygp merged 10 commits into
mainfrom
pgp/wait-complete-guard
Feb 17, 2026
Merged

Materialize waits as entities to prevent duplicate wait_completed events#1057
pranaygp merged 10 commits into
mainfrom
pgp/wait-complete-guard

Conversation

@pranaygp

@pranaygppranaygp commented Feb 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds wait entity materialization to the local and postgres world implementations, matching the server-side DynamoDB behavior in workflow-server PR #265. This prevents duplicate wait_completed events by creating wait entities with status tracking and conditional writes.

Changes by package

@workflow/world

  • New waits.ts with Wait type, WaitSchema, and WaitStatusSchema
  • Added optional wait field to EventResult interface

@workflow/world-local

  • wait_created creates a wait entity file with status: 'waiting'; throws 409 if already exists
  • wait_completed transitions to status: 'completed'; throws 404 if not found, 409 if already completed
  • Waits are cleaned up on terminal run states (run_completed, run_failed, run_cancelled) via deleteAllWaitsForRun
  • wait_created blocked on terminal runs (can't create new entities)

@workflow/world-postgres

  • New workflow_waits table with wait_status enum (migration 0007_add_waits_table)
  • wait_created uses INSERT with onConflictDoNothing + check for 409
  • wait_completed uses conditional UPDATE (WHERE status = 'waiting') with fallback SELECT to distinguish 404 vs 409
  • Waits cleaned up on terminal run states and in legacy handler

@workflow/core

  • Handles 409 conflict gracefully in wait_completed event creation (concurrent VQS invocations)

Server-side counterpart: https://github.com/vercel/workflow-server/pull/265

Test plan

  • Local world unit tests pass (pnpm test --filter world-local)
  • Postgres world unit tests pass (pnpm test --filter world-postgres)
  • TypeScript compiles cleanly
  • E2E Vercel Prod tests pass against server preview branch
  • Clear WORKFLOW_SERVER_URL_OVERRIDE before merging

🤖 Generated with Claude Code

CopilotAI review requested due to automatic review settings February 14, 2026 01:25
@vercel

vercelBot commented Feb 14, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented Feb 14, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 04f58a3

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

This PR includes changesets to release 18 packages
NameType
@workflow/corePatch
@workflow/worldPatch
@workflow/world-localPatch
@workflow/world-postgresPatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/web-sharedPatch
workflowPatch
@workflow/world-testingPatch
@workflow/world-vercelPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

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

@github-actions

github-actionsBot commented Feb 14, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production512038550
✅ 💻 Local Development532068600
✅ 📦 Local Production532068600
✅ 🐘 Local Postgres532068600
✅ 🪟 Windows470350
❌ 🌍 Community Worlds106449159
✅ 📋 Other129021150
Total2390442752709

❌ Failed Tests

🌍 Community Worlds (44 failed)

mongodb (1 failed):

  • webhookWorkflow

turso (43 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • hookWorkflow
  • webhookWorkflow
  • sleepingWorkflow
  • parallelSleepWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • 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
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling retry behavior workflow completes despite transient 5xx on step_completed
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • health check (queue-based) - workflow and step endpoints respond to health check messages
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • instanceMethodStepWorkflow - instance methods with "use step" directive
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context
  • stepFunctionAsStartArgWorkflow - step function reference passed as start() argument
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro4604
✅ example4604
✅ express4604
✅ fastify4604
✅ hono4604
✅ nextjs-turbopack4901
✅ nextjs-webpack4901
✅ nitro4604
✅ nuxt4604
✅ sveltekit4604
✅ vite4604
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable4307
✅ express-stable4307
✅ fastify-stable4307
✅ hono-stable4307
✅ nextjs-turbopack-canary4703
✅ nextjs-turbopack-stable4703
✅ nextjs-webpack-canary4703
✅ nextjs-webpack-stable4703
✅ nitro-stable4307
✅ nuxt-stable4307
✅ sveltekit-stable4307
✅ vite-stable4307
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable4307
✅ express-stable4307
✅ fastify-stable4307
✅ hono-stable4307
✅ nextjs-turbopack-canary4703
✅ nextjs-turbopack-stable4703
✅ nextjs-webpack-canary4703
✅ nextjs-webpack-stable4703
✅ nitro-stable4307
✅ nuxt-stable4307
✅ sveltekit-stable4307
✅ vite-stable4307
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable4307
✅ express-stable4307
✅ fastify-stable4307
✅ hono-stable4307
✅ nextjs-turbopack-canary4703
✅ nextjs-turbopack-stable4703
✅ nextjs-webpack-canary4703
✅ nextjs-webpack-stable4703
✅ nitro-stable4307
✅ nuxt-stable4307
✅ sveltekit-stable4307
✅ vite-stable4307
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack4703
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev300
❌ mongodb4613
✅ redis-dev300
✅ redis4703
✅ turso-dev300
❌ turso4433
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable4307
✅ e2e-local-postgres-nest-stable4307
✅ e2e-local-prod-nest-stable4307

📋 View full workflow run

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

This PR adds graceful handling for 409 Conflict responses when creating wait_completed events, addressing race conditions when multiple concurrent workflow invocations attempt to complete the same wait. The changes build on PR #1055 which added orphaned event detection, and work in conjunction with server-side changes that materialize waits as entities to prevent duplicate completions.

Changes:

  • Wraps wait_completed event creation in try/catch blocks to handle 409 conflicts gracefully
  • Applies 409 handling in both automatic wait completion (runtime.ts) and manual wake-up (runs.ts)
  • Updates event sourcing documentation to reflect that waits are now materialized entities in storage

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.

Show a summary per file
FileDescription
packages/core/src/runtime/runs.tsAdds 409 conflict handling in wakeUpRun() to count already-completed waits as successful
packages/core/src/runtime/runs.test.tsNew test file with unit tests for 409 handling and error cases in wakeUpRun()
packages/core/src/runtime.tsWraps automatic wait_completed event creation in try/catch to log and skip on 409 conflicts
docs/content/docs/how-it-works/event-sourcing.mdxUpdates documentation to clarify waits are materialized entities with atomic completion guarantees
.changeset/wait-complete-guard.mdAdds changeset for patch release describing the fix

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

pranaygpand others added 4 commits February 15, 2026 17:19
When multiple concurrent workflow invocations race to complete the same
wait, the server returns 409 (conflict) for duplicates. This change
handles the 409 gracefully in both runtime.ts (sleep elapsed check) and
runs.ts (wakeUpRun), preventing crashes and treating already-completed
waits as successful.
Also updates event-sourcing docs to reflect that waits are now
materialized as entities in storage with atomic completion guarantees.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ranch
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… duplicate wait_completed events
Adds Wait type/schema to the shared world package and implements wait entity
materialization in both local (filesystem) and postgres world implementations,
matching the DynamoDB behavior. wait_created creates a wait entity with status
'waiting', and wait_completed transitions it to 'completed' with guards that
reject duplicates (409).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
pranaygpand others added 4 commits February 15, 2026 17:54
…ation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* origin/main:
add closure comment in sleep test helper (#1071)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…rride
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM, but can we add (or modify) one e2e test to include Promise.all(sleep(1s) x 10) or similar? That'd probably ensure general race-condition safety for waits into the future

@pranaygp

pranaygp commented Feb 16, 2026

Copy link
Copy Markdown
ContributorAuthor

LGTM, but can we add (or modify) one e2e test to include Promise.all(sleep(1s) x 10) or similar? That'd probably ensure general race-condition safety for waits into the future

I already did that in the previous PR and it's been merged.

exportasyncfunctionparallelSleepWorkflow(){
'use workflow';
conststartTime=Date.now();
awaitPromise.all(Array.from({length: 10},()=>sleep('1s')));
constendTime=Date.now();
return{ startTime, endTime };
}

@pranaygp

Copy link
Copy Markdown
ContributorAuthor

Replying to @TooTallNate's code review:

Backward Compatibility

Good call — both local and postgres would throw 404 for waits created before this code was deployed. For Vercel production this is handled server-side (the backend backfills the wait entity in completed state on wait_completed if it doesn't exist — see vercel/workflow-server#265). For self-hosted postgres and local dev, I agree we should add backfill logic. I'll add that as a follow-up.

waitId Inconsistency

Fixed in a5e093c — both backends now use the raw correlationId as waitId, consistent with how steps and hooks use their correlation IDs as PKs.

Runtime 409 Handling

Agreed it causes one wasted invocation. The self-healing behavior is correct though — the next invocation picks up the persisted event and proceeds. I'll note this as an optimization for a follow-up (re-fetch the event on 409 and push it to the array to avoid the extra round-trip).

wakeUpRun 409 Handling

👍


Re: "world-postgres doesn't seem too happy" — this was fixed, the migration journal entry was missing. Added in ce2eb72.

pranaygpand others added 2 commits February 16, 2026 15:53
The server-side changes (vercel/workflow-server#265) have been merged to main
and deployed to production, so we no longer need to point at the preview URL.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* origin/main:
Remove "workflow/internal/serialization" export (#1082)
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.

4 participants

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

Materialize waits as entities to prevent duplicate wait_completed events - #1057

Merged
pranaygp merged 10 commits into
mainfrom
pgp/wait-complete-guard
Feb 17, 2026
Merged

Materialize waits as entities to prevent duplicate wait_completed events#1057
pranaygp merged 10 commits into
mainfrom
pgp/wait-complete-guard

Conversation

@pranaygp

@pranaygppranaygp commented Feb 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds wait entity materialization to the local and postgres world implementations, matching the server-side DynamoDB behavior in workflow-server PR #265. This prevents duplicate wait_completed events by creating wait entities with status tracking and conditional writes.

Changes by package

@workflow/world

  • New waits.ts with Wait type, WaitSchema, and WaitStatusSchema
  • Added optional wait field to EventResult interface

@workflow/world-local

  • wait_created creates a wait entity file with status: 'waiting'; throws 409 if already exists
  • wait_completed transitions to status: 'completed'; throws 404 if not found, 409 if already completed
  • Waits are cleaned up on terminal run states (run_completed, run_failed, run_cancelled) via deleteAllWaitsForRun
  • wait_created blocked on terminal runs (can't create new entities)

@workflow/world-postgres

  • New workflow_waits table with wait_status enum (migration 0007_add_waits_table)
  • wait_created uses INSERT with onConflictDoNothing + check for 409
  • wait_completed uses conditional UPDATE (WHERE status = 'waiting') with fallback SELECT to distinguish 404 vs 409
  • Waits cleaned up on terminal run states and in legacy handler

@workflow/core

  • Handles 409 conflict gracefully in wait_completed event creation (concurrent VQS invocations)

Server-side counterpart: https://github.com/vercel/workflow-server/pull/265

Test plan

  • Local world unit tests pass (pnpm test --filter world-local)
  • Postgres world unit tests pass (pnpm test --filter world-postgres)
  • TypeScript compiles cleanly
  • E2E Vercel Prod tests pass against server preview branch
  • Clear WORKFLOW_SERVER_URL_OVERRIDE before merging

🤖 Generated with Claude Code

CopilotAI review requested due to automatic review settings February 14, 2026 01:25
@vercel

vercelBot commented Feb 14, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented Feb 14, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 04f58a3

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

This PR includes changesets to release 18 packages
NameType
@workflow/corePatch
@workflow/worldPatch
@workflow/world-localPatch
@workflow/world-postgresPatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/web-sharedPatch
workflowPatch
@workflow/world-testingPatch
@workflow/world-vercelPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

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

@github-actions

github-actionsBot commented Feb 14, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production512038550
✅ 💻 Local Development532068600
✅ 📦 Local Production532068600
✅ 🐘 Local Postgres532068600
✅ 🪟 Windows470350
❌ 🌍 Community Worlds106449159
✅ 📋 Other129021150
Total2390442752709

❌ Failed Tests

🌍 Community Worlds (44 failed)

mongodb (1 failed):

  • webhookWorkflow

turso (43 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • hookWorkflow
  • webhookWorkflow
  • sleepingWorkflow
  • parallelSleepWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • 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
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling retry behavior workflow completes despite transient 5xx on step_completed
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • health check (queue-based) - workflow and step endpoints respond to health check messages
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • instanceMethodStepWorkflow - instance methods with "use step" directive
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context
  • stepFunctionAsStartArgWorkflow - step function reference passed as start() argument
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro4604
✅ example4604
✅ express4604
✅ fastify4604
✅ hono4604
✅ nextjs-turbopack4901
✅ nextjs-webpack4901
✅ nitro4604
✅ nuxt4604
✅ sveltekit4604
✅ vite4604
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable4307
✅ express-stable4307
✅ fastify-stable4307
✅ hono-stable4307
✅ nextjs-turbopack-canary4703
✅ nextjs-turbopack-stable4703
✅ nextjs-webpack-canary4703
✅ nextjs-webpack-stable4703
✅ nitro-stable4307
✅ nuxt-stable4307
✅ sveltekit-stable4307
✅ vite-stable4307
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable4307
✅ express-stable4307
✅ fastify-stable4307
✅ hono-stable4307
✅ nextjs-turbopack-canary4703
✅ nextjs-turbopack-stable4703
✅ nextjs-webpack-canary4703
✅ nextjs-webpack-stable4703
✅ nitro-stable4307
✅ nuxt-stable4307
✅ sveltekit-stable4307
✅ vite-stable4307
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable4307
✅ express-stable4307
✅ fastify-stable4307
✅ hono-stable4307
✅ nextjs-turbopack-canary4703
✅ nextjs-turbopack-stable4703
✅ nextjs-webpack-canary4703
✅ nextjs-webpack-stable4703
✅ nitro-stable4307
✅ nuxt-stable4307
✅ sveltekit-stable4307
✅ vite-stable4307
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack4703
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev300
❌ mongodb4613
✅ redis-dev300
✅ redis4703
✅ turso-dev300
❌ turso4433
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable4307
✅ e2e-local-postgres-nest-stable4307
✅ e2e-local-prod-nest-stable4307

📋 View full workflow run

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

This PR adds graceful handling for 409 Conflict responses when creating wait_completed events, addressing race conditions when multiple concurrent workflow invocations attempt to complete the same wait. The changes build on PR #1055 which added orphaned event detection, and work in conjunction with server-side changes that materialize waits as entities to prevent duplicate completions.

Changes:

  • Wraps wait_completed event creation in try/catch blocks to handle 409 conflicts gracefully
  • Applies 409 handling in both automatic wait completion (runtime.ts) and manual wake-up (runs.ts)
  • Updates event sourcing documentation to reflect that waits are now materialized entities in storage

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.

Show a summary per file
FileDescription
packages/core/src/runtime/runs.tsAdds 409 conflict handling in wakeUpRun() to count already-completed waits as successful
packages/core/src/runtime/runs.test.tsNew test file with unit tests for 409 handling and error cases in wakeUpRun()
packages/core/src/runtime.tsWraps automatic wait_completed event creation in try/catch to log and skip on 409 conflicts
docs/content/docs/how-it-works/event-sourcing.mdxUpdates documentation to clarify waits are materialized entities with atomic completion guarantees
.changeset/wait-complete-guard.mdAdds changeset for patch release describing the fix

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

pranaygpand others added 4 commits February 15, 2026 17:19
When multiple concurrent workflow invocations race to complete the same
wait, the server returns 409 (conflict) for duplicates. This change
handles the 409 gracefully in both runtime.ts (sleep elapsed check) and
runs.ts (wakeUpRun), preventing crashes and treating already-completed
waits as successful.
Also updates event-sourcing docs to reflect that waits are now
materialized as entities in storage with atomic completion guarantees.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ranch
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… duplicate wait_completed events
Adds Wait type/schema to the shared world package and implements wait entity
materialization in both local (filesystem) and postgres world implementations,
matching the DynamoDB behavior. wait_created creates a wait entity with status
'waiting', and wait_completed transitions it to 'completed' with guards that
reject duplicates (409).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
pranaygpand others added 4 commits February 15, 2026 17:54
…ation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* origin/main:
add closure comment in sleep test helper (#1071)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…rride
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM, but can we add (or modify) one e2e test to include Promise.all(sleep(1s) x 10) or similar? That'd probably ensure general race-condition safety for waits into the future

@pranaygp

pranaygp commented Feb 16, 2026

Copy link
Copy Markdown
ContributorAuthor

LGTM, but can we add (or modify) one e2e test to include Promise.all(sleep(1s) x 10) or similar? That'd probably ensure general race-condition safety for waits into the future

I already did that in the previous PR and it's been merged.

exportasyncfunctionparallelSleepWorkflow(){
'use workflow';
conststartTime=Date.now();
awaitPromise.all(Array.from({length: 10},()=>sleep('1s')));
constendTime=Date.now();
return{ startTime, endTime };
}

@pranaygp

Copy link
Copy Markdown
ContributorAuthor

Replying to @TooTallNate's code review:

Backward Compatibility

Good call — both local and postgres would throw 404 for waits created before this code was deployed. For Vercel production this is handled server-side (the backend backfills the wait entity in completed state on wait_completed if it doesn't exist — see vercel/workflow-server#265). For self-hosted postgres and local dev, I agree we should add backfill logic. I'll add that as a follow-up.

waitId Inconsistency

Fixed in a5e093c — both backends now use the raw correlationId as waitId, consistent with how steps and hooks use their correlation IDs as PKs.

Runtime 409 Handling

Agreed it causes one wasted invocation. The self-healing behavior is correct though — the next invocation picks up the persisted event and proceeds. I'll note this as an optimization for a follow-up (re-fetch the event on 409 and push it to the array to avoid the extra round-trip).

wakeUpRun 409 Handling

👍


Re: "world-postgres doesn't seem too happy" — this was fixed, the migration journal entry was missing. Added in ce2eb72.

pranaygpand others added 2 commits February 16, 2026 15:53
The server-side changes (vercel/workflow-server#265) have been merged to main
and deployed to production, so we no longer need to point at the preview URL.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* origin/main:
Remove "workflow/internal/serialization" export (#1082)
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.

4 participants

@pranaygp@TooTallNate@VaguelySerious