vercel / vercel/workflow

Add support for idempotent workflow creation

Open
#85 2 comments 12 reactions 1 assignee View on GitHub

@pranaygp is already working on this.

Since Nov 5, 2025.

Dominant language
TypeScript
Stars
2.4k
Forks
365
Avg merge
2d 11h
Merged PRs (30d)
169

Description

Problem

Currently, when starting a workflow using start(), there is no way to provide a custom runId. The system always auto-generates a unique ID. This creates problems in several common scenarios:

Use Cases Affected
  1. Webhook Deduplication: When webhook providers send duplicate events (common with retry logic), each call to start() creates a new workflow run, even if it's processing the same event.

  2. Business Entity Uniqueness: Applications often need to ensure only one workflow runs per business entity (e.g., one order processing workflow per order ID). Currently, there's no built-in way to enforce this.

  3. Retry-Safe Triggers: In distributed systems, workflow triggers may be retried. Without idempotency support, retries create duplicate workflow runs.

Example Scenario
// Webhook handler receives same event twice due to retry
async function handleOrderWebhook(orderId: string) {
  // Problem: Both calls create separate workflow runs!
  const run1 = await start(processOrderWorkflow, [orderId]);
  const run2 = await start(processOrderWorkflow, [orderId]); // Duplicate!
}

Proposed Solution

Add optional runId field to StartOptions:

interface StartOptions {
  deploymentId?: string;
  runId?: string; // NEW: Enable idempotent workflow creation
}
Usage Example
async function handleOrderWebhook(orderId: string) {
  const customRunId = `wrun_order_${orderId}`;
  
  // First call creates workflow
  const run1 = await start(processOrderWorkflow, [orderId], {
    runId: customRunId
  });
  
  // Second call returns existing run (idempotent)
  const run2 = await start(processOrderWorkflow, [orderId], {
    runId: customRunId
  });
  
  console.log(run1.runId === run2.runId); // true
}
Implementation Requirements
  1. world.runs.create() should accept optional runId
  2. If runId exists, return existing run instead of throwing error
  3. If runId not provided, auto-generate as before (backward compatible)
  4. All storage backends (world-local, world-postgres, world-vercel) should support this
Benefits
  • No breaking changes: Fully backward compatible
  • Simpler code: No external deduplication needed
  • Safer: Built-in idempotency guarantees
  • Standard pattern: Matches idempotency key patterns from Stripe, AWS, etc.

Related

This follows the same pattern already used for step-level idempotency with stepId (see docs).

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.