Move the Instance and Class reducers before the Error reducer in
getCommonReducers() so that Error subclasses implementing
WORKFLOW_SERIALIZE are serialized using custom class serialization
instead of the generic Error reducer. devalue uses first-match-wins,
so the previous ordering caused custom Error subclasses to lose their
custom fields (only name/message/stack were preserved).
* 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>
* feat(core): warn on uncommitted operations when workflow completes
Log a warning when a workflow run completes or fails with uncommitted
operations still in the invocations queue (unawaited steps, hooks, or
sleeps). This helps users diagnose forgotten `await` calls.
Hook disposals are excluded from the warning since the backend
auto-disposes all hooks when a run reaches a terminal state.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: add tests for pending queue warning on workflow completion
Test that:
- Unawaited step triggers warning on successful completion
- Unawaited step triggers warning on workflow failure
- hook_disposed items are excluded from warning
- Clean workflow completion produces no warning
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: update warnPendingQueueItems filter for new hook flag pattern
Filter out hooks with hasCreatedEvent && !disposed (alive hooks) instead
of the removed hook_disposed queue item type.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address PR review comments
- Remove workflow from changeset (transitive via @workflow/core)
- Add default case to switch for exhaustiveness
- Use correlationId instead of token in warning message to avoid leaking
user-provided tokens into logs
- Remove step events from warning tests so unawaited steps stay pending
in the queue (step_completed would clear the item during replay)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address PR review comments on pending queue warnings
- Filter out disposed hooks from warning (not just created hooks)
- Show hook token instead of correlationId in warning (user-facing)
- Rename test to "should not warn when hooks are properly disposed"
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* 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>
* [docs] Split the Worlds page into sections
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
* Wording
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
* Update text
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
* remove benchmark numbers
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
---------
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
* Fix og route handling in i18n middleware
* Add docs CI smoke test for OG and sitemap
* Run docs OG checks against Vercel preview
* Refactor docs CI checks and rename smoke test
* Use docs Vercel token for preview checks
* Expand docs OG smoke coverage
* Check docs HTML OG metadata
* [@workflow/world-vercel] Handle null key response from run-key API endpoint
The API endpoint now returns { key: null } instead of a 404 when a
deployment has no encryption key. Update fetchRunKey to accept the
nullable response and return undefined, signaling that encryption
is disabled for that workflow run.
* [@workflow/world-vercel] Add changeset for null run-key handling
* Address review feedback: improve error message and fix test mock cleanup
- Include Zod error details in the validation error message for better debugging
- Move fetch mock restoration to afterEach to prevent mock leakage on test failure
* fix: use types.isNativeError() for cross-VM Error serialization
FatalError was not properly serialized when passed from workflow code into a step function because the Error reducer checked `value instanceof global.Error` where `global` is the VM's globalThis. Errors created in the host context (like FatalError from @workflow/errors) have a different Error prototype than the VM context, so the instanceof check returned false and the error was silently dropped.
Replaced with `types.isNativeError()` from `node:util` which uses V8's internal type tag and works across VM context boundaries.
* ci: don't cancel in-progress CI runs on main branch
FatalError was not properly serialized when passed from workflow code into a step function because the Error reducer checked `value instanceof global.Error` where `global` is the VM's globalThis. Errors created in the host context (like FatalError from @workflow/errors) have a different Error prototype than the VM context, so the instanceof check returned false and the error was silently dropped.
Replaced with `types.isNativeError()` from `node:util` which uses V8's internal type tag and works across VM context boundaries.
* Bring back sitemap
* Bring back sitemap
* Add pre commit hooks for sitemap
* Add pre commit hooks for sitemap
* Fix failing queue test
* Revert queue test failure
* Add unit tests
* Fix sleep state in side panel
* Side panel improvements
* Side panel improvements
* Side panel improvements
* Side panel improvements
* Side panel improvements
* Add browser-compatible AES-GCM to core and HKDF key derivation to world-vercel
* update changeset
* Move HKDF key derivation server-side: API returns per-run derived key
* Refactor encrypt/decrypt to accept CryptoKey, export importKey for callers to import once per run
* Pass WorkflowRun entity to getEncryptionKeyForRun where available (avoids redundant lookup)
* .
* Refactor handleSuspension to accept WorkflowRun, pass run entity to getEncryptionKeyForRun
* Overload getEncryptionKeyForRun: accept context for start(), fetch WorkflowRun in resume-hook
* Split changeset into per-package descriptions for world, world-vercel, and core
* Remove unnecessary Uint8Array.from() wrapper around Buffer.from()
* Use zod to parse Vercel API response
* Address peter's comment
* Bring back sitemap
* Bring back sitemap
* Add pre commit hooks for sitemap
* Add pre commit hooks for sitemap
* Fix failing queue test
* Revert queue test failure
* Add unit tests
Adds a workflow-init skill that guides users through setting up Vercel
Workflow DevKit. Auto-detects the user's framework from package.json,
then directs the agent to fetch the official getting-started guide from
useworkflow.dev. After setup, hands off to the /workflow skill.
* fix(workflow): improve error message for runtime API usage in workflow context and fix SKILL.md streaming docs
The workflow stub error now tells users to move calls to a step function or outside the workflow context. Also corrects the SKILL.md streaming section which incorrectly stated getWritable() only works in steps.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* chore: add changeset
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(skill): address PR feedback on SKILL.md
- Remove `npx workflow start` from debugging section (args can't easily be passed via CLI)
- Mention --json and --help flags for CLI inspection
- Add reminder to check AI docs for DurableAgent details
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(skill): fix SKILL.md debugging section CLI commands
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* other improvements
* fix(skill): address Copilot PR feedback on streaming examples
- Add missing imports and define all referenced variables
- Wrap writer.releaseLock() in finally blocks for safety
- Remove undefined `data`/`agent`/`messages` references
- Simplify examples to be self-contained and copy-pasteable
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* chore(skill): bump SKILL.md version to 1.1
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: add skill version bump rule to CLAUDE.md
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: John Lindquist <johnlindquist@gmail.com>