Skip to content

Workflows: implement CreateBatch V2 in local-dev - #15571

Draft
Naapperas wants to merge 11 commits into
cloudflare:mainfrom
Naapperas:nunopereira/workflows-create-batch-v2
Draft

Workflows: implement CreateBatch V2 in local-dev#15571
Naapperas wants to merge 11 commits into
cloudflare:mainfrom
Naapperas:nunopereira/workflows-create-batch-v2

Conversation

@Naapperas

Copy link
Copy Markdown
Contributor

Fixes WOR-1452.

Implements local-dev behavior for the new BatchCreate V2 API being released for Workflows.

Based on top of #14847 due to it fixing the way we were rejecting duplicate IDs, which is needed for this work.

Docs pending.


  • Tests
    • Tests included/updated
    • Automated tests not possible - manual testing has been completed as follows:
    • Additional testing not necessary because:
  • Public documentation
    • Cloudflare docs PR(s):
    • Documentation not necessary because:

A picture of a cute animal (not mandatory, but encouraged)

A cute red panda in the snow

TheSaiEaranti and others added 11 commits August 12, 2026 07:01
…orkflows binding

The local binding created duplicate executions for deterministic instance ids:
create() never checked for an existing instance and createBatch() mapped every
input through create(), so code relying on the documented idempotency contract
appeared to work locally while double-executing workflow bodies.

Match the documented production behavior: create() with an id that already
exists throws and retains the existing instance, and createBatch() skips ids
that already exist, or repeat within the batch, excluding them from the result
and creating instances in batch order. Existence is decided by the engine
Durable Object via a new hasInstance() method that reads INSTANCE_METADATA,
which is written exactly once by the first init(), so the check is consistent
across isolates rather than relying on binding-local state. Auto-generated ids
skip the check.

Fixes cloudflare#14836
…tic-id contract

create() with a caller-provided id now throws once the instance exists, so
worker scripts that re-created the same fixed id on every request switch to
the pattern production code needs anyway: try create, fall back to get for
the existing instance.

Covers the miniflare workflows persistence spec (re-created "an-id" in a
polling loop and again after a restart), the dev-registry cross-worker
workflow script, and both vite-plugin workflow playgrounds (whose only
repeated-create path is a vitest retry against the still-running server).
…plicate-id checks

create(): restore the destructuring defaults. The ?? refactor was not
equivalent: an explicit null id became a generated id instead of being
rejected by validation, and an explicit null params was rewritten to {}.

createBatch(): validate every id before any existence probe or create.
Probing an id constructs the engine Durable Object, whose constructor
persists storage, so an invalid id previously left a phantom instance on
disk and rejected the batch only after earlier entries were already
created. A batch with a malformed id now creates nothing.

createBatch(): skip an id that a concurrent create claims between the
existence check and create(), per the skip contract, instead of failing
the batch part way through.

Three regression tests, each verified to fail on the parent commit.
…ded form

Every other instance error here goes through createWorkflowError(message,
code); the duplicate-id error was a free-form sentence, so code branching
on an error code would behave differently locally than against the real
service. Route it through a duplicateInstanceError(id) helper with the
code instance.already_exists, following the instance.not_found vocabulary,
and replace createBatch's substring matcher with a shared
isDuplicateInstanceError predicate so the thrower and matcher stay in
sync. The race regression test throws through the same helper.
…e-id error

The catch-all fallbacks treated every create() failure as "the id already
exists", so a real failure (malformed id, transient RPC error) was masked
by the instance.not_found error from the follow-up get(). Match the
instance.already_exists code and rethrow everything else, in both
playground workers and both miniflare test worker scripts.
createBatch checked every caller-provided id twice (its own loop plus the
identical probe inside create()) and awaited each probe sequentially, up
to 200 serialized engine round-trips for a 100-entry batch. Factor the
creation body into #createUnchecked, shared by create() after its probe
and by createBatch after probing each distinct id once in a concurrent
round. Creations stay in batch order; since init() is dispatched
fire-and-forget, the probes were the only awaited RPCs, so a batch now
costs one concurrent probe round.

The mid-loop duplicate catch and its regression test are removed as dead
code: with a single probe there is no second check to disagree with. A
concurrent external create landing between the batch probe and creation
remains the documented best-effort boundary.
…eset text

The uniqueness probe reads the metadata that init() commits after
create() returns, so duplicate creates racing ahead of that commit all
resolve successfully; the engine's init() guards keep the race from
double-executing the body. State that at the check site, and name the
coded error in the changeset.
…peat requests

The fixture workers create fixed deterministic ids (test, something,
batch-1/batch-2), so a vitest retry re-invoking an endpoint against the
same wrangler session now hit the duplicate-id error or an empty
createBatch result. The create endpoints fall back to get() for the
instance.already_exists error only, and the createBatch endpoint reads
handles for the full id set after the batch call, since existing ids are
skipped and excluded from its return.
@changeset-bot

changeset-bot Bot commented Sep 8, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: eabcfd1

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

This PR includes changesets to release 8 packages
Name Type
miniflare Minor
wrangler Minor
@cloudflare/deploy-helpers Patch
@cloudflare/pages-shared Patch
@cloudflare/remote-bindings Patch
@cloudflare/runtime-types Patch
@cloudflare/vite-plugin Patch
@cloudflare/vitest-plugin 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

@pkg-pr-new

pkg-pr-new Bot commented Sep 8, 2026

Copy link
Copy Markdown
@cloudflare/autoconfig

npm i https://pkg.pr.new/@cloudflare/autoconfig@15571

@cloudflare/build-output-utils

npm i https://pkg.pr.new/@cloudflare/build-output-utils@15571

@cloudflare/codemods

npm i https://pkg.pr.new/@cloudflare/codemods@15571

@cloudflare/config

npm i https://pkg.pr.new/@cloudflare/config@15571

create-cloudflare

npm i https://pkg.pr.new/create-cloudflare@15571

@cloudflare/deploy-helpers

npm i https://pkg.pr.new/@cloudflare/deploy-helpers@15571

@cloudflare/kv-asset-handler

npm i https://pkg.pr.new/@cloudflare/kv-asset-handler@15571

miniflare

npm i https://pkg.pr.new/miniflare@15571

@cloudflare/pages-functions

npm i https://pkg.pr.new/@cloudflare/pages-functions@15571

@cloudflare/pages-shared

npm i https://pkg.pr.new/@cloudflare/pages-shared@15571

@cloudflare/unenv-preset

npm i https://pkg.pr.new/@cloudflare/unenv-preset@15571

@cloudflare/vite-plugin

npm i https://pkg.pr.new/@cloudflare/vite-plugin@15571

@cloudflare/vitest-plugin

npm i https://pkg.pr.new/@cloudflare/vitest-plugin@15571

@cloudflare/workers-auth

npm i https://pkg.pr.new/@cloudflare/workers-auth@15571

@cloudflare/workers-editor-shared

npm i https://pkg.pr.new/@cloudflare/workers-editor-shared@15571

@cloudflare/workers-utils

npm i https://pkg.pr.new/@cloudflare/workers-utils@15571

wrangler

npm i https://pkg.pr.new/wrangler@15571

commit: eabcfd1

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

Labels

None yet

Projects

Status: Untriaged

Development

Successfully merging this pull request may close these issues.

3 participants