Commit Graph

78 Commits

Author SHA1 Message Date
github-actions[bot] 5556b4cde5 fix(core): make step-argument serialization failures catchable in workflow code (#3675) (#3687)
* fix(core): make step-argument serialization failures catchable in workflow code

A step whose arguments fail to serialize is now finalized by the
suspension handler as step_created + step_failed (mirroring a step-body
failure) instead of rejecting the whole suspension. The next replay —
forced in-process, since no step message is dispatched for the failed
step — rejects the step's promise with the SerializationError, so a
try/catch around the step call observes it. Uncaught, the error
propagates out of the workflow body and fails the run as a fatal
USER_ERROR immediately, instead of redelivering the orchestrator
message until max deliveries (49/48) as reported in production on v4.

* Serialize the step_failed error with the VM global; one-sentence changeset

Addresses review feedback: dehydrateStepError in
finalizeUnserializableStep now receives suspension.globalThis like every
other dehydration in this file. Error detection is realm-independent, so
the host-created SerializationError serializes identically, but VM-realm
values guest code threw into the cause chain are now detected by the
realm-sensitive reducers.

* Address review: QuickJS engine support, deferred-batch join, drain gate, placeholder marker, telemetry, docs

- QuickJS: dumpPendingOps now catches a step input's serialization
  failure per-op, reframes it as a SerializationError with the same
  framed message as dehydrateStepArguments, and surfaces it on the
  pending op instead of failing the whole collection. The entrypoint's
  dispatchPendingOps finalizes such steps as step_created (placeholder
  input) + step_failed, excludes them from inline claims and queue
  publishes, marks them handled, and raises the requeue signal so the
  failure is observed even when the feed lags — mirroring the node:vm
  engine, so both engines agree: catchable in workflow code, USER_ERROR
  with the framed message when uncaught. Both step-argument e2e tests
  now pass on WORKFLOW_VM=quickjs.
- runtime.ts: the failed-step replay path now joins
  suspensionResult.deferredBatchWork before continuing, so a trailing
  chunk commit or step-message publish rejection propagates instead of
  being swallowed after ack; committed inline claims are documented as
  deliberately handed to owned recovery.
- Terminal drain: finalization is gated on a stepDispatch target. The
  drain caller has no replay to observe a finalization, so a completed
  run no longer gains failed-step rows for an unawaited unserializable
  step — the rethrown error is swallowed by the drain's catch,
  preserving its pre-existing behavior.
- The placeholder input now carries a marker string ('[input
  unavailable: step argument serialization failed]', shared via
  runtime/unserializable-step.ts) so inspect/o11y don't render the
  failed step as a genuine zero-argument call.
- New workflow.steps.failed_serialization span attribute on the
  suspension span, so occurrence is measurable without log search.
- Docs: v5 serialization-failed error page documents where each
  boundary's failure surfaces (catchable step failure vs run failure)
  and the no-retry USER_ERROR semantics; foundations/errors-and-retries
  gains a Serialization Failures section with the try/catch shape.

* Guard the finalization crash window; self-contained docs samples

- A crash or transient failure between finalization's two durable
  writes leaves a lone placeholder step_created, and redelivery then
  dispatches the step through normal crash recovery — previously
  running user code with the placeholder arguments. The placeholder
  now carries a structural flag on the input triple's top level (which
  user code never controls, so no false positives), and the step
  executor checks it after hydration: instead of running the body, it
  throws the intended fatal SerializationError, completing the
  interrupted finalization as step_failed. Applies to both engines
  (they share the placeholder and the executor).
- Regression tests: executor fails a placeholder-input step without
  running the body (and doesn't trip on a genuine argument equal to
  the display marker); handleSuspension rejects for redelivery when
  step_failed can't be written after step_created landed, leaving the
  recoverable placeholder behind; mixed bad-step + large fan-out
  returns the failure set alongside still-pending deferredBatchWork
  whose rejection surfaces — the contract the runtime's failed-step
  join (added previously) relies on.
- Docs: the two new code samples are now self-contained so the docs
  code-sample typecheck passes.

