Skip to content

Conversation

@mikearnaldi
Copy link

Summary

This PR adds support for custom runId in the start() function to enable idempotent workflow creation. This solves the problem described in #85 where duplicate triggers (e.g., webhooks, events) could create multiple workflow runs for the same logical operation.

Changes

Core Implementation

  • packages/world/src/runs.ts: Added optional runId to CreateWorkflowRunRequest interface
  • packages/core/src/runtime/start.ts: Added optional runId to StartOptions interface
  • packages/world-local/src/storage.ts: Implemented idempotent runs.create() that returns existing run if runId already exists
  • packages/world-postgres/src/storage.ts: Implemented idempotent runs.create() with race condition handling using onConflictDoNothing + fallback fetch

Test Infrastructure

  • packages/world-testing/src/custom-runid.mts: Added 3 comprehensive integration tests covering:
    • Basic custom runId functionality
    • Idempotent behavior (duplicate runIds return same run)
    • Race condition handling under concurrent requests
  • packages/world-testing/src/server.mts: Added runId support to invoke endpoint
  • packages/world-testing/src/util.mts: Added runId parameter to invoke() helper
  • packages/world-testing/src/index.mts: Exported new test suite

Documentation

  • docs/content/docs/api-reference/workflow-api/start.mdx: Added usage examples for custom runId
  • docs/content/docs/foundations/idempotency.mdx: Added "Workflow-level Idempotency" section explaining the feature and use cases

Use Cases

This feature enables several important patterns:

  1. Webhook Deduplication: Prevent duplicate webhooks from creating multiple workflow runs
  2. Business Entity Uniqueness: Ensure one workflow per business entity (order ID, user ID, etc.)
  3. Retry-Safe Triggers: Make workflow creation safe to retry in distributed systems

Example Usage

import { start } from "@workflow/core"
import { processOrder } from "./workflows/process-order"

// Idempotent workflow creation - safe to call multiple times
const run = await start(processOrder, {
  runId: `order-${orderId}`,
  input: { orderId, items }
})

Testing

All existing tests pass (339 tests across 25 packages), plus 3 new integration tests specifically for this feature.

Closes #85

Add optional runId field to StartOptions and CreateWorkflowRunRequest to
enable idempotent workflow creation. When a custom runId is provided,
the system returns the existing workflow run instead of creating a duplicate.

Changes:
- Add runId?: string to StartOptions interface
- Add runId?: string to CreateWorkflowRunRequest interface
- Implement idempotent runs.create() in world-local backend
- Implement idempotent runs.create() in world-postgres backend with race condition handling
- Add runId support to world-testing server and utility functions
- Add 3 new integration tests for custom runId feature
- Update documentation with examples and use cases

Use cases:
- Webhook deduplication when providers send duplicate events
- Business entity uniqueness (e.g., one workflow per order ID)
- Retry-safe workflow triggers in distributed systems

The postgres implementation handles race conditions by using onConflictDoNothing
and fetching the existing run if the insert returns nothing, ensuring true
idempotency even under concurrent requests.

Tests: All 339 tests passing (including 3 new integration tests)
Closes vercel#85
@changeset-bot
Copy link

changeset-bot bot commented Oct 26, 2025

🦋 Changeset detected

Latest commit: 690e1d5

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

This PR includes changesets to release 11 packages
Name Type
@workflow/core Patch
@workflow/world Patch
@workflow/world-local Patch
@workflow/world-postgres Patch
@workflow/world-testing Patch
@workflow/cli Patch
@workflow/next Patch
@workflow/nitro Patch
workflow Patch
@workflow/world-vercel Patch
@workflow/ai Patch

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

@vercel
Copy link
Contributor

vercel bot commented Oct 26, 2025

@mikearnaldi is attempting to deploy a commit to the Vercel Labs Team on Vercel.

A member of the Team first needs to authorize it.

Wrap writeJSON() in try-catch to handle 409 conflicts when concurrent
requests attempt to create a workflow with the same custom runId.
When a conflict occurs, fetch and return the existing run to maintain
idempotency (matching PostgreSQL implementation pattern).
@mikearnaldi
Copy link
Author

Additional note, for this to work in Vercel the API that handles runs creation /v1/runs/create will most likely need to be extended to support runId, this is outside my power :)

runs: {
async create(data) {
const runId = `wrun_${monotonicUlid()}`;
const runId = data.runId ?? `wrun_${monotonicUlid()}`;
Copy link
Contributor

Choose a reason for hiding this comment

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

thoughts on

const runId = data.runId ? 
    `wrun_${data.runId}`
  : `wrun_${monotonicUlid()}`;

so the prefix is consistent?

Copy link
Member

@VaguelySerious VaguelySerious left a comment

Choose a reason for hiding this comment

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

Thanks for contributing! We definitely want to enable idempotency as soon as possible. This PR looks good overall, but I think we'd run into some problems without a few more checks/changes.

World-local requires IDs to be ULIDs because it extracts dates from them, and world-vercel relies on ID being time-sortable (in the backend). We can change world-local to not require ULIDs or prefixes, but this is harder for world-vercel (and our responsibility).

My take is this PR should be extended to:

  • Change world-local to not rely on prefixes or ULIDs, truly allowing custom IDs. Add tests for this.
  • Change world-vercel to validate that any custom ID follows the <prefix>_<ulid> schema. Then we can relax this requirement later (once we start supporting arbitrary custom IDs on Vercel backend). Could also export a helper from core/runtime to create IDs with that schema.

That'd be enough for me personally to merge this. Let me know what you think

@VaguelySerious
Copy link
Member

^ Updated my response to reflect some better next steps and outline requirements for what I'd like to see done to unblock this

@pranaygp
Copy link
Collaborator

I don't think we should change the core runId itself, but rather, provide a system for idempotency. That might just be an idempotency key on the run object, but there i also the problem of runtime idempotency (i.e. deduplicating based on data that's only available within the run, once it's started). This ties in to an upcoming spec we have for tagging runs (and all other entities). Will share more in a bit, but I think we have an elegant solution that would also cleanly apply to step based idempotency and cover all cases with one API

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add support for custom runId to enable idempotent workflow creation

4 participants