Files
vercel__workflow/.changeset/retry-idempotent-event-posts.md
Pranay Prakash 897aac979f fix(world-vercel): retry idempotent event POSTs in-process to avoid step re-execution (#2675)
* fix(world-vercel): retry idempotent event POSTs in-process to avoid step re-execution

undici's RetryAgent never retries a POST, so a transient transport blip (UND_ERR_REQ_RETRY, ECONNRESET, socket/headers timeout, transient 5xx) when committing a step's terminal event bubbles out, the queue redelivers, and the step's user code re-executes with attempt++ even though it already ran to completion.

workflow-server makes these writes idempotent in outcome: entity handlers run before the event-log row is inserted and state transitions are conditional writes excluding terminal states, so a retry whose original landed throws before any row is written and surfaces as a 409 the SDK already handles. This adds a bounded in-process retry (new event-retry.ts) gated by a validated per-event EVENT_RETRY_ELIGIBILITY map, excluding step_started (double-increments attempt), step_retrying (appends a duplicate row), and hook_received (no server guard).

Complements #2666, which routes completion-persistence failures to queue redelivery instead of recording them as user failures; this avoids the redelivery (and re-execution) for transient blips.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(world-vercel): address review on event-POST retry

- Don't retry external cancellation: drop AbortError from the transient set (keep self-timeout TimeoutError), so a caller-requested abort isn't re-issued or stalled by the backoff budget.

- Add DEBUG-gated logging on each retry and on retry exhaustion so an in-process retry vs. a fall-through to queue redelivery is distinguishable in logs.

- Clarify docs: a landed retry surfaces as 409 for most types, but run_started/attr_set return 200 success (not a 409).

- Add createWorkflowRunEvent integration tests (events-retry.test.ts): eventType is threaded into the retry wrapper, and the 404->HookNotFoundError mapping still fires after the retry loop.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Peter Wielander <mittgfu@gmail.com>
2026-06-28 16:05:23 -07:00

371 B

@workflow/world-vercel
@workflow/world-vercel
patch

Retry transient transport failures (e.g. UND_ERR_REQ_RETRY, ECONNRESET, socket timeouts, 5xx) in-process for idempotent-on-retry event POSTs, so a brief network blip after a step completes no longer re-executes the step. step_started, step_retrying, and hook_received are excluded as they are not safe to blindly retry.