Signed-off-by: Nathan Rajlich <n@n8.io>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-20 18:44:04 +00:00
github-actions[bot] 643cae23d3 test: reduce e2e timing flakes (#2665) (#2701)
* test: reduce e2e timing flakes

* test: tighten e2e timing bounds

Signed-off-by: Nathan Colosimo <110621881+NathanColosimo@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-30 10:33:34 -07:00
github-actions[bot] 04e78129dc Propagate trace context to vercel-workflow.com in workbench instrumentation (#2601) (#2602)
* Propagate trace context to vercel-workflow.com in workbench instrumentation

@vercel/otel only propagates W3C trace context to Vercel deployment URLs
by default, so outgoing requests to the workflow-server
(vercel-workflow.com) got a client span with no `traceparent` header —
breaking the APM trace link to workflow-server's spans. Add
`instrumentationConfig.fetch.propagateContextUrls` for the workflow-server
domain in every workbench that uses @vercel/otel: example,
nextjs-turbopack, nextjs-webpack, and sveltekit. The Next.js and SvelteKit
apps already declared @vercel/otel but weren't registering it at all; they
now do.



* Also propagate trace context to the Vercel Queue Service (vercel-queue.com)

The workflow-server queue path (@vercel/queue) sends to regional
vercel-queue.com subdomains (e.g. iad1.vercel-queue.com) when not using the
queues proxy, which were missing a `traceparent` header for the same reason
as vercel-workflow.com. Add `/vercel-queue\.com/` to propagateContextUrls in
all four workbench instrumentation configs.



---------

Signed-off-by: Pranay Prakash <pranay.gp@gmail.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Peter Wielander <mittgfu@gmail.com>
2026-06-29 13:04:09 -07:00
github-actions[bot] 97d4bd334d chore: ignore workflow swc caches (#2640) (#2650)
Signed-off-by: Nathan Colosimo <110621881+NathanColosimo@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-26 11:53:34 -07:00
github-actions[bot] f9119d4b6a fix(world-local,world-postgres): make duplicate hook_created idempotent (#2295) (#2374)
Co-authored-by: Nathan Rajlich <n@n8.io>
Co-authored-by: Peter Wielander <peter.wielander@vercel.com>
Co-authored-by: Peter Wielander <mittgfu@gmail.com>
2026-06-15 15:20:32 +02:00
github-actions[bot] ffa053ecb5 Backport #2387: test: e2e coverage for run-idempotency conflict-handling strategies (#2402)
* test: e2e coverage for run-idempotency conflict-handling strategies (#2387)

* test: e2e coverage for run-idempotency conflict-handling strategies

Covers the patterns documented in foundations/idempotency:
- claim-only hook mutex: token claimed and held with no payload data,
  duplicate identifies the owner, token released after completion
- adopt the owner's result via conflict.returnValue
- signal the owner: duplicate forwards its payload via resumeHook
- supersede: duplicate cancels the owner and reclaims the token
- route-side resume-or-start retry pattern reaching the started run

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: fix adopt-owner-result race — gate owner completion on observed conflict

On slow runtimes the duplicate's first invocation could land after the
owner completed and released the token, making the duplicate a fresh
owner that waits forever for a payload (90s timeout across CI matrices).
Poll the duplicate's event log for hook_conflict before resuming the
owner, and widen the test timeout for the added gate budget.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* review: assert superseded owner's returnValue rejection; empty changeset

- Await run1.returnValue and assert WorkflowRunCancelledError so the
  cancellation is verified end-to-end and no rejection leaks from the
  supersede test.
- Test-only PR: use an empty changeset.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ci: retrigger preview deployments (turbopack deployment for 2e9d000 wedged in esbuild hang)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ci: bust poisoned turbo cache entry for nextjs-turbopack build

The 2e9d000 deployment's next build crashed in an esbuild hang but its
task (70724907c9dd3a29) was recorded into the turbo remote cache anyway,
so every subsequent build with the same input hash replays the broken
artifact (missing routes-manifest). Change a build input to force a
fresh execution.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Pranay Prakash <pranay.gp@gmail.com>

* fix(backport): adapt conflict-handling tests to stable's getConflict/getRun API

The backport of #2387 used APIs that only exist on `main`, breaking the
nextjs-turbopack/webpack builds and the e2e suite on `stable`:

- `hookAdoptOwnerResultWorkflow`/`hookSupersedeOwnerWorkflow` read
  `conflict.returnValue`/`conflict.cancel()`, but on `stable`
  `getConflict()` resolves with `{ runId }`. Resolve the owning run via
  `getRun(conflict.runId)` inside a step (the documented stable pattern)
  to await its result / cancel it.
- Import `resumeHook` from `workflow/api` in 99_e2e.ts (was used by
  `forwardPayloadToOwner` but never imported).
- Convert the backported `waitForHook(token, { runId })` call sites to
  `waitForHookState(token, predicate)`; `waitForHook` does not exist on
  `stable` (#2405 standardized on `waitForHookState`).

Both workbench builds and `biome check` pass locally.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(backport): import HookNotFoundError in e2e test

The backported conflict tests call `HookNotFoundError.is()` in
`hookClaimOnlyMutexWorkflow` (token-release wait) and the resume-or-start
route test, but the import was never carried into the stable test file —
causing a runtime `ReferenceError: HookNotFoundError is not defined`.
Import it from `@workflow/errors` (matches `main`).

Verified locally against nextjs-turbopack: the two previously-failing
tests plus the adopt/signal/supersede rewrites all pass (5/5).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Signed-off-by: Pranay Prakash <pranay.gp@gmail.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Pranay Prakash <pranay.gp@gmail.com>
2026-06-14 01:00:20 -07:00
github-actions[bot] 296b785db0 Replace hook.hasConflict with hook.getConflict() returning the conflicting Run (#2373) (#2382)
* feat: replace hook.hasConflict with hook.getConflict (Promise<Run | null>)

hasConflict's boolean didn't expose WHICH run owns the token, so the
duplicate run couldn't act on the conflict. getConflict resolves with
null once registration commits, or with a Run handle for the conflicting
run — letting the workflow return/log the owner's runId, inspect its
status, await its result, or cancel it and continue, all in code.

The workflow-mode create-hook module exposes the bundle's compiled Run
class (durable step-proxy methods) on a well-known symbol so the host-
side hook consumer can construct the conflicting run inside the VM.
Contexts without the class (plain unit tests) fall back to a { runId }
object, which is also the documented v4 shape (no native Run
serialization in v4).



* fix: never resolve getConflict with a non-Run fallback shape

getConflict's contract is Promise<Run | null>. In the degenerate cases
where a real Run cannot be constructed — a hook_conflict event persisted
by an old world without conflictingRunId, or a context that never loaded
the workflow-mode create-hook module — reject with HookConflictError
instead of resolving with a { runId }-shaped impostor.

Test harnesses now register the Run class on the (VM) globalThis like
real bundles do.



* refactor: make getConflict a method — hook.getConflict()

A property getter that triggers registration/suspension reads as passive
state; a method makes the side effect explicit. Update implementation,
types, tests, e2e workflows, docs, and changeset.



* review: guard Run class registration, fix anchors, clarify changeset

- Only register WORKFLOW_RUN_CLASS when the workflow runtime is present
  (WORKFLOW_CREATE_HOOK installed on globalThis), so host imports of the
  workflow-mode module neither mutate the host global nor expose the
  non-step-proxy host Run.
- Drop #run-idempotency link fragments — that section lands in the
  stacked docs PR (#2011), which restores the anchored links.
- Note in docs that getConflict() rejects with HookConflictError for
  legacy hook_conflict events lacking the owner's run ID.
- Changeset now calls out the hasConflict -> getConflict() replacement.



* refactor: resolve the conflicting Run through the serialization class registry

Replace the bespoke WORKFLOW_RUN_CLASS global with the registry the
serialization pipeline already uses to revive Run instances:

- The SWC plugin already auto-registers the workflow bundle's compiled
  Run in globalThis[workflow-class-registry], but under a path-derived
  classId the host cannot know statically. The workflow-mode create-hook
  module now aliases it under a stable id (class//workflow//Run) via a
  new aliasSerializationClass() helper (a plain registry entry —
  registerSerializationClass cannot be reused since the plugin's IIFE
  already defined the non-configurable classId property).

- createConflictingRun() looks the class up with
  getSerializationClass(RUN_CLASS_ID, ctx.globalThis) and constructs
  through its WORKFLOW_DESERIALIZE hook, exactly as the Instance reviver
  would for a serialized Run crossing from a step into the workflow.

- Because the registry is keyed per-global, no environment guard is
  needed: a stray host-side import registers the host Run on the host
  registry, which is the correct class for that context. The
  WORKFLOW_CREATE_HOOK guard, the ??=, and the WORKFLOW_RUN_CLASS symbol
  are all deleted.

Verified: 1156 core unit tests; compiled workbench bundle contains the
stable alias alongside the plugin's path-derived registration with zero
WORKFLOW_RUN_CLASS references; all 5 hookGetConflict e2e tests pass
against a local nextjs-turbopack dev server, including conflict
resolution reading conflict.status through a durable step.

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Nathan Rajlich <n@n8.io>
2026-06-12 11:11:57 -07:00
github-actions[bot] 3a1a88f6f0 Add hook.hasConflict for early hook conflict detection (#2015) (#2372)
* feat: add hook ready promise

* test: cover hook ready continuation scheduling

* feat: replace hook.ready with hook.hasConflict (Promise<boolean>)

- hook.hasConflict resolves true when the token is owned by another
  active hook, false once registration is committed — no throw, so
  workflows can branch on conflicts early. Awaiting it suspends the
  workflow to commit the hook registration (createHook alone does not).
- Chain the already-created fast-path through promiseQueue so
  resolution order matches event-log order (review feedback).
- Skip inline step execution when a suspension has an awaited hook
  creation so the hasConflict continuation can advance independently
  of step execution (review feedback).
- Update unit tests, e2e tests, workbench workflows, and v4/v5 docs.



* docs: fix inconsistent hasConflict bullet in create-webhook reference

State both resolution values explicitly (true = token already owned,
false = registered) instead of a parenthetical that only described the
false case.

* docs: require docs preview links in PR descriptions for docs changes



* docs: restore SWC Plugin heading in AGENTS.md



---------

Signed-off-by: Pranay Prakash <pranay.gp@gmail.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Nathan Rajlich <n@n8.io>
2026-06-11 15:49:57 -07:00
github-actions[bot] 9092640013 feat(core): support passing parent WritableStream to child workflow via start() (#2059) (#2070)
* test(e2e): cover WritableStream passed as start() argument

Adds an e2e workflow + test where a parent workflow gets a WritableStream
via getWritable(), forwards it through start() to a child workflow, and
the child step writes raw bytes to it. Asserts the external reader on
the parent's stream observes the exact bytes the child wrote.

* fix(core): avoid double-framing when WritableStream is forwarded via start()

When a workflow's getWritable() handle is passed across start() to a
child workflow, the parent step's reviver wraps it in a serialize
transform that pipes into a workflow server stream. Until now,
getExternalReducers.WritableStream then installed a second serialize
transform on top of that — so every chunk the child step wrote got
devalue-framed twice but only deframed once on the reader side, and
external consumers saw the inner frame instead of the original bytes.

Fix: tag every user-visible writable that's already backed by a
workflow server stream with its (runId, name). When the external
reducer recognizes those tags during dehydration, it bridges bytes
straight from the new child-side server stream to the original server
stream instead of piping through the user's writable. That leaves the
producer-side serialize transform (installed once by the child's step
reviver) as the only framing layer in the chain.

* fix(core): forward (runId, name) when a tagged WritableStream crosses start()

Replaces the previous in-process bridge with first-class writable
forwarding at the descriptor level. When a parent workflow's
getWritable() handle is passed as an argument to a child workflow,
the dehydrated descriptor now carries the original (runId, name).
The child run's step-side reviver opens the writable against the
parent's server stream directly and resolves the parent run's
encryption key (encrypt-only) via getEncryptionKeyForRun.

This removes the architectural limitation that the bridge could
only stay alive for the duration of the parent step process — on
Vercel that capped forwarding at ~15 minutes regardless of the
child run's lifetime, dropping any writes the child made after the
parent step process exited.

importKey() now accepts a usages parameter, defaulting to
['encrypt', 'decrypt']. The cross-run forwarding path imports with
['encrypt'] only so a compromised child run cannot decrypt any
existing data on the parent's stream — only contribute new writes.

* test: rename writable-forwarded workflows and cover step-context getWritable()

Addresses PR review:

- Rename writableForwardedToChildChildWorkflow → writableForwardedChildWorkflow
  (drops the duplicated 'Child' segment).
- Split writableForwardedToChildWorkflow into two variants covered by a
  test.each: writableForwardedFromWorkflowWorkflow (workflow-context
  getWritable, the original test) and writableForwardedFromStepWorkflow
  (step-context getWritable passed directly into start() from the same
  step that called getWritable()).
- Terser changeset description.

Signed-off-by: Nathan Rajlich <n@n8.io>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Nathan Rajlich <n@n8.io>
2026-06-01 12:10:36 -07:00
github-actions[bot] 6aabd6fd2e [swc-plugin] Capture lexical this for nested arrow step functions (#1935) (#1945)
* [swc-plugin] Capture lexical `this` for nested arrow step functions

When a nested arrow `"use step"` references the enclosing function/method's
`this`, plumb that `this` through the workflow runtime so the step body
sees the correct receiver.

- Workflow mode wraps the step proxy with `.bind(this)`, so invoking the
  proxy captures the caller's `this` as `thisVal` on the queue item.
- Step mode hoists the body as a regular `function` (not an arrow) so the
  runtime's `stepFn.apply(thisVal, args)` rebinds `this` inside the
  hoisted body.

Detection only fires for arrows, since arrows inherit `this` lexically.
Nested non-arrow functions/methods/getters/setters introduce their own
`this`, so the detector stops at those boundaries.

The runtime already supported `thisVal` for instance-method steps; this
PR is purely a compiler change to feed the existing pipeline.

Caveat: capture works at runtime only when the captured value is
serializable across the workflow->step boundary (i.e. the enclosing
class implements `WORKFLOW_SERIALIZE`/`WORKFLOW_DESERIALIZE`).

Refs vercel/workflow#1865

* Address PR review: preserve step proxy metadata + tighter `this` detection

- core: Override `.bind` on step proxies so the bound function retains
  `stepId` and `__closureVarsFn`. Without this, a bound proxy that flows
  through workflow serialization (e.g. as a step argument) would be
  treated as a non-serializable plain function by `getStepFunctionReducer`.
- swc-plugin: Detector now also walks `arrow.params` so `this` references
  in default values / destructuring initializers (e.g. `(x = this.foo) =>
  ...`) trigger the `.bind(this)` path.
- swc-plugin: Class bodies inside the arrow body are now treated as
  `this`-binding boundaries — `this` inside class field initializers,
  methods, etc. is bound to the class instance, not the outer arrow. The
  detector still walks `extends` clauses and computed property keys
  because those are evaluated in the surrounding scope.
- spec.md: Sharpen the note about `this` in step bodies — it's
  syntactically allowed but only meaningful for instance-method steps and
  lexical-`this` arrow steps; other shapes compile but `this` will be
  whatever the caller of the step proxy passes.
- Add `lexical-this-detector-edge-cases` fixture covering both the
  default-param positive case and the inner-class false-positive guard.
- Strengthen the runtime test to assert `stepId` / `__closureVarsFn`
  survive `.bind(...)`.

* [swc-plugin] Fix `arguments` closure-var capture; drop dead `this`/`arguments` checks

- Add `arguments` to `is_global_identifier` so it's not captured as a
  closure variable. Previously a nested `function`-form step like

      function step() { 'use step'; return arguments[0]; }

  was hoisted with `const { arguments } = ...` (a strict-mode syntax
  error) and the body's `arguments[0]` resolved against the destructured
  binding instead of the function's intrinsic `arguments` object.
- Remove dead `ForbiddenExpression` checks for `this` and `arguments` in
  `visit_mut_this_expr` / `visit_mut_ident`. The `'use step'` /
  `'use workflow'` directives are stripped during the module-level
  traversal before children are visited, so `in_step_function` /
  `in_workflow_function` are never observed as true here in practice.
  The existing `step-with-this-arguments-super` fixture explicitly
  documents that all three identifiers are allowed in step bodies.
- Tighten the spec note about `arguments` accordingly: it works in
  `function`-form steps (reflecting positional args) but is not captured
  for arrow-form steps; use `...args` for that case.
- Add `nested-step-arguments` fixture pinning down the new behavior.

Signed-off-by: Nathan Rajlich <n@n8.io>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Nathan Rajlich <n@n8.io>
2026-05-22 10:16:25 -07:00
github-actions[bot] 478a9c7618 Generate local gitignore when using public workflow manifests (#1683) (#2085)
Signed-off-by: Pranay Prakash <pranay.gp@gmail.com>
Co-authored-by: Peter Wielander <peter.wielander@vercel.com>
Co-authored-by: Peter Wielander <mittgfu@gmail.com>
2026-05-22 15:37:33 +02:00
Gregor Martynus 7faedb7967 fix(ai): preserve provider tool identity across step boundaries (#1663)
* fix(ai): preserve provider tool identity across step boundaries

Port of vercel/ai#14229. Provider tools (e.g. anthropic.tools.webSearch)
were converted to plain function tools in toolsToModelTools, stripping
type, id, and args fields. This caused providers like Anthropic Gateway
to not recognize them as provider-executed tools.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* DCO Remediation Commit for Gregor Martynus <39992+gr2m@users.noreply.github.com>

I, Gregor Martynus <39992+gr2m@users.noreply.github.com>, hereby add my Signed-off-by to this commit: b1930b307d

Signed-off-by: Gregor Martynus <39992+gr2m@users.noreply.github.com>

---------

Signed-off-by: Gregor Martynus <39992+gr2m@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 16:25:04 +00:00
Peter Wielander d8aaf27c79 [core] Enforce single ALS context to protect against duplicate module and caching issues (#1591) 2026-04-03 13:47:19 -07:00
Peter Wielander d119c740d0 [builders] Fix import.meta.url missing when using CJS (#1509) 2026-03-30 12:18:39 -07:00
Pranay Prakash 672d9195a4 Fix step/workflow not found errors to fail gracefully instead of queue retry (#1452)
* feat: enhance error handling for missing workflow functions

Slack-Thread: https://vercel.slack.com/archives/C09G3EQAL84/p1773856370214769?thread_ts=1773856370.214769&cid=C09G3EQAL84
Co-authored-by: Pranay Prakash <1797812+pranaygp@users.noreply.github.com>

* fix: update step not found handling to match FatalError pattern

Move step function validation after step_started and call step_failed directly if not found.

Co-authored-by: Pranay Prakash <1797812+pranaygp@users.noreply.github.com>

* changes

Signed-off-by: Pranay Prakash <pranay.gp@gmail.com>

* feat: add StepNotRegisteredError and WorkflowNotRegisteredError semantic errors

Introduce dedicated error types for when step/workflow functions are not
registered in the current deployment, replacing generic WorkflowRuntimeError.
These are infrastructure errors (not user code errors) with proper error
slugs, docs pages, and a new FUNCTION_NOT_REGISTERED error code.

Step not found fails the step (like FatalError) so the workflow can handle
it gracefully. Workflow not found fails the run.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address PR review comments

- Remove FUNCTION_NOT_REGISTERED error code, use RUNTIME_ERROR instead
- Use .is() instead of instanceof for WorkflowRuntimeError check in runtime.ts
- Remove non-working example from WorkflowNotRegisteredError docs (custom
  errors not serialized yet)
- Update all references from FUNCTION_NOT_REGISTERED to RUNTIME_ERROR

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add e2e tests for step/workflow not registered errors and fix docs typecheck

E2E tests:
- WorkflowNotRegisteredError: start a run with a fake workflowId, verify
  the run fails with RUNTIME_ERROR
- StepNotRegisteredError (caught): workflow catches the step failure,
  verify workflow completes and step is marked failed
- StepNotRegisteredError (uncaught): verify the run fails when workflow
  doesn't catch the error

Step not registered is tested by manually invoking useStep with a
non-existent step ID in the workflow VM — this is the same pattern the
SWC transform generates for real step calls.

Also fix docs typecheck by using declare/\@setup pattern instead of
\@skip-typecheck for code samples.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: cast globalThis to any for Symbol index access in e2e workflow

TypeScript's strict mode doesn't allow using a symbol to index
globalThis. Cast to any since this runs in the workflow VM where
the symbol is defined.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: classify WorkflowNotRegisteredError as RUNTIME_ERROR

The .is() check uses name-based matching, so WorkflowNotRegisteredError
(name='WorkflowNotRegisteredError') doesn't match WorkflowRuntimeError.is().
Add explicit check in classifyRunError so the error code is RUNTIME_ERROR
instead of USER_ERROR.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: use instanceof for WorkflowRuntimeError checks, improve docs

Address PR review feedback:

1. Revert .is() checks back to instanceof WorkflowRuntimeError in
   runtime.ts and classify-error.ts. instanceof catches all subclasses
   (current and future), which is the correct behavior for these catch
   blocks.

2. Remove duplicated try/catch example from step-not-registered-error
   API reference (troubleshooting page already has it).

3. Add Callout in API reference docs clarifying that .is() works in
   server-side Node.js code but not inside "use workflow" functions
   where errors arrive deserialized from the event log.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* changes

Signed-off-by: Pranay Prakash <pranay.gp@gmail.com>

---------

Signed-off-by: Pranay Prakash <pranay.gp@gmail.com>
Co-authored-by: v0 <v0[bot]@users.noreply.github.com>
Co-authored-by: Pranay Prakash <1797812+pranaygp@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 10:04:42 -07:00
Peter Wielander 823f58e5c6 Revert "Add support for calling start() inside workflow functions (#1133)" (#1475)
This reverts commit e889860984.
2026-03-20 17:04:28 -07:00
Pranay Prakash e889860984 Add support for calling start() inside workflow functions (#1133)
* Add support for calling `start()` directly inside workflow functions

Enable `start()` to work in workflow context by routing through an
internal step (`__workflow_start`), reusing existing step infrastructure
with no new event types or server changes needed.

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

* Address PR review feedback

- Use typeof check instead of truthiness for WORKFLOW_START symbol
- Validate start() options in workflow context (reject unsupported options like world)
- Set maxRetries=0 on __workflow_start step to prevent orphaned child runs
- Add unit tests for createStart factory (6 tests)

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

* Make Run serializable in workflow context with step-backed methods

- Add Run serialization via __serializable marker + custom Run reducer/reviver
  in the serialization module (avoids SWC plugin injecting class-serialization imports)
- Create WorkflowRun class factory (packages/core/src/workflow/run.ts) with
  step-backed methods: cancel(), status, returnValue, workflowName, createdAt,
  startedAt, completedAt, exists
- Register 8 built-in steps (__run_cancel, __run_status, etc.) in step-handler
- Update __workflow_start to return full Run object (serialized → WorkflowRun in VM)
- Update createStart to pass through step result directly
- Update docs to reflect full Run support in workflow context

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix start() in workflow VM by delegating from api-workflow stub

The workflow VM loads api-workflow.ts (via the "workflow" export condition)
which stubs all runtime functions. The start stub needs to check for the
injected WORKFLOW_START symbol and delegate to it, otherwise start() throws
"doesn't allow this runtime usage" in the workflow context.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Address PR review: fix stale WORKFLOW_SERIALIZE comments and register Run in host registry

- Update comments in step-handler.ts and start.ts to reference the actual
  serialization mechanism (Run reducer with __serializable marker) instead
  of the stale WORKFLOW_SERIALIZE reference
- Register Run class in the host's class registry from step-handler.ts so
  the Run reviver can deserialize Run/WorkflowRun instances in step context

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add docs for recursive/repeating workflows and deploymentId: "latest"

- Document using start() for self-chaining workflows to avoid large event logs
- Add examples for batch processing and cron-like repeating patterns
- Document deploymentId: "latest" option with type safety warning
- Update skill file with same patterns

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Return full Run object from startFromWorkflow e2e workflow

Update the e2e workflow to return the childRun object directly instead of
just childRun.runId, exercising Run serialization across the workflow boundary.
Update e2e test assertions to match.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add recursive fibonacci e2e test for start() in workflow

Demonstrates recursive workflow composition: fibonacciWorkflow starts
new instances of itself via start() + Promise.all to compute fib(6)=8,
fanning out across independent workflow runs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Move Run method steps to builtins with "use step" directives

Refactor: instead of manually registering Run method steps via
registerStepFunction in step-handler.ts, define them as proper "use step"
functions in builtins.ts with __builtin_ prefix. This leverages the
existing SWC plugin infrastructure — functions starting with "__builtin"
get stable bare-name step IDs.

- Add __builtin_run_{cancel,status,return_value,...} to both builtins files
- Use dynamic import() for getRun inside step bodies to avoid pulling
  Node.js modules into the workflow bundle
- Remove manual registerStepFunction calls from step-handler.ts
- Update WorkflowRun step references to __builtin_run_* names
- Fix step name display in web observability: fall back to raw name
  instead of "?" for built-in steps that don't follow step//module//fn format
- Add fibonacciWorkflow default args for nextjs-turbopack workbench UI

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Render Run objects as clickable links in web observability UI

- Add RunRef type and Run reviver to observabilityRevivers so serialized
  Run objects are hydrated as RunRef instead of showing raw Uint8Array
- Add RunRefInline component (purple badge with run ID) that navigates
  to the target run on click, matching the StreamRef pattern
- Thread onRunClick callback through the component chain:
  WorkflowTraceViewer → EntityDetailPanel → AttributePanel → DataInspector
- Wire up navigation in the web app's run-detail-view
- Add startFromWorkflow default args for workbench UI

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Throw error instead of silent fallback when Run class not in registry

Address PR review: the Run reviver now throws if the class isn't found
in the registry, instead of silently returning a plain { runId } object
that would break the assumption of getting a valid Run instance.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix e2e failures: allow retries on Run getter steps, fix docs code samples

- Remove maxRetries=0 from read-only Run getter steps (status, returnValue,
  workflowName, etc.) — these are safe to retry and need retries when the
  child workflow hasn't completed within the step timeout. Only cancel
  keeps maxRetries=0.
- Fix docs code samples: use correct import path (workflow/api not workflow),
  add declare statements for helper functions used in examples.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Use standard step//module//function naming for built-in steps

Update the SWC plugin's __builtin_ special case to generate proper
step//@workflow/core//{name} IDs instead of bare function names. This
makes parseStepName work correctly for built-in steps, showing:
- StepName: "Run#returnValue" (not "__builtin_run_return_value")
- ModuleSpecifier: "@workflow/core" (not the raw function name)

Convention: __builtin_Run_cancel → step//@workflow/core//Run#cancel
(uppercase prefix + underscore → instance method # notation)

- Move __workflow_start to builtins.ts as __builtin_start
- Rename __builtin_run_* to __builtin_Run_* for proper # notation
- Update WorkflowRun step refs to use full step// IDs
- Remove manual registerStepFunction from step-handler.ts
- Update SWC spec.md with new naming examples

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Remove SWC __builtin special case, use standard step naming for builtins

Remove the SWC plugin's __builtin_ special case so built-in steps get
standard step//{module}@{version}//{fn} IDs like any other step. This
makes parseStepName work correctly, showing proper StepName and
ModuleSpecifier in observability.

The VM reconstructs the same IDs via builtinStepId() which uses the
@workflow/core version to build: step//workflow/internal/builtins@{v}//{fn}

- Remove __builtin special case from SWC plugin (revert to original)
- Add builtinStepId() helper shared by workflow.ts, start.ts, run.ts
- Rename Run steps: __builtin_Run_cancel → Run_cancel, etc.
- Rename start step: __builtin_start → start
- Move start step from manual registerStepFunction to builtins.ts
- Keep __builtin_response_* names unchanged (pre-existing)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Use static class methods for Run steps to get Run.method naming

Refactor Run method steps from standalone functions (Run_cancel) to
static methods on a Run class, so the SWC plugin generates step IDs
with the standard static method convention: Run.cancel, Run.returnValue,
Run.status, etc.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Address PR review: tests, docs warnings, skill fix

- Add TODO on Run.returnValue about polling blocking (replace with system
  hooks once AbortSignal/AbortController PR lands)
- Add docs callout warning about returnValue holding workers alive
- Fix SKILL.md contradiction that said start() can't be used in workflows
- Enhance suspension test to assert step arguments are forwarded
- Add WorkflowRun unit tests: serializable marker, runId, registry, delegation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix response builtins: adopt this-serialization from PR #1413

The rebase onto main didn't fully adopt PR #1413's refactor of response
builtins to use `this` instead of explicit parameters. The old pattern
(resJson(this) wrappers) passed `this` as an argument, but the step
functions now expect `this` to be set via method call context.

Switch to Object.defineProperties on Request/Response prototypes,
matching main's approach. Also document WORKFLOW_PUBLIC_MANIFEST=1
for local e2e testing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Address docs review: returnValue polling is temporary, link to start() API ref

- Update returnValue warning to note this is a temporary implementation
  that will be replaced with internal hooks
- Replace inline deploymentId: "latest" docs with link to the existing
  start() API reference which already covers it comprehensively

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix e2e tests: replace collectedRunIds with trackRun API

PR #1426 replaced the manual collectedRunIds array with a trackRun()
helper. The start() wrapper already auto-tracks, so just remove the
manual push calls and add trackRun for the child run.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Signed-off-by: Pranay Prakash <pranay.gp@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 13:00:17 -07:00
Pranay Prakash afa3931a59 Add stress benchmarks: 1000-step, data payload, and stream tests (#1214) 2026-03-19 16:59:09 -07:00
Pranay Prakash 7d535bd680 test: fix flaky promiseAnyWorkflow e2e test (#1436)
* test: fix flaky promiseAnyWorkflow e2e test

Widen the delay gap between step b (100ms) and step c (6000ms) so that
b reliably wins the Promise.any race regardless of server/network jitter.
Previously the gap was only 1s vs 3s which was insufficient in CI.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix flaky promiseAnyWorkflow e2e test

Widened the step delay gap to stabilize the test.

Signed-off-by: Pranay Prakash <pranay.gp@gmail.com>

---------

Signed-off-by: Pranay Prakash <pranay.gp@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 10:47:46 -07:00
Pranay Prakash 3f2ebb2f90 test: validate sleep() works correctly inside loops (#1415)
* test: add e2e test proving sleep() works correctly inside loops

Validates a user-reported concern that sleep() inside a loop with step
calls fires all iterations instantly. The test confirms sleep is honored
on replay — 3 iterations with 3s sleeps take ~6s+ total elapsed time.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address PR review comments

- Fix misleading "3s per iteration" comment to "3s between iterations"
- Update test comment to reflect 2.5s jitter-tolerant threshold
- Add per-iteration delta assertions to ensure each individual sleep fires

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Apply suggestion from @TooTallNate

Co-authored-by: Nathan Rajlich <n@n8.io>
Signed-off-by: Peter Wielander <mittgfu@gmail.com>

---------

Signed-off-by: Peter Wielander <mittgfu@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Peter Wielander <mittgfu@gmail.com>
Co-authored-by: Nathan Rajlich <n@n8.io>
2026-03-17 12:29:26 -07:00
Peter Wielander 2c80ec7217 DurableAgent: various compatibility fixes (#1385) 2026-03-17 09:47:04 -07:00
Pranay Prakash 74aea7b0af Add DurableAgent compat tests, e2e tests, and migrate to AI SDK v6 (#1362)
* Add DurableAgent compat tests, e2e agent tests, and migrate to AI SDK v6

- Port ToolLoopAgent test suite as DurableAgent compatibility spec (34 tests,
  all expected to fail — each maps to a feature gap to implement)
- Add e2e workflow definitions using mock LLM providers (no API keys needed)
- Add e2e test file for DurableAgent workflows
- Migrate all AI SDK types from V2 to V3 (LanguageModelV2 → V3, etc.)
- Drop AI SDK v5 support: ai peer dep ^5||^6 → ^6, @ai-sdk/provider ^2||^3 → ^3
- Update ai catalog version from 5.0.104 to 6.0.116
- Simplify CompatibleLanguageModel to just LanguageModelV3

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Address PR review feedback

- Remove providerExecuted guard on tool-result stream parts (V3: all
  tool-results are provider-executed by definition)
- Remove providerExecuted spread from tool-output-available UI chunks
- Replace inline MockLanguageModelV3 with import from ai/test (works
  without msw in AI SDK v6)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Remove streamTextIterator mock from compat tests, use it.fails for gaps

Tests now exercise the real DurableAgent code path instead of mocking
the core iterator. 5 tests pass (features DurableAgent already has),
29 are marked it.fails() for known API gaps that will alert when fixed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Implement Tier 1+2 gaps, add @workflow/ai/test mock provider, wire e2e in CI

DurableAgent API additions:
- Add `instructions` (string | SystemModelMessage | SystemModelMessage[])
  as alias for deprecated `system` on constructor
- Add `onStepFinish` and `onFinish` on constructor, merged with stream
  options (constructor first, then stream — matching ToolLoopAgent)
- Add `timeout` on stream options (converted to AbortSignal)
- Add `text`, `finishReason`, `totalUsage` to onFinish event

Test infrastructure:
- Add @workflow/ai/test export with `mockModel()` wrapper that wraps
  MockLanguageModelV3 from ai/test as an async step function
- E2e workflows now use mockModel() + convertArrayToReadableStream
  from @workflow/ai/test instead of inline V2 mock models
- Add e2e-agent.test.ts to test:e2e script so it runs in CI
- Flip 6 compat tests from it.fails → it (now passing)

Score: 11 passing / 23 it.fails (was 5/29)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Remove e2e agent tests — mock models can't serialize across step boundary

The workflow runtime serializes step arguments, and function closures
(like mock model doStream callbacks) aren't serializable. Mock models
only work in unit tests where 'use step' is a no-op. Real e2e agent
tests would need either a mock HTTP server or real provider credentials.

Also removes 'use step' from mockModel wrapper (closures aren't
serializable) and reverts test:e2e script and example workbench dep.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add working e2e agent tests with mock model step factories

Mock model factories use the same 'use step' pattern as real providers
(anthropic, openai). Closure variables are bound to locals at the step
body level so the SWC plugin detects them via __private_getClosureVars.

All 4 e2e tests pass against local dev server:
- agentBasicE2e: text response (11s)
- agentToolCallE2e: single tool call + text (11s)
- agentMultiStepE2e: 3 sequential tool calls (12s)
- agentErrorToolE2e: FatalError recovery (11s)

Also adds e2e-agent.test.ts to test:e2e script for CI.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Use @workflow/ai/test package imports for e2e mock models

Split mock provider into two files to work around SWC constructor
closure bug: mock-create.ts has the model creation logic,
mock.ts has the 'use step' wrappers that capture only serializable
args (strings, plain object arrays).

Exports mockTextModel(text) and mockSequenceModel(responses) —
same 'use step' pattern as real providers (anthropic, openai, etc.).
E2e workflows now import directly from @workflow/ai/test.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Simplify mock provider, add comprehensive e2e tests for all features + gaps

Mock provider:
- Replace mock-create.ts with mock-function-wrapper.ts that simply wraps
  MockLanguageModelV3 constructor in a function (SWC class closure bug)
- mockTextModel/mockSequenceModel use mockProvider() from wrapper file
- Bind closure vars at step body level (_text = text) for SWC detection
- Fix AbortController not available in workflow VM sandbox

E2e tests (13 total, all passing):
- Core: basic text, tool call, multi-step, error recovery (4)
- Callbacks: onStepFinish constructor+stream, onFinish constructor+stream (2)
- Features: instructions, timeout (2)
- GAPs documented: onStart, onStepStart, onToolCallStart,
  onToolCallFinish, prepareCall (5 — complete but callbacks not called)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add tool approval (needsApproval) gap tests, fix SWC closure var binding

Unit tests: 2 new it.fails() tests for tool approval
- needsApproval: true should pause agent (pending tool call, no result)
- needsApproval as function should receive tool input

E2e tests: 1 new test for tool approval gap
- Documents that needsApproval is currently ignored (tool executes anyway)

Also fixes:
- Bind closure vars at step body level in mock provider (_text = text,
  _responses = responses) so SWC plugin detects them
- Guard AbortController usage in workflow VM (not available in sandbox)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add default args for agent e2e workflows in UI definitions

The nextjs-turbopack UI calls workflows with hardcoded default args.
Without these entries, agent workflows were called with no args,
causing prompt=undefined → ModelMessage validation failure.

Also removes debug logging.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add DurableAgent chat UI with tools, update docs for AI SDK v6

Chat UI:
- Tab-based layout with Workflows (existing) and DurableAgent Chat tabs
- Chat powered by DurableAgent + WorkflowChatTransport + ai-elements
- Tools: getWeather (fake data), calculate (math expressions)
- Uses createUIMessageStreamResponse for proper stream serialization
- Reconnect route at /api/chat/[runId]/stream
- ai-elements components: conversation, message, prompt-input, tool
- onStepFinish + onFinish callbacks with console logging

Docs (AI SDK v6 migration):
- system → instructions in DurableAgent constructor examples (10 places)
- LanguageModelV2Prompt → LanguageModelV3Prompt in type references (3 places)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix tool rendering, add reasoning support, model picker, observability links

- Fix tool part rendering: use `input`/`output` props (not `args`/`result`)
  and `tool-{name}` part type (AI SDK v6 format)
- Add reasoning support for Opus 4.5 via providerOptions
- Model picker: Haiku 4.5, Sonnet 4, Opus 4.5 (reasoning), GPT-5.2, GPT-5.3
- Fix observability links: localhost:3456 for local, Vercel dashboard for prod
- Add suggestions above prompt input
- Add MessageParts component handling text, tool, reasoning, step-start
- Add loading spinner for submitted state
- Update docs: system → instructions, LanguageModelV2Prompt → V3

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix tool output rendering: use input/output props on ToolInput/ToolOutput

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix: Documentation for `PrepareStepInfo` and `PrepareStepResult` interfaces references the obsolete `LanguageModelV2` type while the codebase has fully migrated to `LanguageModelV3`.

This commit fixes the issue reported at docs/content/docs/ai/message-queueing.mdx:36

**Bug explanation:**

The codebase migrated from AI SDK V2 to V3. In `packages/ai/src/agent/types.ts`, `CompatibleLanguageModel` is defined as `LanguageModelV3` (from `@ai-sdk/provider`). The actual TypeScript interfaces in `packages/ai/src/agent/durable-agent.ts` use `string | (() => Promise<CompatibleLanguageModel>)` which resolves to `LanguageModelV3`.

However, the documentation in `docs/content/docs/ai/message-queueing.mdx` at lines 36 and 43 still referenced `LanguageModelV2` for the `model` field in both `PrepareStepInfo` and `PrepareStepResult`. This is inconsistent because:
1. The `messages` fields in the same interfaces were correctly updated to `LanguageModelV3Prompt`
2. The actual source code uses `LanguageModelV3` via `CompatibleLanguageModel`
3. There is no `LanguageModelV2` type anywhere in the codebase

This would mislead developers reading the documentation into using the wrong type.

**Fix explanation:**

Changed both `LanguageModelV2` references to `LanguageModelV3` on lines 36 and 43 of the documentation file, matching the actual codebase types. Verified no other stale `LanguageModelV2` references remain in the docs directory.

Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Co-authored-by: pranaygp <pranay.gp@gmail.com>

* Fix instructions tests: flip from it.fails to it, update snapshots

The 3 instructions tests (string, SystemModelMessage, array) now pass.
The snapshots include the assistant reply message from the agent loop,
which is a behavioral difference from ToolLoopAgent (DurableAgent
captures the prompt after the full loop iteration).

Score: 14 passing / 22 it.fails (was 11/25)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix getReadable call: pass startIndex as options object

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix docs type errors and turbopack build

Docs:
- Add await to convertToModelMessages() calls (now async in AI SDK v6)
- Change LanguageModelV3Prompt → ModelMessage[] in type references
- Change LanguageModelV3 → LanguageModel in PrepareStepInfo
- Update docs-globals.d.ts convertToModelMessages return type
- Add LanguageModel to import inference map

DurableAgent:
- Update OutputSpecification to match AI SDK v6 Output interface
  (type→name, parsePartial→parsePartialOutput, parseOutput→parseCompleteOutput,
  responseFormat now PromiseLike)

Turbopack build:
- Remove streamdown plugins from MessageResponse (plugins prop API
  changed in streamdown 2.4.0, causing type mismatch in CI)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix pnpm-workspace.yaml: use double quotes for catalog entries

The stage-workbench-with-tarballs.mjs script only strips double quotes
when parsing catalog keys. Single-quoted @-scoped entries (e.g.,
'@types/node') weren't matched, causing "unresolved catalog dependencies"
errors in CI.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix CI build failures, add changeset

- Remove streamdown plugins from reasoning.tsx (same CI type mismatch)
- Cast ToolHeader type prop and WorkflowChatTransport to fix type errors
- Fix pnpm-workspace.yaml single→double quotes for staging script
- Add minor changeset for @workflow/ai (breaking: AI SDK v6 migration)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Sync webpack workbench with turbopack: add chat UI deps and symlinks

- Symlink app-shell.tsx, chat-client.tsx, agent_chat workflow,
  chat API routes into nextjs-webpack
- Add matching deps: streamdown, @streamdown/*, shiki, cmdk, nanoid,
  motion, @radix-ui/react-use-controllable-state

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix circular symlink: restore chat-client.tsx as real file in turbopack

The previous commit accidentally converted turbopack's chat-client.tsx
into a circular symlink pointing to itself. Webpack's symlink to it
then couldn't resolve, breaking both builds on Vercel.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add missing deps to webpack: use-stick-to-bottom, radix-ui, @vercel/blob

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix model picker: merge body params via prepareSendMessagesRequest

WorkflowChatTransport sends { messages } by default, ignoring the
body option from ChatRequestOptions. Use prepareSendMessagesRequest
to merge { messages, ...body } so the model selection reaches the API.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix WorkflowChatTransport: forward body/headers from ChatRequestOptions

The transport hardcoded body: undefined when calling
prepareSendMessagesRequest, so extra body params (like model selection)
from sendMessage({ body: { model } }) were silently dropped.

Now forwards options.body and options.headers to both
prepareSendMessagesRequest and the default request body.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix model IDs: use real AI Gateway model names

gpt-5.2 and gpt-5.3 don't exist in the AI Gateway.
Replace with gpt-4o and gpt-4o-mini.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Use correct AI Gateway model IDs: Opus 4.5, GPT-5.2, GPT-5.3

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Enable reasoning for all model providers

- Anthropic: thinking.type='enabled' with 10k token budget
- OpenAI: reasoningEffort='high'
- Instructions kept for all models (no longer conditionally removed)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix OpenAI reasoning: use 'medium' effort (GPT-5.3 doesn't support 'high')

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Address all PR review comments

- Change changeset from minor to patch (repo convention)
- Use ?? instead of || for system/instructions fallback
- Clean up timeout: store ID, clearTimeout in finally, { once: true } listeners
- Update class docstring example to use instructions
- Map unrecognized finish reasons to 'other' with validation
- Fix duplicate test, align assertion for unrecognized type
- Support ^ exponentiation in calculate tool
- Remove debug console.log from chat client
- Fix ReactNode/ComponentProps type imports in UI components
- Remove unused MockLanguageModelV3 re-exports from mock.ts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Remove accidentally created empty mock2.ts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Change changeset back to minor for breaking AI SDK v6 migration

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix agent e2e tests: add Vercel world setup for CI

The agent e2e tests only configured the local filesystem world but not
the Vercel world backend. On CI (Vercel prod tests), this caused
VercelOidcTokenError because the world wasn't initialized.

Now matches the setup pattern from e2e.test.ts: configures Vercel world
with OIDC token and project config from CI environment variables.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Deduplicate e2e test utilities: extract shared code to utils.ts

Extract manifest fetching, workflow lookup, world setup, and types
into shared utils.ts. Both e2e.test.ts and e2e-agent.test.ts now
import from the same source, eliminating ~200 lines of duplication.

Shared utilities:
- WorkflowManifest interface
- fetchManifest() with caching
- getWorkflowMetadata() with retry and fallback
- setupWorld() handling local/Vercel/Postgres backends

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix missing deploymentUrl args in e2e.test.ts after utils refactor

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add @workflow/ai dep to all workbenches for agent e2e tests

All workbenches now have @workflow/ai as a dependency and the
100_durable_agent_e2e.ts symlink, so agent e2e tests run everywhere.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix missing imports in e2e.test.ts: add fetchManifest and sleep

The utils refactor removed these imports but they're still used:
- fetchManifest: used in stepFunctionAsStartArgWorkflow test
- sleep (setTimeout): used in webhookWorkflow test

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
2026-03-13 15:20:06 -07:00
Nathan Rajlich 7df13854f8 fix: separate infrastructure vs user code error handling (#1339)
* fix: separate infrastructure vs user code error handling in runtime and step handler

Transient network errors (ECONNRESET, etc.) during infrastructure calls
(event listing, event creation) were caught by a shared try/catch that
also handles user code errors, incorrectly marking runs as run_failed
or steps as step_failed instead of letting the queue redeliver.

- runtime.ts: Move infrastructure calls outside the user-code try/catch
  so errors propagate to the queue handler for automatic retry
- step-handler.ts: Same structural separation — only stepFn.apply() is
  wrapped in the try/catch that produces step_failed/step_retrying
- helpers.ts: Add isTransientNetworkError() and update withServerErrorRetry
  to retry network errors in addition to 5xx responses
- helpers.test.ts: Add tests for network error detection and retry

* add changeset

* remove withServerErrorRetry and isTransientNetworkError

Redundant with undici RetryAgent which already handles 5xx retries
and network error retries at the HTTP dispatcher level.

* address review feedback: move getEncryptionKeyForRun out of user-code try/catch, re-add 5xx/410 safety net in step handler, relax e2e test assertion

* remove serverError5xxRetryWorkflow e2e test

This test validated withServerErrorRetry's in-process retry behavior,
which was removed. Queue-level retry with process-scoped fault injection
is unreliable across serverless instances and too slow for e2e timeouts.

* remove serverError5xxRetryWorkflow and fault injection helpers from e2e workflows

* remove inline comment about deleted test
2026-03-13 00:39:20 +00:00
Nathan Colosimo d72c82220f Fix bug where the SWC compiler bug prunes step-only imports in the client-mode transformation
* first pass

* fix failing test

* Update changeset to fix SWC compiler issue

Fix bug where the SWC compiler bug prunes step-only imports in the client-mode transformation

Signed-off-by: Nathan Rajlich <n@n8.io>

* DCO Remediation Commit for nathancolosimo <nathancolosimo@gmail.com>

I, nathancolosimo <nathancolosimo@gmail.com>, hereby add my Signed-off-by to this commit: 150d175e7d
I, nathancolosimo <nathancolosimo@gmail.com>, hereby add my Signed-off-by to this commit: 429747fcc3

Signed-off-by: nathancolosimo <nathancolosimo@gmail.com>

---------

Signed-off-by: Nathan Rajlich <n@n8.io>
Signed-off-by: nathancolosimo <nathancolosimo@gmail.com>
Co-authored-by: Nathan Rajlich <n@n8.io>
2026-03-11 09:11:19 -07:00
Pranay Prakash c71befe8ec fix(core): premature suspension when hooks have buffered payloads with concurrent pending entities (#1294)
* test: reproduce hook+sleep promiseQueue regression

Add failing tests that reproduce a regression from #1246 where the
sleep's WorkflowSuspension fires before all hook payloads are delivered
when a hook and sleep run concurrently.

Root cause: when the null event fires, the sleep queues a suspension
through promiseQueue. After the first hook payload resolves, subsequent
hook payload resolutions are queued AFTER the sleep suspension, causing
the workflow to terminate prematurely via Promise.race.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: expand tests to isolate bug to hook + pending entity pattern

Add control tests proving sequential steps are NOT affected by the
promiseQueue regression, isolating the bug to hooks specifically:

Failing (hook-based):
- hook + sleep: all 3 payloads → step invocation
- hook + sleep: 2 payloads → return
- hook + incomplete step: 2 payloads → return

Passing (step-based controls):
- sleep + sequential steps: both step events exist
- sleep + sequential steps: only 1st step completed
- incomplete step + sequential steps: all step events exist
- hook only (no concurrent entity): payloads + step

The bug is: any entity that queues a suspension through promiseQueue
at null-event time preempts hook payload delivery, because hooks
buffer payloads in payloadsQueue and only resolve them one-at-a-time
as the workflow code iterates. Steps are unaffected because each step
has its own events consumed before null fires.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(core): move WorkflowSuspension calls back to setTimeout(0) from promiseQueue

The promiseQueue refactor (#1246) moved ALL promise resolutions through
the queue, including WorkflowSuspension calls. This caused premature
termination when hooks had buffered payloads and a concurrent entity
(sleep or incomplete step) was pending:

1. Null event fires → all subscribers run
2. Sleep/step queues WorkflowSuspension via promiseQueue.then()
3. Hook's next payload resolution is also queued via promiseQueue.then()
4. Suspension fires first → Promise.race terminates workflow
5. Hook payload never delivered → infinite retry loop

Fix: move WorkflowSuspension calls back to setTimeout(0) (macrotask).
Suspensions fire AFTER all microtask-based deliveries (promiseQueue
resolve/reject for step results and hook payloads) have completed.
Non-suspension resolve/reject calls remain on promiseQueue for
deterministic ordering of data delivery.

Affected paths:
- step.ts: null event handler
- sleep.ts: null event handler
- hook.ts: null event handler, eventLogEmpty suspension, dispose suspension

* fix(core): use pendingDeliveries counter + scheduleWhenIdle for suspensions

Replace all nested setTimeout/promiseQueue suspension patterns with a
clean idle-polling mechanism:

1. pendingDeliveries counter: incremented before async hydration (step
   results, hook payloads), decremented in finally block after delivery
2. scheduleWhenIdle(ctx, fn): polls via setTimeout(0) → check counter →
   if > 0, wait for promiseQueue.then() → repeat. Only fires fn when
   pendingDeliveries reaches 0.

This correctly handles:
- Sync deserialization (no encryption): counter is 0, fires immediately
  after first setTimeout(0)
- Async deserialization (with encryption): waits for decryption to
  complete before firing
- Multi-round hook payload delivery: each createHookPromise() call
  increments the counter, preventing premature suspension between
  delivery rounds

Also adds payloadsQueue.length check to hook null handler — don't
trigger suspension if buffered payloads can satisfy pending awaits.

Tests now run in both sync and async modes (14 total = 7 scenarios x 2).
All 164 tests pass.

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Nathan Rajlich <n@n8.io>
2026-03-07 11:02:08 -08:00
Nathan Rajlich 36a901d2d2 feat: expose workflow and step names in metadata functions (#1285)
* feat: expose workflow and step names in metadata functions

Add `workflowName` to `WorkflowMetadata` and `stepName` to `StepMetadata`,
making them available via `getWorkflowMetadata()` and `getStepMetadata()`.
This is useful for adding context to structured logs, collating logs during
investigations, and bucketing errors by workflow/step name.

Closes #1103

* add changeset

* Apply suggestion from @TooTallNate

Signed-off-by: Nathan Rajlich <n@n8.io>

---------

Signed-off-by: Nathan Rajlich <n@n8.io>
2026-03-06 21:51:16 +00:00
Pranay Prakash 30e24d441e Merge commit from fork
* Prevent deterministic tokens in createWebhook

Remove the `token` option from `WebhookOptions` to prevent unauthorized
access to the public webhook endpoint. Webhook tokens are now always
randomly generated. Deterministic tokens remain available for
`createHook()` with server-side `resumeHook()`.

**BREAKING CHANGE**: `createWebhook()` no longer accepts a `token` option.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Address PR feedback: throw on token, revert unnecessary doc changes, replace examples

- createWebhook() now throws an error if token is passed at runtime
  instead of silently stripping it
- Revert unnecessary changes to create-hook.mdx (only keep
  slack_webhook -> slack_messages prefix change)
- Replace Slack webhook examples in create-webhook.mdx and hooks.mdx
  with generic event collector examples that make sense without
  deterministic tokens

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Address andresriancho's feedback: stricter token check, add unit test

- Use `token !== undefined` instead of `if (token)` for correctness
  (empty string should also be rejected)
- Add unit test verifying createWebhook({ token: "anything" }) throws
  the expected error message

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* change changeset to minor

* Fix docs typechecker to actually resolve workflow module types

The docs typechecker was silently failing to resolve `workflow` imports
because:
1. repoRoot was computed 4 levels up instead of 3
2. No path mappings for workspace packages
3. Missing TS lib files for disposable and DOM iterables

This meant all imported symbols became `any` and type errors in docs
code samples went undetected.

Fixes:
- Fix repoRoot: '../../../..' -> '../../..'
- Add path mappings for workflow, @workflow/core, @workflow/ai, etc.
- Add lib.dom.iterable.d.ts and lib.esnext.disposable.d.ts
- Fix regex that ate first line of some code blocks
- Auto-skip error demonstration code blocks
- Add respondWith to global Request for webhook doc patterns

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Revert "Fix docs typechecker to actually resolve workflow module types"

This reverts commit 02d42ac866e6db1b6d016339739344cdd9538e90.

* chore: trigger CI

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-05 10:33:53 -08:00
JJ Kasper a9fea9132e Update workbench tests to build and run outside of monorepo (#1230)
* Setup fixes

* ci: run local e2e against staged tarball workbenches

* ci: update staged workbench tarball setup script

* chore: set nextjs workbenches back to next 16.1.6

* update lock

* test(e2e): resolve workbench path from WORKBENCH_APP_PATH

* fix: address deferred builder issues outside monorepo

* ci: stage tarball workbenches only for nextjs local e2e

* fix(next): discover deferred steps imported via workflows

* test(core): gate deferred step-discovery dev test to canary

* test(e2e): cover cross-file imported step in build/start lanes

* fix(e2e): use local manifest in local runs and relax dev rebuild timeout

* fix(workbench): add imported-step workflow symlink for sveltekit/astro

* test(e2e): scope imported-step workflow test to nextjs lanes

* fix(next): rebuild deferred entries on discovered file updates

* fix(next): watch transitive deferred step deps for dev rebuilds

* fix(next): restore socket-driven deferred step rebuilds

* add changeset

* chore: address review feedback on deferred e2e updates

* fix(cli): guard stream flush against closed write streams
2026-03-03 11:17:39 -08:00
Pranay Prakash 02681dce4a feat(core): add hook.dispose() method to release hook tokens early (#1181)
* feat(core): add hook.dispose() method to release hook tokens early

Add a `dispose()` method to the Hook interface that allows workflows to
explicitly release hook tokens for reuse by other workflows while the
current workflow is still running. This enables handoff patterns where
one workflow can transfer a hook token to another workflow.

- Add `dispose()` method to Hook interface in create-hook.ts
- Implement dispose functionality in workflow/hook.ts
- Add HookDisposedInvocationQueueItem to global.ts
- Handle hook_disposed events in suspension-handler.ts
- Update documentation in hooks.mdx and create-hook.mdx
- Add e2e test for hook token reuse after explicit disposal

https://claude.ai/code/session_01AkvrXduyFTbtV2joTPHrDH

* feat(core): implement TypeScript Disposable spec for hooks

Add Symbol.dispose to Hook interface to support the TC39 Explicit Resource
Management proposal. This allows hooks to be used with the `using` keyword
for automatic disposal when exiting scope.

https://claude.ai/code/session_01AkvrXduyFTbtV2joTPHrDH

* docs: prefer `using` keyword for hooks and webhooks

Update all documentation and e2e tests to use the `using` keyword as the
recommended approach for creating hooks and webhooks. This leverages the
TC39 Explicit Resource Management proposal for automatic disposal.

- Update e2e tests to use `using` syntax
- Update foundational hooks guide to recommend `using`
- Update create-hook API reference with `using` examples
- Update create-webhook API reference with `using` examples
- Update example workflow to use `using`

https://claude.ai/code/session_01AkvrXduyFTbtV2joTPHrDH

* chore: simplify `using` examples in docs and e2e tests

Remove unnecessary block scopes and excessive comments about automatic
disposal. Block scopes are only used when early disposal is relevant
(like in the handoff test).

https://claude.ai/code/session_01AkvrXduyFTbtV2joTPHrDH

* docs: add brief explanation of `using` in first examples

Add a one-liner explaining the `using` keyword in the intro examples
of the API reference docs, so new users understand the syntax.

https://claude.ai/code/session_01AkvrXduyFTbtV2joTPHrDH

* fix: address PR review comments

- Restore code highlights (`[!code highlight]`) that were unintentionally
  removed from pre-existing doc examples
- Move `using` explanation from prose to inline code comment in intro
  examples
- Add `{/* @skip-typecheck */}` to incomplete manual dispose() snippet
- Add 409 (conflict/duplicate) error handling for hook_disposed events
  in suspension handler to handle workflow re-invocation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: polyfill Symbol.dispose in workflow VM context

The workflow VM sandbox doesn't have Symbol.dispose/Symbol.asyncDispose
available, causing `using` keyword to fail with "Symbol.dispose is not
defined" at runtime. Add polyfill in the VM context creation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: use VM's Symbol.dispose for hook disposable support

The workflow VM sandbox has its own Symbol object with a polyfilled
Symbol.dispose. The hook object was using the host's Symbol.dispose,
which is a different symbol instance. The SWC-compiled `using` keyword
looks up the VM's Symbol.dispose on the object, causing "Object not
disposable" errors.

Fix by setting Symbol.dispose on the hook object dynamically using the
VM's globalThis.Symbol.dispose from the orchestrator context.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: revert webhookWorkflow to create all webhooks upfront

The webhookWorkflow e2e test creates 3 webhooks that must all exist
before the test sends HTTP requests. Using `using` with sequential
creation meant only the first webhook existed at the first suspension
point. Revert to `const` since all webhooks need to be created upfront.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: eliminate hook_disposed queue item, use flag pattern for disposal

Instead of adding a separate HookDisposedInvocationQueueItem to the
queue on dispose, keep the HookInvocationQueueItem throughout the
hook lifecycle and track state with flags (hasCreatedEvent, disposed).
A closure variable (hasDisposedEvent) makes disposeHook() a pure no-op
on replay, avoiding redundant server calls and 409 errors.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: delete hook from invocations queue on hook_disposed terminal event

Match the pattern used by steps (step_completed/step_failed) and waits
(wait_completed) where the queue item is removed on the terminal event.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: verify dispose() is safe when called twice after replay

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: add comprehensive edge case tests for hook disposal

- WorkflowSuspension counts disposed hooks separately from active hooks
- Dispose after hook_created replay produces correct suspension
- Dispose before first suspension (needs both create + dispose)
- Multiple hooks where only one is disposed
- Dispose on a conflicted hook is safe (no crash)
- Symbol.dispose calls disposeHook correctly (using keyword pattern)
- Iterator break without dispose keeps hook alive in queue
- Await after dispose on first invocation triggers suspension

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: remove unsafe cast by adding Symbol.dispose to hook object literal

Add [Symbol.dispose] directly to the hook object so it satisfies the
Hook<T> type without `as unknown as`. The VM's Symbol.dispose is still
added separately when it differs from the host's.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address remaining Copilot review comments

- Rename test to match new behavior (hooks stay in queue, not removed)
- Use neutral "processed" verb in WorkflowSuspension message when
  mixed item types are present
- Remove extends Disposable from Hook interface to avoid requiring
  lib.esnext.disposable in downstream consumers (explicit
  [Symbol.dispose]() method is still declared on the interface)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: drain pending promises in disposeHook to prevent orphaned awaits

When dispose() is called while a promise is pending (e.g., iterator
suspended on yield await this, or direct await hook after dispose),
the promise would hang forever since the event consumer will never
deliver another hook_received. Now disposeHook() clears the promises
array and triggers a WorkflowSuspension so the runtime processes the
disposal cleanly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: remove workflow from changeset, keep only @workflow/core

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-27 12:59:54 -08:00
Nathan Rajlich 54879835f3 Fix pages router default args: use empty array instead of [42] (#1081) 2026-02-17 01:47:48 -08:00
Pranay Prakash c2b4fe9906 fix(core): detect and fatal error on orphaned/invalid events (#1055)
* fix(core): detect and fatal error on orphaned/invalid events in EventsConsumer

When an event log has duplicate or invalid events (e.g., 2 wait_completed for a single wait_created), the EventsConsumer gets stuck: the orphaned event has no callback to consume it, so eventIndex never advances, blocking all subsequent events and hanging the workflow forever. This adds deferred orphaned event detection that raises a WorkflowRuntimeError instead.

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

* add corrupted-event-log error slug and docs page

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

* add changeset for orphaned event detection

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

* address PR feedback on docs and JSDoc

- Add JSDoc to EventsConsumer constructor's onUnconsumedEvent parameter
- Rewrite docs to clarify this is a SDK/server bug, not user code
- Remove cancel/programmatic retry advice; run is already marked failed
- Emphasize error is uncatchable inside workflow code
- Add steps: upgrade package, retry via dashboard/CLI, report issue on GitHub

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

* address PR review: structured options, required callback, reorder docs

- Change EventsConsumer constructor to use structured EventsConsumerOptions object
- Make onUnconsumedEvent required (all callsites provide it)
- Move corrupted-event-log card to last position in errors index
- Update all callsites in tests

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

* fix docs code sample to pass type-checking

Use correct imports (getRun from workflow/api) and declare variables
to satisfy the docs-typecheck CI job.

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

* add e2e test for parallel sleep (Promise.all with 10x sleep)

Adds parallelSleepWorkflow that does Promise.all with 10 concurrent
sleep('1s') calls, and an e2e test asserting it completes or fails
within a reasonable time (not hanging).

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

* fix parallelSleep e2e test: await returnValue instead of checking status

run.status returns immediately and the workflow is still 'running'.
Use run.returnValue which polls until completion.

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

* fix parallelSleep e2e: assert parallel completion under 10s

The test verifies 10 concurrent sleep('1s') calls complete in parallel
(~1s) rather than serially (10s) or hanging indefinitely. 30s timeout
is sufficient since the workflow should finish in ~1-2s.

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 23:31:39 -08:00
JJ Kasper f5ae600db4 fix(e2e): stabilize 5xx step_completed fault injection (#1046) 2026-02-13 14:03:06 -08:00
Pranay Prakash 8d117cd219 Retry 5xx errors from workflow-server in step handler (#1011)
* Retry 5xx errors from workflow-server in step handler

Add `withServerErrorRetry` helper that retries world calls on 5xx errors
with exponential backoff (500ms, 1s, 2s ≈ 3.5s total). Applied to all
`world.events.create` calls in the step handler so transient
workflow-server errors don't consume step attempts.

If retries are exhausted, the error is thrown to the queue for
higher-level retry without burning a step attempt.

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

* Address PR review comments on 5xx retry handling

- Fix misleading `maxAttempts` log field to `maxRetries` in withServerErrorRetry
- Update step-handler comment to accurately note that queue retries may
  still consume step attempts since step_started has already incremented
- Add unit tests for withServerErrorRetry (7 tests covering success,
  retry/backoff, exhaustion, and non-5xx passthrough)

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

* Add tests for 429/5xx retry handling

Unit tests for withThrottleRetry and withServerErrorRetry helpers, plus
an e2e test that exercises the 5xx retry codepath during step execution
via run-scoped fault injection.

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

* Wrap VQS errors in WorkflowAPIError and add retry to queueMessage

VQS throws its own error types (InternalServerError, ConsumerDiscoveryError,
ConsumerRegistryNotConfiguredError) that don't match WorkflowAPIError.is().
Wrapping them at the world-vercel boundary enables withServerErrorRetry in
queueMessage() to automatically retry transient queue failures.

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

* Update changeset to include @workflow/world-vercel

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

* Remove queue retry logic per review feedback

Queue retrying will be handled natively by the @vercel/queue client
instead. Reverts VQS error wrapping and withServerErrorRetry in
queueMessage().

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 18:42:36 -08:00
Nathan Rajlich fcfaf8bbaa Support step function serialization in client mode (#924) 2026-02-07 15:34:29 -08:00
Nathan Rajlich 5b5b36a03b [e2e] Remove /api/hook and /api/test-health-check endpoints, call APIs directly
- Remove debug logging from bench.bench.ts
- Remove awaitReturnValue() wrapper, use run.returnValue directly in benchmarks
- Add changeset for Nitro builder manifest fix
- Refactor hookWorkflow tests to call getHookByToken()/resumeHook() directly
  (pass hook object to resumeHook to avoid duplicate lookups)
- Refactor queue-based health check test to call healthCheck() directly
- Assert specific error message for invalid hook token test
- Remove /api/hook and /api/test-health-check endpoints from all workbench apps
  (nextjs-turbopack, nextjs-webpack, vite, hono, express, fastify, sveltekit,
  astro, nuxt, nitro-v2, nitro-v3, nest, example)
2026-02-07 09:00:19 -08:00
JJ Kasper 82c209c669 Add missing env for tests on deploy (#973) 2026-02-06 16:57:11 -08:00
Nathan Rajlich 86f62f2779 Refactor e2e tests to no longer use "trigger" endpoint (#958)
## Summary

Refactors the E2E tests to call `start()` from `workflow/api` directly instead of going through the `/api/trigger` HTTP endpoint in each workbench app. This removes a layer of indirection — the tests now use the same API that users would use to start workflows programmatically.

### Before

```ts
const run = await triggerWorkflow('addTenWorkflow', [123]);
const returnValue = await getWorkflowReturnValue(run.runId);
```

- `triggerWorkflow()` sent an HTTP POST to `/api/trigger` on the workbench app
- The workbench app looked up the workflow function, called `start()`, and returned the run ID
- `getWorkflowReturnValue()` polled `GET /api/trigger?runId=...` until the workflow completed

### After

```ts
const run = await start(await e2e('addTenWorkflow'), [123]);
const returnValue = await run.returnValue;
```

- `e2e()` / `getWorkflowMetadata()` fetches the manifest from `/.well-known/workflow/v1/manifest.json` to look up the correct `workflowId`
- `start()` is called directly from the test process via the configured World
- `run.returnValue` polls for completion via the World (no HTTP polling endpoint needed)

### Changes

**`packages/core/e2e/e2e.test.ts`**
- Removed `triggerWorkflow()` and `getWorkflowReturnValue()` helpers
- Added `fetchManifest()` to fetch and cache the workflow manifest from the deployment
- Added `getWorkflowMetadata(file, fn)` to look up `{ workflowId }` from the manifest
- Added `e2e(fn)` shorthand for the common case of `workflows/99_e2e.ts`
- All tests call `start()` and `run.returnValue` directly
- Error tests use `.catch()` to inspect `WorkflowRunFailedError`
- Output stream tests use `run.getReadable()` directly (skipped on local world where cross-process streaming isn't supported)
- `beforeAll` configures the local World with the correct data directory and base URL
- Pages Router tests use `startWorkflowViaHttp()` to specifically validate the HTTP trigger path

**Workbench apps (hono, express, fastify, nest)**
- Removed `/api/trigger` route handlers
- Kept `/api/hook`, `/api/test-direct-step-call`, `/api/test-health-check` endpoints
- Re-added `_workflows.js` side-effect import for hono/express/fastify to maintain Nitro's HMR dependency graph

**Deleted trigger-only route files** from: nextjs-turbopack, nextjs-webpack, vite, sveltekit, astro, nuxt, nitro-v2, nitro-v3, example

**`.github/workflows/tests.yml`**
- Added `WORKFLOW_PUBLIC_MANIFEST: '1'` to all E2E test jobs

### Dependencies

Stacked on #963 which adds `WORKFLOW_PUBLIC_MANIFEST` support to all framework builders.
2026-02-06 16:25:47 -08:00
Nathan Rajlich 661724c01e Expose workflow manifest via HTTP when WORKFLOW_PUBLIC_MANIFEST=1 (#963)
## Summary

When `WORKFLOW_PUBLIC_MANIFEST=1` is set, each framework builder exposes the workflow manifest at `/.well-known/workflow/v1/manifest.json` via the most appropriate mechanism for the framework:

- **Next.js**: Copies manifest to `public/.well-known/workflow/v1/manifest.json` (served as a static file)
- **SvelteKit**: Copies manifest to `static/.well-known/workflow/v1/manifest.json` (served as a static file)
- **Vercel Build Output API** (example workbench): Copies manifest to `.vercel/output/static/.well-known/workflow/v1/manifest.json` (served as a static file)
- **Nitro** (vite, hono, express, fastify, nuxt, nitro-v2, nitro-v3): Registers a virtual handler that reads and serves the manifest JSON
- **Astro**: Generates a `manifest.json.js` page route that returns the manifest JSON
- **NestJS**: Adds a `@Get('manifest.json')` endpoint on the `WorkflowController` (gated by the env var at runtime)

### Other changes

- `BaseBuilder.createManifest()` now returns the manifest JSON string so framework builders can use it
- Added `shouldExposePublicManifest` getter to `BaseBuilder`
- Fixed Astro `LocalBuilder` which was missing the `createManifest()` call that all other framework builders have
- Removed unused `public/` directory and static file copy from the example workbench build script
- Added `WORKFLOW_PUBLIC_MANIFEST` to `turbo.json` build task env so Vercel builds can access it
- Added generated manifest paths to `.gitignore`
2026-02-06 12:22:37 -08:00
Nathan Rajlich 2453b29426 Make wf build --manifest-file include steps / classes metadata (#931) 2026-02-04 09:22:11 -08:00
Pranay Prakash 6208616d06 Add sequential step benchmarks and fix broken benchmark infrastructure (#845)
* Add 50, 100, 500 concurrent step benchmarks

Enable Promise.all and Promise.race benchmarks for 50, 100, and 500
concurrent steps (previously 100+ were skipped).

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

* Add 50, 100, 500 sequential step benchmarks

Extends the sequential step benchmarks to test workflows with 50, 100,
and 500 sequential steps in addition to the existing 10 step test.

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

* Add full/quick benchmark suite toggle for CI

- Add BENCHMARK_FULL_SUITE env var to control which benchmarks run
- Quick suite (default for PRs): 10, 25, 50 step benchmarks
- Full suite (main branch, manual dispatch): adds 100, 500 step benchmarks
- Add workflow_dispatch input to manually trigger full suite from GitHub UI
- Skip 100+ sequential and concurrent step benchmarks by default

This keeps PR benchmarks fast while allowing full stress testing on demand.

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

* Add 25 sequential steps benchmark

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

* Fix benchmark API calls to use binary format

PR #853 changed the workflow trigger API to expect binary data
(application/octet-stream) instead of JSON. The e2e tests were updated
but the benchmark file was missed, causing all benchmarks to fail
silently since Jan 28.

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

* Only run full benchmark suite on manual dispatch

Remove automatic full suite on main branch pushes - only run full suite
when manually triggered with full_suite=true.

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

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 18:02:36 -08:00
Nathan Rajlich c1d7c8dbb4 Add support for "use step" functions in class instance methods (#777)
Added support for `"use step"` directive in class instance methods, allowing instance methods to be used as workflow steps.

### What changed?

- Modified the SWC plugin to recognize and transform instance methods with the "use step" directive
- Added registration logic for instance method steps using `ClassName.prototype.methodName`
- Implemented proper serialization of class instances to preserve the `this` context across workflow/step boundaries
- Added comprehensive end-to-end tests for instance method steps
- Updated error handling to allow "use step" in instance methods while still preventing "use workflow" in instance methods

### How to test?

The PR includes a new end-to-end test `instanceMethodStepWorkflow` that demonstrates the functionality:

1. Run the e2e tests to verify the new instance method step functionality
2. The test creates a `Counter` class with instance methods marked as steps
3. It verifies that the instance methods can be called as steps with proper serialization of the `this` context
4. It also verifies that multiple instances of the same class can be used independently

### Why make this change?

This change enables a more natural object-oriented programming model when working with workflows. Previously, only static methods, standalone functions, and object methods could be marked as steps. Now, developers can create classes with instance methods that are steps, allowing for better encapsulation and more intuitive code organization. This is particularly useful for complex workflows that need to maintain state across multiple step invocations.
2026-02-03 00:11:38 -08:00
Nathan Rajlich b5296a7a32 Add discovered serializable classes in all context modes (#874)
This PR ensures that all classes with custom serialization are automatically included in all bundle contexts (step, workflow, client) to ensure proper serialization/deserialization when crossing execution boundaries:

- Classes defined in any context can now be properly serialized when passing data between:
  - Client → Workflow (when starting workflows)
  - Workflow → Step (when calling steps)
  - Step → Workflow (when returning step results)
  - Workflow → Client (when returning workflow results)

- The build system now automatically discovers all files containing serializable classes and includes them in each bundle, regardless of where the class is originally defined.

- No manual configuration is required - cross-registration happens automatically during the build process.
2026-02-02 23:26:29 -08:00
Nathan Rajlich 1060f9d04a Change user input/output to be binary data at the World interface (#853) 2026-01-28 10:36:28 -08:00
Nathan Rajlich f2ab6ee179 Fix Nest workbench app build (#865)
* Use proper OpenAI import

```
Error: ../example/workflows/5_hooks.ts(36,22): error TS2351: This expression is not constructable.
  Type 'typeof import("/vercel/path0/node_modules/.pnpm/openai@6.1.0_ws@8.18.3_zod@4.1.11/node_modules/openai/index")' has no construct signatures.
```

* .
2026-01-27 00:21:33 -08:00
Pranay Prakash 4966b728a8 implement event-sourced architecture (#621)
* perf: implement event-sourced architecture

* Apply suggestions from code review

* Improve invalid event log handling in step/hook/wait

* Handle serialized workflow run errors correctly

* log error in failing test

* Handle queue idempotency in vercel world

* hotfix for error propogation

* Fix: Incorrect HTTP status code 409 should be 410 for terminal run state rejections in postgres storage

* Fix: The code attempts to pass an unsupported `fatal` property when creating a `step_failed` event. The TypeScript schema for `step_failed` events only allows `error` and `stack` properties, so the `fatal` property causes a compilation error.

This commit fixes the issue reported at packages/core/src/runtime/step-handler.ts:133-139

## TypeScript error: Invalid property 'fatal' in step_failed event

**What fails:** TypeScript compilation fails in `packages/core` due to an invalid property in the `step_failed` event creation.

**How to reproduce:**
```bash
cd /vercel/sandbox/primary
pnpm run -F @workflow/core build
```

**Result:**
```
src/runtime/step-handler.ts(133,32): error TS2769: No overload matches this call.
  Overload 1 of 2, '(runId: null, data: { eventType: "run_created"; ... }, gave the following error.
    Argument of type 'string' is not assignable to parameter of type 'null'.
  Overload 2 of 2, '(runId: string, data: CreateEventRequest, params?: CreateEventParams | undefined): Promise<EventResult>', gave the following error.
    Object literal may only specify known properties, and 'fatal' does not exist in type '{ error: any; stack?: string | undefined; }'.
```

**Issue:** The code attempted to pass a `fatal: true` property in the `eventData` object when creating a `step_failed` event. However, the event schema defined in `packages/world/src/events.ts` for `StepFailedEventSchema` only allows `error` and `stack` properties - the `fatal` property is not part of the schema.

Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>

* Fix: Code silently skips updating workflowRun if result.run is undefined, causing workflows to be incorrectly skipped instead of throwing an error

* Add hook_conflict event type for duplicate token detection

Implements hook_conflict events across all world implementations to handle
cases where a workflow attempts to use a hook token already claimed by another
workflow. Instead of throwing errors, the system now records hook_conflict
events in the event log, enabling deterministic replay.

- Add HookConflictEvent schema to @workflow/world
- Implement hook_conflict in world-local, world-postgres, and suspension-handler
- Update hook consumer to reject promises with WorkflowRuntimeError on conflict
- Add HOOK_CONFLICT error slug with documentation
- Add e2e and unit tests for conflict scenarios

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

* Add changeset for hook_conflict events

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

* Add unit tests for hook_conflict handling

- Add tests for hook_conflict event in workflow.test.ts
- Fix world-postgres test to not expect removed message field

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

* Improve hook-conflict.mdx error guide

- Remove redundant third point in 'Why This Happens' section
- Add example showing how to handle WorkflowRuntimeError for hook conflicts

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

* Fix docs validation: add hook-conflict to errors index

- Fix broken link in hook-conflict.mdx (/docs/foundations/webhooks -> /docs/api-reference/workflow/create-webhook)
- Add hook-conflict to errors index page so it's discoverable by the docs link validator

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

* Fix world-local tests for hook_conflict event behavior

Update tests to expect hook_conflict events instead of thrown errors when
duplicate hook tokens are used. This aligns with the new event-sourced
approach where conflicts are recorded as events rather than thrown.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

* Add specVersion property to World interface for backwards compatibility

- Add specVersion property to World interface to track world package version
- Add specVersion to WorkflowRun schema and run_created event data
- World implementations (vercel, local, postgres) set specVersion from npm version
- Server can use specVersion to route operations based on world version
- Add specVersion display to observability UI attribute panel
- Add spec_version column to postgres runs schema

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

* Add migration for spec_version column in postgres schema

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

* Add drizzle migration journal and snapshot for spec_version column

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

* Regenerate postgres migration using drizzle-kit

- Properly generates migration with drizzle-kit CLI
- Removes deprecated 'paused' status from enum
- Adds spec_version column

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

* Add backwards compatibility for event-sourced runs

- Add RunNotSupportedError for runs requiring newer world versions
- Add semver-based version utilities (isLegacyVersion) to @workflow/world
- World implementations check specVersion and route to legacy handlers
- Legacy runs (< 4.1.0): run_cancelled skips event storage, wait_completed stores event only
- New runs always get current world version (4.1.0-beta.0)
- Make EventResult.event optional for legacy compatibility

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

* Refactor spec version from semver strings to integers

Replace semver-based version compatibility with explicit integer spec versions:
- SPEC_VERSION_LEGACY (1): pre-event-sourcing runs
- SPEC_VERSION_CURRENT (2): event-sourced architecture

Use branded SpecVersion type to enforce importing from @workflow/world.
Remove semver dependency from world, world-local, and world-postgres.

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

* Store error as CBOR in postgres world for consistency with input/output

- Add error_cbor bytea columns to workflow_runs and workflow_steps tables
- Deprecate text error column, rename to errorJson with fallback parsing
- Remove JSON.stringify from error writes (run_failed, step_failed, step_retrying)
- Add parseErrorJson helper for backwards compatibility with legacy data

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

* Use WorkflowRuntimeError and improve run entity handling in core runtime

- Replace generic Error with WorkflowRuntimeError for runtime assertions
- Add explicit check for run entity in run_created response
- Use run.runId instead of event.runId for consistency
- Use actual run status instead of hardcoded 'pending' in attributes

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

* Add specVersion to Step, Hook, and Event entities

- Add specVersion field to Step, Hook, and Event interfaces in @workflow/world
- Add spec_version column to steps, hooks, events tables in postgres schema
- Set specVersion to SPEC_VERSION_CURRENT when creating entities in all worlds
- Update migration to include spec_version columns for all entity tables

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

* Refactor world-local storage into modular files

Split storage.ts (1041 lines) into smaller, focused modules:
- storage/filters.ts: Data filtering helpers
- storage/helpers.ts: ULID and date utilities
- storage/hooks-storage.ts: Hook CRUD operations
- storage/legacy.ts: Legacy event handling
- storage/runs-storage.ts: Run get/list operations
- storage/steps-storage.ts: Step get/list operations
- storage/events-storage.ts: Event create/list operations
- storage/index.ts: Main composition

Also extracted test helpers to test-helpers.ts for reusability.

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

* Remove genversion and World.specVersion property

The World.specVersion string property was never actually read - only
the numeric SPEC_VERSION_CURRENT is used for backwards compatibility.

- Remove genversion dependency and generated version.ts from @workflow/world
- Remove specVersion property from World interface and all implementations
- Minor fix: correct error message to reference 'workflow' package
- Minor fix: correct error source priority in world-vercel events.ts
- Minor fix: update comment in runtime.ts

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

* Remove genversion from world-local, world-postgres, and world-vercel

These packages no longer need genversion since we removed the
World.specVersion property in the previous commit.

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

* Remove version.ts from .gitignore files

No longer needed after removing genversion.

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

* Add legacy/backwards compatibility tests

Tests for world-local and world-postgres covering:
- Legacy runs (specVersion < 2 or null/undefined)
  - run_cancelled handling (updates run, no event stored)
  - wait_completed handling (stores event only)
  - Rejection of unsupported events
  - Hook cleanup on cancellation
- Future runs (specVersion > current)
  - Rejection with RunNotSupportedError
- Current version runs (normal processing)
- Legacy error parsing (errorJson field parsing)

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

* Add hook_received support for legacy runs

When resumeHook() is called on a legacy run (specVersion < 2), the
hook_received event was previously rejected. This adds support for
storing hook_received events on legacy runs without entity mutation,
matching the behavior of wait_completed.

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

* Fix missing genversion in world-vercel

Signed-off-by: Peter Wielander <mittgfu@gmail.com>

* Remove deprecated workflow_completed, workflow_failed, and workflow_started events

Replace with run_completed, run_failed, and run_started equivalents.

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

* Add specVersion to EventWithRefsSchema in world-vercel

The manually-created EventWithRefsSchema was missing the specVersion field,
which caused specVersion to be stripped when using lazy (refs) mode for events.

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

* Wire specVersion from client through world backends

- Core runtime now sends specVersion in run_created eventData
- world-local accepts specVersion from eventData (defaults to current)
- world-postgres accepts specVersion from eventData (defaults to current)

This matches workflow-server behavior where v2 endpoints accept
specVersion from the client, while v1 endpoints default to legacy.

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

* Move specVersion to event object level, propagate to entities

- Add specVersion to BaseEventSchema (event level, not eventData)
- Remove specVersion from RunCreatedEventSchema.eventData
- Core runtime sends specVersion on event object
- world-local reads specVersion from event, propagates to run/step/hook entities
- world-postgres reads specVersion from event, propagates to run/step/hook entities

This ensures specVersion flows from client through event to all created entities.

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

* Update specVersion to be optional in types for backwards compatibility

- specVersion is optional in all entity schemas (runs, steps, hooks, events)
  for backwards compatibility with legacy data in storage
- Runtime always sends specVersion on event requests
- world-local and world-postgres provide fallback to SPEC_VERSION_CURRENT
- Test helpers include specVersion in all event creation calls
- EventWithRefsSchema in world-vercel defaults specVersion to 1 for legacy

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

* Fix world-vercel queue tests missing VERCEL_DEPLOYMENT_ID setup

Two tests were calling queue.queue() without setting up
VERCEL_DEPLOYMENT_ID, causing them to fail with "No deploymentId
provided" error before reaching the code they were testing.

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

* Add specVersion to all event creation calls in core package

The server's CreateEventSchemaV2 requires specVersion on all events,
but only run_created was sending it. This caused 400 Bad Request errors
for all other event types (run_started, run_completed, run_failed,
run_cancelled, step_created, step_started, step_completed, step_failed,
step_retrying, hook_created, hook_received, wait_created, wait_completed).

Now all event creation calls include specVersion: SPEC_VERSION_CURRENT.

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

---------

Signed-off-by: Peter Wielander <mittgfu@gmail.com>
Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Peter Wielander <mittgfu@gmail.com>
2026-01-22 17:24:52 -08:00
Michael Han 0b5cc48140 fix(builders): manifest missing workflow-only files (no steps), add tests (#831)
* Fix manifest missing workflow-only files (no steps), add tests

Signed-off-by: voyager14 <21mh124@queensu.ca>

* Update packages/builders/src/base-builder.ts

Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com>
Signed-off-by: Michael Han <21mh124@queensu.ca>

---------

Signed-off-by: voyager14 <21mh124@queensu.ca>
Signed-off-by: Michael Han <21mh124@queensu.ca>
Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com>
2026-01-22 17:22:14 -08:00
Nathan Rajlich 1843704b83 Add support for custom class instance serialization (#762)
Added support for custom class instance serialization across workflow/step boundaries.

### What changed?

- Introduced a new `@workflow/serde` package with `WORKFLOW_SERIALIZE` and `WORKFLOW_DESERIALIZE` symbols
- Enhanced the serialization system to handle custom class instances using these symbols
- Updated the SWC plugin to detect classes with serialization methods and register them
- Added class registry mechanism that works in both step and workflow contexts
- Implemented comprehensive tests for various serialization scenarios

### How to test?

The PR includes a new e2e test `customSerializationWorkflow` that demonstrates the feature:

```typescript
import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from '@workflow/serde';

// Define a class with custom serialization
class Point {
  constructor(public x: number, public y: number) {}

  static [WORKFLOW_SERIALIZE](instance: Point) {
    return { x: instance.x, y: instance.y };
  }

  static [WORKFLOW_DESERIALIZE](data: { x: number; y: number }) {
    return new Point(data.x, data.y);
  }
}

// Use in workflow and steps
export async function customSerializationWorkflow(x: number, y: number) {
  'use workflow';
  const point = new Point(x, y);
  const scaled = await transformPoint(point, 2);
  // ...
}
```

Run the e2e test to verify that class instances are properly serialized and deserialized.

### Why make this change?

Previously, user-defined class instances couldn't be passed between workflows and steps without losing their prototype chain and methods. This change allows developers to define custom serialization/deserialization logic for their classes, enabling proper reconstruction of instances with their full functionality intact when crossing workflow/step boundaries.
2026-01-19 15:38:19 -08:00
Nathan Rajlich 7906429541 Add support for serializing this when invoking step functions (#754)
Added support for serializing `this` context when invoking step functions.

### What changed?

- Modified the step function implementation to capture and serialize the `this` context when it's defined and not the global object
- Updated the step invocation queue item interface to include an optional `thisVal` property
- Enhanced the step handler to apply the hydrated `thisVal` when executing step functions

### Why make this change?

This enhancement allows step functions to be invoked with an explicit context object using standard JavaScript methods like `.call()` and `.apply()`. This is particularly useful for step functions that need to access properties from a specific context, enabling more flexible and idiomatic JavaScript patterns within workflows.
2026-01-14 00:07:29 -08:00
Nathan Rajlich a2fc53a0dc Support class static methods with "use step" / "use workflow" (#753)
The SWC compiler plugin had logic to walk through class static methods
with "use step" / "use workflow", but no actual transformation was being
applied. This fixes that.
2026-01-13 23:52:26 -08:00
Nathan Rajlich 61fdb41e1b Add queue-based health check (#743)
* feat: add queue-based health check to bypass Deployment Protection

- Add HealthCheckPayloadSchema and HEALTH_CHECK_STREAM_PREFIX to @workflow/world
- Add healthCheck() method to Queue interface
- Update workflow and step handlers to detect and respond to health check messages
- Implement healthCheck() in world-local, world-vercel, and world-postgres

The queue-based health check sends a message through the queue pipeline,
which bypasses Vercel's Deployment Protection. The handler writes a response
to a stream that the caller reads to confirm health.

This complements the existing HTTP-based ?__health approach which still works
for local development and when bypass headers are available.

* refactor: move healthCheck to core package as utility function

Instead of adding healthCheck to the World interface (which duplicated
the same implementation across all worlds), this is now a utility function
in @workflow/core that takes the World as a parameter.

Usage:
  import { healthCheck } from '@workflow/core';
  const result = await healthCheck(world, 'workflow');

This is cleaner because:
- Single implementation instead of 3 identical ones
- World implementations remain simple
- No changes needed to the World interface

* .

* refactor: move health check types from world to core

Health check types (HealthCheckPayloadSchema, HealthCheckResult, etc.)
are now defined in @workflow/core since that's where they're used.

The HealthCheckPayloadSchema is still part of QueuePayloadSchema in
world (so the queue accepts health check messages), but it's not
exported from the public API.

* .

* Refactor health check implementation based on code review feedback (#746)

* Initial plan

* Address PR review comments: export types, fix race condition, improve error handling

Co-authored-by: TooTallNate <71256+TooTallNate@users.noreply.github.com>

* Add queue-based health check test and document security considerations

Co-authored-by: TooTallNate <71256+TooTallNate@users.noreply.github.com>

* Replace 'any' type with proper type guards for health check response

Co-authored-by: TooTallNate <71256+TooTallNate@users.noreply.github.com>

* Extract health check queue names as constants and improve type guards

Co-authored-by: TooTallNate <71256+TooTallNate@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: TooTallNate <71256+TooTallNate@users.noreply.github.com>

* .

* Fix e2e test

* .

* .

* .

* fix(ai): preserve providerMetadata as providerOptions in multi-turn tool calls (#733)

When tool calls are added to the conversation history, map providerMetadata
to providerOptions following the AI SDK convention. This fixes Gemini thinking
models that require thoughtSignature to be preserved across multi-turn tool calls,
preventing the error 'function call is missing a thought_signature'.

Fixes #727

* Local ui cli flag (#744)

* [web] Increase contrast on attribute items in sidebar (#736)

Signed-off-by: Peter Wielander <mittgfu@gmail.com>

* [world] Remove pause and resume events, actions and states (#751)

* Version Packages (beta) (#735)

* .

* .

* Update turbo inputs to include shared config (#752)

* Update turbo inputs to include shared config

* Apply suggestions from code review

Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com>

---------

Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com>

* feat(web): add self-hosted mode for world configuration (#747)

* feat(web): add self-hosted mode for world configuration

When WORKFLOW_TARGET_WORLD env var is set, the web UI operates in
self-hosted mode where the world configuration is locked to server-side
environment variables and cannot be changed via query params or UI.

- Add getHardcodedConfig server action to detect self-hosted mode
- Modify getWorldFromEnv to use server env vars in hardcoded mode
- Create WorldConfigContext to provide config state app-wide
- Update settings sidebar to show locked state with disabled inputs
- Update connection status to show PostgreSQL backend info
- Mask sensitive values (postgres URL) in hardcoded mode UI

* fix: address PR review feedback

- Remove unused ConfigMode type export
- Fix postgres substring to undefined (tooltip has details)
- Extract buildEnvMapFromProcessEnv helper to reduce duplication
- Remove unused EnvMap import from layout-client
- Import HardcodedConfig from web-shared/server instead of re-defining

* Fix: PostgreSQL URL parameter missing from configParsers, causing loss of postgres URL configuration on page reload in dynamic mode

* fix(cli): clear WORKFLOW_TARGET_WORLD when spawning web server

The CLI sets WORKFLOW_TARGET_WORLD as an env var, which the spawned
Next.js server inherits. This caused the web UI to enter self-hosted
mode even when launched via CLI.

Now we explicitly clear WORKFLOW_TARGET_WORLD from the server's
environment so it starts in dynamic mode where config comes from
query params as intended.

* refactor(web): use server-side env vars for world config

BREAKING CHANGE: The web UI no longer supports configuring the world
backend via URL query parameters. Configuration is now read exclusively
from server-side environment variables.

Changes:
- Remove query param parsing from @workflow/web config.ts
- Add ServerConfig interface with non-sensitive display info
- Update all components to use useServerConfig() hook
- Settings sidebar is now read-only
- CLI passes env vars to spawned web server instead of query params
- Server actions use process.env directly (envMap param reserved for future use)

This simplifies the architecture and improves security by never sending
sensitive data (connection strings, auth tokens) to the client.

* fix(web): fix settings sidebar overflow and shorten data dir path

- Add truncate/overflow handling to settings sidebar config values
- Add shortenPath() helper to abbreviate long file paths:
  - Replaces home directory with ~
  - Shows .../last-two-segments if still too long
- Add title attributes for full path on hover

* Update changeest

---------

Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>

* Version Packages (beta) (#755)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Update packages/world/src/queue.ts

Co-authored-by: Pranay Prakash <pranay.gp@gmail.com>

* [web] Tidy wake-up and re-enqueue buttons (#737)


---------

Signed-off-by: Peter Wielander <mittgfu@gmail.com>

* [cli] Use dotenv to resolve .env and .env.local files on startup (#765)

* Use temporary workflow-server deployment URL

* feat: add queue-based health check to bypass Deployment Protection

- Add HealthCheckPayloadSchema and HEALTH_CHECK_STREAM_PREFIX to @workflow/world
- Add healthCheck() method to Queue interface
- Update workflow and step handlers to detect and respond to health check messages
- Implement healthCheck() in world-local, world-vercel, and world-postgres

The queue-based health check sends a message through the queue pipeline,
which bypasses Vercel's Deployment Protection. The handler writes a response
to a stream that the caller reads to confirm health.

This complements the existing HTTP-based ?__health approach which still works
for local development and when bypass headers are available.

* refactor: move healthCheck to core package as utility function

Instead of adding healthCheck to the World interface (which duplicated
the same implementation across all worlds), this is now a utility function
in @workflow/core that takes the World as a parameter.

Usage:
  import { healthCheck } from '@workflow/core';
  const result = await healthCheck(world, 'workflow');

This is cleaner because:
- Single implementation instead of 3 identical ones
- World implementations remain simple
- No changes needed to the World interface

* .

* refactor: move health check types from world to core

Health check types (HealthCheckPayloadSchema, HealthCheckResult, etc.)
are now defined in @workflow/core since that's where they're used.

The HealthCheckPayloadSchema is still part of QueuePayloadSchema in
world (so the queue accepts health check messages), but it's not
exported from the public API.

* .

* Refactor health check implementation based on code review feedback (#746)

* Initial plan

* Address PR review comments: export types, fix race condition, improve error handling

Co-authored-by: TooTallNate <71256+TooTallNate@users.noreply.github.com>

* Add queue-based health check test and document security considerations

Co-authored-by: TooTallNate <71256+TooTallNate@users.noreply.github.com>

* Replace 'any' type with proper type guards for health check response

Co-authored-by: TooTallNate <71256+TooTallNate@users.noreply.github.com>

* Extract health check queue names as constants and improve type guards

Co-authored-by: TooTallNate <71256+TooTallNate@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: TooTallNate <71256+TooTallNate@users.noreply.github.com>

* .

* Fix e2e test

* .

* .

* .

* .

* .

* Update packages/world/src/queue.ts

Co-authored-by: Pranay Prakash <pranay.gp@gmail.com>

* Use temporary workflow-server deployment URL

* .

* .

---------

Signed-off-by: Peter Wielander <mittgfu@gmail.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: TooTallNate <71256+TooTallNate@users.noreply.github.com>
Co-authored-by: Pranay Prakash <pranay.gp@gmail.com>
Co-authored-by: Peter Wielander <mittgfu@gmail.com>
Co-authored-by: Vercel Release Bot <88769842+vercel-release-bot@users.noreply.github.com>
Co-authored-by: JJ Kasper <jj@jjsweb.site>
Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com>
Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-01-12 14:57:10 -08:00