Enforces the published per-run events limit, which was previously not enforced. The server supplies the limit on the run_started response (separate change); once a run's event log reaches it, the runtime throws MaxEventsExceededError at the top of the replay loop, and the existing terminal-error path records it as run_failed with a new MAX_EVENTS_EXCEEDED code — instead of letting a runaway workflow (e.g. an unbounded step loop) grow the event log without bound.
Adds a new client side WORKFLOW_MAX_EVENTS_OVERRIDE env var which can override the server side provided value (lower only).
* [core] Enforce maxRetries for steps that time out
A step that is hard-killed by the platform function timeout writes no
step_failed/step_retrying, so `step.error` stays null and the error-based
max-retries guards never fire. Each redelivery re-runs step_started
(incrementing the attempt), so a timing-out step retried without bound
instead of stopping at maxRetries.
Enforce the retry ceiling BEFORE running the body, via a new
`authoritativeAttempt` param on executeStep:
- Inline (combined handler): count the step_started events already in the
event log for the step (+1 for this attempt). The log is authoritative
because the optimistic-start path synthesizes step.attempt = 1.
- Background (queue-dispatched): the queue delivery count (metadata.attempt),
which increments on the visibility-timeout redelivery a timed-out step
produces.
When the attempt exceeds maxRetries + 1 the step is failed without starting
another attempt. Thrown-error exhaustion is unchanged — it still terminates
via the post-body guard one attempt earlier, with the thrown error as cause.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Apply suggestions from code review
Co-authored-by: Peter Wielander <mittgfu@gmail.com>
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
* [core] Verify step_started count before failing a backgrounded step
`metadata.attempt` (queue delivery count) also advances for redeliveries
that never run the step body (ThrottleError, TooEarlyError, other pre-body
failures), so trusting it directly could fail a step as "exceeded max
retries" before the body ever ran.
Use the delivery count only as a fast gate: while it is at or under the
ceiling the step can't be exhausted, so proceed without touching the log.
Only once it crosses the ceiling, load the full event log and derive the
authoritative attempt from the recorded step_started count (which only real
attempts write) — excluding throttle/too-early redeliveries. The load also
primes the replay's cachedEvents/eventsCursor.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* [core] Skip the event-log scan for brand-new inline steps
Deriving an inline step's attempt number by scanning the cumulative event
log for step_started events ran for every inline execution, which is O(n²)
across a long sequential workflow.
A lazy inline step is brand-new by construction (it only enters the batch
with no step_created yet), so it has zero prior starts and is always attempt
1 — no scan needed. Reserve the scan for owned-recovery re-runs (this
message re-executing a step it crashed/timed out on), which are uncommon and
few per batch.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* [world-vercel] Idempotent retry policy for stream close (5xx retriable)
Stream close is the one idempotent stream PUT: a duplicate close of a
completed stream early-returns on the server, and the close-barrier
protocol's durable `closing` fence is an if_not_exists stamp that a
re-entered close resumes. The barrier protocol relies on close retrying
5xx: transient reconciliation failures — and unsafe close shapes
awaiting in-flight backups — surface as retriable 503s with the stream
left durably closing, expecting the writer to close again. Under the
write dispatcher's no-5xx policy (correct for non-idempotent chunk
appends), that 503 rejected writer.close() outright and left the stream
fenced until run expiry.
Close now uses its own shared RetryAgent (429 + 5xx + transient
connection errors, Retry-After honored); chunk writes keep the narrowed
no-5xx policy unchanged. Contract pinned by tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* changeset for stream close retry
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* concise changeset
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* docs: replace migration guides with a Comparisons section
Add a Comparisons section (v4 + v5) with an index/snapshot across all frameworks and deep-dive pages for Temporal, Cloudflare Workflows, AWS Step Functions, AWS Bedrock AgentCore, Inngest, and trigger.dev. Remove the old migration-guides section, folding its concept-mapping and migration content into the relevant comparison pages, and repoint top-level nav in both versions.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs: refresh comparison pages with current facts (July 2026)
Re-verified each comparison against the vendor's current public docs and
updated what changed since the June 2026 snapshot:
- Temporal: Worker Versioning is now GA; Serverless Workers (AWS Lambda,
pre-release) scale to zero, so soften the blanket "no scale-to-zero";
drop the unsubstantiated "Uber" customer claim (Uber is Cadence's origin,
not a Temporal customer).
- Cloudflare Workflows: note the new per-step billing dimension (500K/mo
included, then $0.80/100K) landing no earlier than Aug 10, 2026; note the
50K concurrency ceiling was raised from 4,500 at GA.
- AWS Bedrock AgentCore: add newer GA modules (Harness, Policy, Evaluations);
correct compliance (SOC/PCI/ISO under internal assessment, audits pending;
FedRAMP not yet authorized; drop GovCloud claim); refresh languages
(@aws/agentcore CLI scaffolds TS or Python); "some modules preview" is stale.
- Inngest: Pro pricing $75 -> $99/mo; encryption middleware now TS + Python;
AgentKit/Realtime are Developer Preview and Connect is public beta; self-host
is community/best-effort (not "unsupported"); Free-tier run duration 30 days
vs 366 on Pro; soften funding to ~$30M+.
- AWS Step Functions & trigger.dev: facts re-confirmed; date stamp only.
Bumped every "as of June 2026" stamp to July 2026. v4 and v5 kept identical.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: present comparisons in the present tense; v5-only; Pro usage-based pricing
Follow-up pass on the comparison pages:
- Remove all date references (past and future). Anything that lands on a
date is stated as already in effect: Cloudflare's per-step billing, Temporal
Serverless Workers and GA Worker Versioning, AgentCore's Harness/Policy/
Evaluations modules. Dropped "as of July 2026" stamps, founding/GA years,
funding round dates, and roadmap/"being added" phrasing.
- Workflow SDK: reference v5 only and treat it as GA (was "v4 GA / v5 beta").
- Pricing and limits: quote the Pro/paid tier only and usage-based rates only;
drop plan-included quotas and free-tier allowances (Step Functions 4K/mo free,
Cloudflare 500K steps/mo included, Inngest 50K free execs, Inngest/Free 30-day
run cap, Temporal $100/mo plan minimum).
v4 and v5 kept identical.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: tighten comparison maturity/status wording
- Drop dateless "upcoming change" phrasing: AgentCore compliance now states
current facts only (no "self-assessed"/audit-pending implication); remove
Inngest's "SSPL → Apache after 3 yrs" license-conversion note.
- Don't label the Workflow SDK "GA" — non-beta is the default; also drop bare
"GA" where it only meant "not beta" (Temporal "7 SDKs", Inngest "TypeScript",
competitor maturity cells).
- Maturity cells no longer cite version numbers; they describe backing/track
record instead (e.g. "Built and maintained by Vercel", "Backed by AWS").
v4 and v5 kept identical.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: make "features without a 1:1 equivalent" sections directional
Rename each heading to name the competitor that has the feature (e.g.
"Temporal features without a direct Workflow SDK equivalent") and add a
lead-in clarifying these are the competitor's capabilities the Workflow SDK
doesn't replicate one-to-one, with how to cover each on the Workflow SDK side.
v4 and v5 kept identical.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: point world links at the /worlds routes
The comparison pages linked to /docs/deploying/world/* and
/docs/deploying/building-a-world, which no longer exist in the docs
trees (the Docs Links check rejects them on v5 pages, where /docs hrefs
are render-rewritten and skip the legacy redirects). Link the canonical
/worlds/* routes directly, in both body links and frontmatter refs.
v4 and v5 kept identical.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: address toolbar review feedback on the comparison pages
- Drop the Maturity row from every at-a-glance table
- Add a "what the limits mean in practice" paragraph to each comparison,
spelling out what the competitor's caps mean for long-running AI
workloads, and link Vercel World limits to the pricing doc
- Security cells: lead with zero-config per-run E2E encryption and note
platform security is per-World, instead of the VM-sandbox framing
- Temporal: drop the throughput sentence and the still-in-preview
Serverless Workers mention from the performance cell
- Cloudflare: end the recommendation on "already all-in on Cloudflare"
- Convert the "features without a direct equivalent" bullet lists into
two-column tables so it's unambiguous which product owns each feature
- Fix the Inngest page's "no step cap" cell (Vercel World caps runs at
10K steps per the pricing doc)
v4 and v5 kept identical.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Peter Wielander <peter.wielander@vercel.com>
* test: regression coverage for hook.resume() from isolated route bundles (o2flow beta.26 incident)
Reproduces the o2flow v5 upgrade failure (workflow@5.0.0-beta.26, fixed by
#2752 in beta.28): a plain API route importing defineHook() from the root
`workflow` entry and calling .resume() failed with Turbopack's
"Cannot find module as expression is too dynamic" stub, because the world
registration was tree-shaken out of the route bundle and getWorldLazy()'s
dynamic-import fallback got stubbed.
The bug only manifests when a route bundle loads in isolation (a Vercel
lambda): local `next dev`/`next start` evaluates next.config.ts, whose
workflow/next import chain registers the world process-wide and masks it —
which is why no existing server-driven suite caught it.
- route-bundle-isolation.test.ts: production Turbopack build of the
nextjs-turbopack workbench, then loads ONLY the compiled route bundle in a
bare Node subprocess (cold-lambda simulation) and invokes its POST handler.
Fails with the exact incident error on regressed code; passes on main.
Wired into the build-error-messages CI job.
- e2e: plainModuleDoneHook round-trip through a plain API route on the two
Next workbenches (deployed matrix covers real lambda isolation).
- Workbench fixtures mirroring o2flow: a directive-less defineHook module
shared by a workflow (create) and a plain route (resume). The webpack
workbench gets a real route file because `next dev` (webpack) does not
serve directory-symlinked app routes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Pranay Prakash <pranay.gp@gmail.com>
* test: authenticate plain hook resume request
* test: address review — marker-based harness output parsing, changeset summary
- route-bundle-isolation: prefix the harness result line with a unique
marker and locate it explicitly instead of JSON.parse()ing the last
stdout line, so stray logging from the route bundle or the world can't
break parsing; failures now include the full subprocess stdout.
- changeset: add a human-readable summary to the (release-less) changeset.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Pranay Prakash <pranay.gp@gmail.com>
---------
Signed-off-by: Pranay Prakash <pranay.gp@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Karthik Kalyanaraman <karthik.kalyanaraman@vercel.com>
Co-authored-by: Karthik Kalyan <105607645+karthikscale3@users.noreply.github.com>
* docs: make /worlds the canonical home for World docs
The world pages (Local/Postgres/Vercel) and Building a World were
duplicated inside the v4 and v5 docs trees while /worlds/[id] rendered
the v4 copy — hiding v5-only content like multi-region and leaving two
diverging sources of truth.
- Move world docs to an unversioned docs/content/worlds/ collection
(based on the v5 copies, with inline 4.x callouts for factory naming
and 5.x-only env vars), rendered at /worlds/*
- Add /worlds/building-a-world; flatten the docs Deploying section to a
single intro page and drop its Rocket icon
- Point every link, frontmatter ref, and worlds-manifest docs field at
/worlds/*; add redirects for the removed v5 and building-a-world URLs
- Keep world docs on agent-facing surfaces: search, llms.txt,
sitemap.md/.xml, and .md exports now serve the worlds collection
- Extend the docs link linter to validate worlds pages (with heading
anchors) and their outgoing links
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Pranay Prakash <pranay.gp@gmail.com>
* docs: version the world docs like the docs trees (v4/v5 switcher)
Instead of a single unversioned copy, world docs now follow the same
versioning strategy as the docs pages: content/worlds/v4 is served at
/worlds/* (current) and content/worlds/v5 at /v5/worlds/*, restoring the
original per-version content. Each world detail page (and Building a
World) renders the docs version switcher — the worlds listing page has
no natural home for it, so it lives on the world pages themselves.
- Render-time href rewriting on v5 pages now covers /worlds/... links
(shared rewriteHrefForVersion helper, also used by the v5 docs and
cookbook routes), and the markdown-export rewrite does the same
- v5 world pages are noindexed with a canonical to /worlds/<id>;
community worlds stay unversioned (/v5/worlds/<id> redirects)
- /v5/docs/deploying/world/* redirects now land on /v5/worlds/*;
/v5/worlds and /v5/worlds/compare redirect to the unversioned pages
- Link linter models the versioned worlds URL spaces (v5 pages resolve
/worlds hrefs against the v5 collection); sitemap.md and the .md
export routes cover /v5/worlds/*
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Pranay Prakash <pranay.gp@gmail.com>
* docs: fix v4 multi-region anchor and tighten version-prefix matching
Address PR review:
- The v4 Deploying page linked /worlds/vercel#multi-region, but the
Multi-region section only exists on the v5 world page; use the
explicit cross-version /v5/worlds/vercel#multi-region link (this was
the Docs Links CI failure)
- rewriteHrefForVersion now uses the boundary-checked hasPathPrefix
(shared leaf module lib/geistdocs/path-prefix.ts, also used by
source.ts) instead of bare startsWith
- buildVersionUrl's shared-route fast path is segment-based rather than
substring includes()
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Pranay Prakash <pranay.gp@gmail.com>
---------
Signed-off-by: Pranay Prakash <pranay.gp@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Hook timeline bars were amber/yellow via RESOURCE_COLORS; passive
spans should be gray to match the event list icons and minimap.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
* Give Metadata Token and Hook ID copy + truncation
Add token to the copyable metadata attributes set and constrain
copyable key-value rows so MiddleTruncate can shrink long IDs.
* Remove AttributePanel copy unit tests
The Metadata Token/Hook ID copy change is small enough that the
dedicated panel render tests are unnecessary.
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
* fix(nitro): support React Router Vite builds
* refactor(nitro): simplify React Router cleanup
* docs(react-router): specify cleanup version
* fix(nitro): close temporary Vite servers
The recoverActiveRuns factory option had no environment variable, so
disabling startup re-enqueueing of pending/running runs required a custom
world module via WORKFLOW_TARGET_WORLD. Wire an env fallback
(0/false disables, 1/true enables, explicit factory option wins) and
document it in the worlds configuration reference and local world guide.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>