Skip to content

workflow: read spec 7 and skip sealed noops - #319

Merged
fantix merged 4 commits into
mainfrom
pgp/port-noop-event-to-py
Aug 24, 2026
Merged

workflow: read spec 7 and skip sealed noops#319
fantix merged 4 commits into
mainfrom
pgp/port-noop-event-to-py

Conversation

@pranaygp

Copy link
Copy Markdown
Contributor

Python half of vercel/workflow#3634 (Add support for 'noop' event type — spec version 7).

Why this is urgent for us

That PR moves SPEC_VERSION_CURRENT and SPEC_VERSION_MAX_SUPPORTED to 7 and switches both @workflow/world-local and @workflow/world-vercel to mintedSpecVersion(), which returns 7 by default. Our reader ceiling was 6, and it is a pydantic le= on BaseEvent.specVersion — so a spec-7 row does not degrade, it fails validation, and with it the whole page. The e2e-python conformance lane runs the TypeScript driver against workbench/python over a shared world-local data dir, so it is the first thing that breaks: the driver's run_created is now labelled 7 and the Python app cannot parse the log it is supposed to replay.

What spec 7 actually asks of a client

On the server, a spec-7 run's slot positions come from a per-run counter handed out before the write that fills them, so concurrent writers never race for a position. The price is that a writer that takes a position and dies leaves a hole, and the backend's read path fills provably-abandoned holes with a server-written noop so returned pages stay dense prefixes.

So the client contract is one thing, and it is read-only: a reader must know a noop occupies its slot and means nothing.

There is no writer half for us. A World only owes the sealing half if it pre-assigns positions ahead of the commit; our local world allocates each id at the commit that fills it, so it has no holes to seal and will never emit one.

The change

world.py

  • SPEC_VERSION_SUPPORTS_SEALED_LOG = 7, and SPEC_VERSION_MAX_SUPPORTED follows it. SPEC_VERSION_CURRENT stays 2 — we still stamp what we always stamped, and a run created elsewhere keeps its creator's version.
  • NoopEvent / NoopEventData, in the read union and deliberately not in CreateEventRequest. eventData is optional and open (extra="allow", matching the .passthrough() on the TS schema): nothing reads it, the shape belongs to whichever backend sealed the slot, and a reader whose only interest is skipping the row has no business rejecting a field it has not heard of.
  • is_sealed_noop_event(), mirroring isSealedNoopEvent — one home for the test.

runtime.pyget_all_workflow_run_events() drops seals, so the log the replay walks is the log without them.

That is the whole skip, and the reason it is one line at the loader rather than a continue in resume() is the clock. Everything downstream of that function is reconstructing what the run did — matching correlation ids to suspensions, looking for a terminal event, deciding which waits elapsed, and reading createdAt for now(). Nothing on this side walks positions, so there is no caller that wants the seal. Meanwhile a seal's createdAt is the sealer's wall clock, which can postdate every real event around it; now() dates the run from the last event the replay consumed, so a seal left in the list hands the body a time no event of the run happened at — and then, because the clock only moves forward, every later now() too. Dropping it at the boundary makes the equivalence the contract is about (a sealed log replays exactly like the same log whose holes their own writers filled) hold by construction, rather than by three call sites remembering. The cursor is untouched: it is the World's, and still points past every row that was read.

Tests

tests/unit/test_workflow_sealed_log_noop.py, 12 tests in four parts:

  • the row — parses out of the read union with eventData, without it, and with an eventData field we have never heard of; the predicate answers for seals only; NoopEvent is in Event and absent from CreateEventRequest; 7 is the ceiling and 8 is still rejected.
  • storage — a seal round-trips through the local world and numbering continues past it, mirroring slot-identity.test.ts's stores, lists, and numbers past a noop event.
  • the loader — seals at head, middle (consecutive) and tail are dropped with the order of everything else intact; a page of nothing but seals still pages on; the cursor survives.
  • replay — the real claim, driven end to end through workflow_handler: one recorded two-sleep run, twice, differing only in seals at the head, in the middle (including one splitting a wait_created/wait_completed pair), and at the tail. Same output, same events written back, and the three now() readings pinned literally so a change that broke both logs the same way cannot slip past the comparison. Plus the degenerate all-seals-before-the-run shape, where the first now() has to find the first real event.

Every seal in the file is stamped a minute into the future, so each of those clock assertions would pass by accident if a seal's createdAt were plausible. Reverting the one-line filter fails 5 of the 12.

test_workflow_local_world_format.py's two spec-version tests move 6 → 7; the ceiling assertion now derives its match from SPEC_VERSION_MAX_SUPPORTED instead of hardcoding the number, so the next bump only has to touch the constant.

Full suite: 1127 tests, 0 failures (test_the_api_token_is_config_then_env_then_oidc fails locally on a box with VERCEL_TOKEN exported, before and after this change). Lint and typecheck clean repo-wide.

Follow-up, not in this PR

workbench/python/pyproject.toml in vercel/workflow resolves vercel-workflow from PyPI (0.9.0 today) regardless of its [tool.uv.sources] rev, so the e2e-python lane only picks this up once it is released and the lock is refreshed there — or once that file git-pins vercel-workflow at a commit containing it, which is what the comment block above that source entry describes.

🤖 Generated with Claude Code

`@workflow/world-local` and `@workflow/world-vercel` both stamp
specVersion 7 now (vercel/workflow#3634), so every event a TypeScript
driver writes arrives labelled 7 and our reader ceiling of 6 rejected
the whole log -- including on the local world the Python e2e lane runs
on. Raise the ceiling, and add the one thing the version gates: a
`noop` row, written by a World's backend to occupy a slot whose writer
allocated the position and then died.
A noop parses out of the read union but is dropped from the log the
replay walks. Its `createdAt` is the sealer's wall clock, which can
postdate every real event around it, so letting it reach the
deterministic clock would make a log whose hole was sealed replay
differently from the same log whose hole its own writer filled.
Nothing here writes one: we allocate each event id at the commit that
fills it, so we have no holes to seal, and `noop` stays out of the
create union.
Co-Authored-By: Pranay Prakash <1797812+pranaygp@users.noreply.github.com>

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@fantixfantix left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM; let me trim off some AI in-code chats and merge this to unbreak the e2e test.

@fantix
fantixforce-pushed the pgp/port-noop-event-to-py branch from 3116c4e to abb6ce9CompareAugust 24, 2026 18:44
@fantix
fantix merged commit a5969aa into mainAug 24, 2026
14 checks passed
@fantix
fantix deleted the pgp/port-noop-event-to-py branch August 24, 2026 18:49
@scotttrinhscotttrinh mentioned this pull request Aug 26, 2026
scotttrinh added a commit that referenced this pull request Aug 26, 2026
vercel-internal-core
--------------------
0.1.3 - 2026-08-26
------------------
Internal
--------
- Support disabling HTTP timeouts for selected SDK operations while preserving the client default elsewhere. (#307)
vercel-connect
--------------
0.1.1 - 2026-08-26
------------------
- Update dependencies.
vercel-queue
------------
0.8.1 - 2026-08-26
------------------
Documentation
-------------
- Remove documentation and examples for `asgi_app` in preparation for its removal. (#309)
vercel-sandbox
--------------
0.5.0 - 2026-08-26
------------------
Features
--------
- Add sync and async `fork_sandbox(...)` support for creating a sandbox from an existing named sandbox with optional configuration overrides. (#257)
- Add `region` and `failover_regions` configuration for sandbox creation, forks, and updates, plus multi-region snapshot availability reporting. (#308)
Bug Fixes
---------
- Allow Sandbox process waits and log streams to remain idle longer than the session HTTP timeout. (#307)
vercel-workflow
---------------
0.10.0 - 2026-08-26
-------------------
Breaking Changes
----------------
- Use type annotations on workflows and step to allow passing Pydantic models and dataclasses. (#317)
- This is a breaking change, because type annotations will now be enforced. Passing a `dict` when the declaration expects a `list` will fail. (#317)
- Pydantic models and dataclasses can no longer be passed to `@serializable` or `register_serializable()`. Annotate the workflow or step parameter or return value with their type instead. (#317)
Features
--------
- `get_workflow_metadata()` returns the current run's `WorkflowInfo` (run id, workflow name, start time, deployment URL, and feature flags), callable from a workflow body or a step body — mirroring the JS SDK's `getWorkflowMetadata()`. (#320)
- One current limitation is that `started_at` is `None` from inside a step. (#320)
- `BaseHook.wait()` accepts `metadata` to record on the hook, and `get_hook_by_token()` reads it back for a resumer. (#301)
- A step can raise `RetryableError` to control when its next attempt runs. (#302)
- Accept `specVersion` 7 sealed noop event logs. (#319)
- A workflow or step can attach plaintext metadata to its run with `set_attributes()`. (#303)
- Add a `share_sandboxes` parameter to `SandboxPolicy` to enable reusing already created sandboxes instead of creating a new one on each invocation. This speeds up workflows but means that modifications to global state may persist between invocations. (#310)
- Expose unstable API to serve workflow HTTP endpoint from your own web framework. (#294)
- Added semi-internal manifest API for TS tools and e2e test. (#296)
Bug Fixes
---------
- Fix failing or even crashing cipher calls inside the workflow sandbox. (#305)
- Support resuming hooks with payload in the queue message. (#300)
- Fixed nulls rejected by server, requiring Pydantic 2.12 or newer. (#321)
- Fixed workflow and step calls with both positional-or-keyword parameters and `*args` failing during replay because their arguments were recorded in an unbindable shape. (#312)
Internal
--------
- Construct the protocol models by Python field name. (#322)
vercel
------
0.11.0 - 2026-08-26
-------------------
Features
--------
- Expose `get_deadline()` for reading the current Function invocation deadline. (#306)
- Answer workflow health checks for both queue-based transport and HTTP. (#292)
- Add support to read the sealed (`encp`) workflow payloads (X25519 + AES-GCM) an outside writer addresses to a run, under the `encryption` extra. (#297)
Bug Fixes
---------
- Remove upper bounds on aggregate Sandbox and Workflow dependencies so sibling releases cannot make the `vercel` package un-installable. (#334)
- Start a workflow run even when its queue message arrives before the `run_created` event has landed. (#284)
Internal
--------
- The Workflows implementation now ships in the separate `vercel-workflow` distribution, which `vercel` depends on, so `vercel.workflow` imports keep working without installing anything extra. (#299)
vercel-apscheduler
------------------
0.3.0 - 2026-08-26
------------------
Breaking Changes
----------------
- The managed Redis backend was removed. The integration now always runs on its managed job store (Vercel Runtime Cache); a configured default `RedisJobStore` is rejected at import, `VERCEL_APSCHEDULER_BACKEND` accepts only `cache`, and the `redis` dependency is gone. The scheduler's durable identity now always derives from the builder-assigned subscriber id (previously the Redis `jobs_key`); the `scheduler_id` option still pins an identity explicitly. (#286)
vercel-celery
-------------
0.7.5 - 2026-08-26
------------------
- Update dependencies.
vercel-django-tasks
-------------------
0.7.0 - 2026-08-26
------------------
Features
--------
- Add a Vercel Queues backend for Django Tasks and use it by default when no task backends are configured. (#291)
vercel-dramatiq
---------------
0.7.4 - 2026-08-26
------------------
- Update dependencies.
scotttrinh added a commit that referenced this pull request Aug 31, 2026
vercel-headers
--------------
0.7.2 - 2026-08-31
------------------
Bug Fixes
---------
- Accept request objects with concrete header implementations in the IP address and geolocation type annotations. (#337)
vercel-internal-core
--------------------
0.1.3 - 2026-08-31
------------------
Internal
--------
- Support disabling HTTP timeouts for selected SDK operations while preserving the client default elsewhere. (#307)
vercel-oidc
-----------
0.8.1 - 2026-08-31
------------------
- Update dependencies.
vercel-connect
--------------
0.1.1 - 2026-08-31
------------------
- Update dependencies.
vercel-internal-telemetry
-------------------------
0.7.3 - 2026-08-31
------------------
- Update dependencies.
vercel-queue
------------
0.8.1 - 2026-08-31
------------------
Bug Fixes
---------
- Force embedded development servers to exit when graceful shutdown stalls. (#351)
Documentation
-------------
- Remove documentation and examples for `asgi_app` in preparation for its removal. (#309)
vercel-sandbox
--------------
0.5.0 - 2026-08-31
------------------
Features
--------
- Add sync and async `fork_sandbox(...)` support for creating a sandbox from an existing named sandbox with optional configuration overrides. (#257)
- Add `region` and `failover_regions` configuration for sandbox creation, forks, and updates, plus multi-region snapshot availability reporting. (#308)
- Forward private ``__``-prefixed parameters to the Sandbox API. (#350)
Bug Fixes
---------
- Allow Sandbox process waits and log streams to remain idle longer than the session HTTP timeout. (#307)
- Expose Linux process signals consistently on every SDK host platform. (#352)
vercel-cache
------------
0.7.3 - 2026-08-31
------------------
- Update dependencies.
vercel-workflow
---------------
0.10.0 - 2026-08-31
-------------------
Breaking Changes
----------------
- Make sleep() and retry delays treat numbers as seconds, not ms (#346)
- This matches Python standard library APIs. (#346)
- Use type annotations on workflows and step to allow passing Pydantic models and dataclasses. (#317)
- This is a breaking change, because type annotations will now be enforced. Passing a `dict` when the declaration expects a `list` will fail. (#317)
- Pydantic models and dataclasses can no longer be passed to `@serializable` or `register_serializable()`. Annotate the workflow or step parameter or return value with their type instead. (#317)
Features
--------
- Support `call_later`, `call_at`, and `now` in the event loop implementation. (#343)
- This enables use of `asyncio.sleep()` as well as `asyncio.timeout` and the `timeout` parameter of `asyncio.wait_for`. (#343)
- `get_workflow_metadata()` returns the current run's `WorkflowInfo` (run id, workflow name, start time, deployment URL, and feature flags), callable from a workflow body or a step body — mirroring the JS SDK's `getWorkflowMetadata()`. (#320)
- One current limitation is that `started_at` is `None` from inside a step. (#320)
- `BaseHook.wait()` accepts `metadata` to record on the hook, and `get_hook_by_token()` reads it back for a resumer. (#301)
- A step can raise `RetryableError` to control when its next attempt runs. (#302)
- Accept `specVersion` 7 sealed noop event logs. (#319)
- Failed run and step events now preserve serialized error classes, messages, stacks, and causes. Failed runs also expose a plaintext `errorCode`. (#304)
- A workflow or step can attach plaintext metadata to its run with `set_attributes()`. (#303)
- Add a `share_sandboxes` parameter to `SandboxPolicy` to enable reusing already created sandboxes instead of creating a new one on each invocation. This speeds up workflows but means that modifications to global state may persist between invocations. (#310)
- Support `timedelta` arguments for workflow `sleep()` and retry delays. (#342)
- Expose unstable API to serve workflow HTTP endpoint from your own web framework. (#294)
- Added semi-internal manifest API for TS tools and e2e test. (#296)
Bug Fixes
---------
- Fix failing or even crashing cipher calls inside the workflow sandbox. (#305)
- Fail a workflow run with `HookConflictError` when another run already owns its hook token instead of leaving it running indefinitely. (#327)
- Support resuming hooks with payload in the queue message. (#300)
- Fix some bugs involving hooks arriving when the workflow was not yet blocked on them. (#339)
- Fixed nulls rejected by server, requiring Pydantic 2.12 or newer. (#321)
- Prevent workflows from having side effects while suspending. (#332)
- `hook.dispose()` will now work properly in a `finally` block. (That is, the hook will be disposed only when the workflow is actually terminating, and not every time it gets replayed.) (#332)
- More reliably fail runs whose replay diverges from the event log. (#347)
- Runs will now fail even in the case where the main thread of execution is not directly blocked on the suspension that is erroring. (#347)
- Fixed workflow and step calls with both positional-or-keyword parameters and `*args` failing during replay because their arguments were recorded in an unbindable shape. (#312)
Internal
--------
- Remove a just-added return from a finally block. (#344)
- Correct internal workflow type annotations found by checking untyped function bodies. (#337)
- Refactored event replay. (#341)
- Construct the protocol models by Python field name. (#322)
vercel
------
0.11.0 - 2026-08-31
-------------------
Features
--------
- Expose `get_deadline()` for reading the current Function invocation deadline. (#306)
- Answer workflow health checks for both queue-based transport and HTTP. (#292)
- Add support to read the sealed (`encp`) workflow payloads (X25519 + AES-GCM) an outside writer addresses to a run, under the `encryption` extra. (#297)
Bug Fixes
---------
- Remove upper bounds on aggregate Sandbox and Workflow dependencies so sibling releases cannot make the `vercel` package un-installable. (#334)
- Start a workflow run even when its queue message arrives before the `run_created` event has landed. (#284)
Internal
--------
- The Workflows implementation now ships in the separate `vercel-workflow` distribution, which `vercel` depends on, so `vercel.workflow` imports keep working without installing anything extra. (#299)
vercel-apscheduler
------------------
0.3.0 - 2026-08-31
------------------
Breaking Changes
----------------
- The managed Redis backend was removed. The integration now always runs on its managed job store (Vercel Runtime Cache); a configured default `RedisJobStore` is rejected at import, `VERCEL_APSCHEDULER_BACKEND` accepts only `cache`, and the `redis` dependency is gone. The scheduler's durable identity now always derives from the builder-assigned subscriber id (previously the Redis `jobs_key`); the `scheduler_id` option still pins an identity explicitly. (#286)
vercel-celery
-------------
0.7.5 - 2026-08-31
------------------
- Update dependencies.
vercel-django-tasks
-------------------
0.7.0 - 2026-08-31
------------------
Features
--------
- Add a Vercel Queues backend for Django Tasks and use it by default when no task backends are configured. (#291)
vercel-dramatiq
---------------
0.7.4 - 2026-08-31
------------------
- Update dependencies.
scotttrinh added a commit that referenced this pull request Aug 31, 2026
vercel-headers
--------------
0.7.2 - 2026-08-31
------------------
Bug Fixes
---------
- Accept request objects with concrete header implementations in the IP address and geolocation type annotations. (#337)
vercel-internal-core
--------------------
0.1.3 - 2026-08-31
------------------
Internal
--------
- Support disabling HTTP timeouts for selected SDK operations while preserving the client default elsewhere. (#307)
vercel-oidc
-----------
0.8.1 - 2026-08-31
------------------
- Update dependencies.
vercel-connect
--------------
0.1.1 - 2026-08-31
------------------
- Update dependencies.
vercel-internal-telemetry
-------------------------
0.7.3 - 2026-08-31
------------------
- Update dependencies.
vercel-queue
------------
0.8.1 - 2026-08-31
------------------
Bug Fixes
---------
- Force embedded development servers to exit when graceful shutdown stalls. (#351)
Documentation
-------------
- Remove documentation and examples for `asgi_app` in preparation for its removal. (#309)
vercel-sandbox
--------------
0.5.0 - 2026-08-31
------------------
Features
--------
- Add sync and async `fork_sandbox(...)` support for creating a sandbox from an existing named sandbox with optional configuration overrides. (#257)
- Add `region` and `failover_regions` configuration for sandbox creation, forks, and updates, plus multi-region snapshot availability reporting. (#308)
- Forward private ``__``-prefixed parameters to the Sandbox API. (#350)
Bug Fixes
---------
- Allow Sandbox process waits and log streams to remain idle longer than the session HTTP timeout. (#307)
- Expose Linux process signals consistently on every SDK host platform. (#352)
vercel-cache
------------
0.7.3 - 2026-08-31
------------------
- Update dependencies.
vercel-workflow
---------------
0.10.0 - 2026-08-31
-------------------
Breaking Changes
----------------
- Make `await hook` never return `None`
- Raises a new `HookDisposedError` instead of returning `None` when the hook has been disposed. It is now typed to return `T` instead of `T | None`. `async for` over a hook will stop iterating on disposal, still.
- Make sleep() and retry delays treat numbers as seconds, not ms (#346)
- This matches Python standard library APIs. (#346)
- Use type annotations on workflows and step to allow passing Pydantic models and dataclasses. (#317)
- This is a breaking change, because type annotations will now be enforced. Passing a `dict` when the declaration expects a `list` will fail. (#317)
- Pydantic models and dataclasses can no longer be passed to `@serializable` or `register_serializable()`. Annotate the workflow or step parameter or return value with their type instead. (#317)
Features
--------
- Support `call_later`, `call_at`, and `now` in the event loop implementation. (#343)
- This enables use of `asyncio.sleep()` as well as `asyncio.timeout` and the `timeout` parameter of `asyncio.wait_for`. (#343)
- `get_workflow_metadata()` returns the current run's `WorkflowInfo` (run id, workflow name, start time, deployment URL, and feature flags), callable from a workflow body or a step body — mirroring the JS SDK's `getWorkflowMetadata()`. (#320)
- One current limitation is that `started_at` is `None` from inside a step. (#320)
- Make `HookEvent` an async context manager
- This matches TS, which supports `using`. ``` # disposes the hook on block exit async with SomeHook.wait(...) as hook: res = await hook ```
- `BaseHook.wait()` accepts `metadata` to record on the hook, and `get_hook_by_token()` reads it back for a resumer. (#301)
- A step can raise `RetryableError` to control when its next attempt runs. (#302)
- Accept `specVersion` 7 sealed noop event logs. (#319)
- Failed run and step events now preserve serialized error classes, messages, stacks, and causes. Failed runs also expose a plaintext `errorCode`. (#304)
- A workflow or step can attach plaintext metadata to its run with `set_attributes()`. (#303)
- Add a `share_sandboxes` parameter to `SandboxPolicy` to enable reusing already created sandboxes instead of creating a new one on each invocation. This speeds up workflows but means that modifications to global state may persist between invocations. (#310)
- Support `timedelta` arguments for workflow `sleep()` and retry delays. (#342)
- Expose unstable API to serve workflow HTTP endpoint from your own web framework. (#294)
- Added semi-internal manifest API for TS tools and e2e test. (#296)
Bug Fixes
---------
- Fix failing or even crashing cipher calls inside the workflow sandbox. (#305)
- Fail a workflow run with `HookConflictError` when another run already owns its hook token instead of leaving it running indefinitely. (#327)
- Support resuming hooks with payload in the queue message. (#300)
- Fix some bugs involving hooks arriving when the workflow was not yet blocked on them. (#339)
- Fixed nulls rejected by server, requiring Pydantic 2.12 or newer. (#321)
- Prevent workflows from having side effects while suspending. (#332)
- `hook.dispose()` will now work properly in a `finally` block. (That is, the hook will be disposed only when the workflow is actually terminating, and not every time it gets replayed.) (#332)
- More reliably fail runs whose replay diverges from the event log. (#347)
- Runs will now fail even in the case where the main thread of execution is not directly blocked on the suspension that is erroring. (#347)
- Fixed workflow and step calls with both positional-or-keyword parameters and `*args` failing during replay because their arguments were recorded in an unbindable shape. (#312)
Internal
--------
- Remove a just-added return from a finally block. (#344)
- Correct internal workflow type annotations found by checking untyped function bodies. (#337)
- Refactored event replay. (#341)
- Construct the protocol models by Python field name. (#322)
vercel
------
0.11.0 - 2026-08-31
-------------------
Features
--------
- Expose `get_deadline()` for reading the current Function invocation deadline. (#306)
- Answer workflow health checks for both queue-based transport and HTTP. (#292)
- Add support to read the sealed (`encp`) workflow payloads (X25519 + AES-GCM) an outside writer addresses to a run, under the `encryption` extra. (#297)
Bug Fixes
---------
- Remove upper bounds on aggregate Sandbox and Workflow dependencies so sibling releases cannot make the `vercel` package un-installable. (#334)
- Start a workflow run even when its queue message arrives before the `run_created` event has landed. (#284)
Internal
--------
- The Workflows implementation now ships in the separate `vercel-workflow` distribution, which `vercel` depends on, so `vercel.workflow` imports keep working without installing anything extra. (#299)
vercel-apscheduler
------------------
0.3.0 - 2026-08-31
------------------
Breaking Changes
----------------
- The managed Redis backend was removed. The integration now always runs on its managed job store (Vercel Runtime Cache); a configured default `RedisJobStore` is rejected at import, `VERCEL_APSCHEDULER_BACKEND` accepts only `cache`, and the `redis` dependency is gone. The scheduler's durable identity now always derives from the builder-assigned subscriber id (previously the Redis `jobs_key`); the `scheduler_id` option still pins an identity explicitly. (#286)
vercel-celery
-------------
0.7.5 - 2026-08-31
------------------
- Update dependencies.
vercel-django-tasks
-------------------
0.7.0 - 2026-08-31
------------------
Features
--------
- Add a Vercel Queues backend for Django Tasks and use it by default when no task backends are configured. (#291)
vercel-dramatiq
---------------
0.7.4 - 2026-08-31
------------------
- Update dependencies.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@pranaygp@fantix
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
workflow: read spec 7 and skip sealed noops by pranaygp · Pull Request #319 · vercel/vercel-py · GitHub
Skip to content

workflow: read spec 7 and skip sealed noops - #319

Merged
fantix merged 4 commits into
mainfrom
pgp/port-noop-event-to-py
Aug 24, 2026
Merged

workflow: read spec 7 and skip sealed noops#319
fantix merged 4 commits into
mainfrom
pgp/port-noop-event-to-py

Conversation

@pranaygp

Copy link
Copy Markdown
Contributor

Python half of vercel/workflow#3634 (Add support for 'noop' event type — spec version 7).

Why this is urgent for us

That PR moves SPEC_VERSION_CURRENT and SPEC_VERSION_MAX_SUPPORTED to 7 and switches both @workflow/world-local and @workflow/world-vercel to mintedSpecVersion(), which returns 7 by default. Our reader ceiling was 6, and it is a pydantic le= on BaseEvent.specVersion — so a spec-7 row does not degrade, it fails validation, and with it the whole page. The e2e-python conformance lane runs the TypeScript driver against workbench/python over a shared world-local data dir, so it is the first thing that breaks: the driver's run_created is now labelled 7 and the Python app cannot parse the log it is supposed to replay.

What spec 7 actually asks of a client

On the server, a spec-7 run's slot positions come from a per-run counter handed out before the write that fills them, so concurrent writers never race for a position. The price is that a writer that takes a position and dies leaves a hole, and the backend's read path fills provably-abandoned holes with a server-written noop so returned pages stay dense prefixes.

So the client contract is one thing, and it is read-only: a reader must know a noop occupies its slot and means nothing.

There is no writer half for us. A World only owes the sealing half if it pre-assigns positions ahead of the commit; our local world allocates each id at the commit that fills it, so it has no holes to seal and will never emit one.

The change

world.py

  • SPEC_VERSION_SUPPORTS_SEALED_LOG = 7, and SPEC_VERSION_MAX_SUPPORTED follows it. SPEC_VERSION_CURRENT stays 2 — we still stamp what we always stamped, and a run created elsewhere keeps its creator's version.
  • NoopEvent / NoopEventData, in the read union and deliberately not in CreateEventRequest. eventData is optional and open (extra="allow", matching the .passthrough() on the TS schema): nothing reads it, the shape belongs to whichever backend sealed the slot, and a reader whose only interest is skipping the row has no business rejecting a field it has not heard of.
  • is_sealed_noop_event(), mirroring isSealedNoopEvent — one home for the test.

runtime.pyget_all_workflow_run_events() drops seals, so the log the replay walks is the log without them.

That is the whole skip, and the reason it is one line at the loader rather than a continue in resume() is the clock. Everything downstream of that function is reconstructing what the run did — matching correlation ids to suspensions, looking for a terminal event, deciding which waits elapsed, and reading createdAt for now(). Nothing on this side walks positions, so there is no caller that wants the seal. Meanwhile a seal's createdAt is the sealer's wall clock, which can postdate every real event around it; now() dates the run from the last event the replay consumed, so a seal left in the list hands the body a time no event of the run happened at — and then, because the clock only moves forward, every later now() too. Dropping it at the boundary makes the equivalence the contract is about (a sealed log replays exactly like the same log whose holes their own writers filled) hold by construction, rather than by three call sites remembering. The cursor is untouched: it is the World's, and still points past every row that was read.

Tests

tests/unit/test_workflow_sealed_log_noop.py, 12 tests in four parts:

  • the row — parses out of the read union with eventData, without it, and with an eventData field we have never heard of; the predicate answers for seals only; NoopEvent is in Event and absent from CreateEventRequest; 7 is the ceiling and 8 is still rejected.
  • storage — a seal round-trips through the local world and numbering continues past it, mirroring slot-identity.test.ts's stores, lists, and numbers past a noop event.
  • the loader — seals at head, middle (consecutive) and tail are dropped with the order of everything else intact; a page of nothing but seals still pages on; the cursor survives.
  • replay — the real claim, driven end to end through workflow_handler: one recorded two-sleep run, twice, differing only in seals at the head, in the middle (including one splitting a wait_created/wait_completed pair), and at the tail. Same output, same events written back, and the three now() readings pinned literally so a change that broke both logs the same way cannot slip past the comparison. Plus the degenerate all-seals-before-the-run shape, where the first now() has to find the first real event.

Every seal in the file is stamped a minute into the future, so each of those clock assertions would pass by accident if a seal's createdAt were plausible. Reverting the one-line filter fails 5 of the 12.

test_workflow_local_world_format.py's two spec-version tests move 6 → 7; the ceiling assertion now derives its match from SPEC_VERSION_MAX_SUPPORTED instead of hardcoding the number, so the next bump only has to touch the constant.

Full suite: 1127 tests, 0 failures (test_the_api_token_is_config_then_env_then_oidc fails locally on a box with VERCEL_TOKEN exported, before and after this change). Lint and typecheck clean repo-wide.

Follow-up, not in this PR

workbench/python/pyproject.toml in vercel/workflow resolves vercel-workflow from PyPI (0.9.0 today) regardless of its [tool.uv.sources] rev, so the e2e-python lane only picks this up once it is released and the lock is refreshed there — or once that file git-pins vercel-workflow at a commit containing it, which is what the comment block above that source entry describes.

🤖 Generated with Claude Code

`@workflow/world-local` and `@workflow/world-vercel` both stamp
specVersion 7 now (vercel/workflow#3634), so every event a TypeScript
driver writes arrives labelled 7 and our reader ceiling of 6 rejected
the whole log -- including on the local world the Python e2e lane runs
on. Raise the ceiling, and add the one thing the version gates: a
`noop` row, written by a World's backend to occupy a slot whose writer
allocated the position and then died.
A noop parses out of the read union but is dropped from the log the
replay walks. Its `createdAt` is the sealer's wall clock, which can
postdate every real event around it, so letting it reach the
deterministic clock would make a log whose hole was sealed replay
differently from the same log whose hole its own writer filled.
Nothing here writes one: we allocate each event id at the commit that
fills it, so we have no holes to seal, and `noop` stays out of the
create union.
Co-Authored-By: Pranay Prakash <1797812+pranaygp@users.noreply.github.com>

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@fantixfantix left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM; let me trim off some AI in-code chats and merge this to unbreak the e2e test.

@fantix
fantixforce-pushed the pgp/port-noop-event-to-py branch from 3116c4e to abb6ce9CompareAugust 24, 2026 18:44
@fantix
fantix merged commit a5969aa into mainAug 24, 2026
14 checks passed
@fantix
fantix deleted the pgp/port-noop-event-to-py branch August 24, 2026 18:49
@scotttrinhscotttrinh mentioned this pull request Aug 26, 2026
scotttrinh added a commit that referenced this pull request Aug 26, 2026
vercel-internal-core
--------------------
0.1.3 - 2026-08-26
------------------
Internal
--------
- Support disabling HTTP timeouts for selected SDK operations while preserving the client default elsewhere. (#307)
vercel-connect
--------------
0.1.1 - 2026-08-26
------------------
- Update dependencies.
vercel-queue
------------
0.8.1 - 2026-08-26
------------------
Documentation
-------------
- Remove documentation and examples for `asgi_app` in preparation for its removal. (#309)
vercel-sandbox
--------------
0.5.0 - 2026-08-26
------------------
Features
--------
- Add sync and async `fork_sandbox(...)` support for creating a sandbox from an existing named sandbox with optional configuration overrides. (#257)
- Add `region` and `failover_regions` configuration for sandbox creation, forks, and updates, plus multi-region snapshot availability reporting. (#308)
Bug Fixes
---------
- Allow Sandbox process waits and log streams to remain idle longer than the session HTTP timeout. (#307)
vercel-workflow
---------------
0.10.0 - 2026-08-26
-------------------
Breaking Changes
----------------
- Use type annotations on workflows and step to allow passing Pydantic models and dataclasses. (#317)
- This is a breaking change, because type annotations will now be enforced. Passing a `dict` when the declaration expects a `list` will fail. (#317)
- Pydantic models and dataclasses can no longer be passed to `@serializable` or `register_serializable()`. Annotate the workflow or step parameter or return value with their type instead. (#317)
Features
--------
- `get_workflow_metadata()` returns the current run's `WorkflowInfo` (run id, workflow name, start time, deployment URL, and feature flags), callable from a workflow body or a step body — mirroring the JS SDK's `getWorkflowMetadata()`. (#320)
- One current limitation is that `started_at` is `None` from inside a step. (#320)
- `BaseHook.wait()` accepts `metadata` to record on the hook, and `get_hook_by_token()` reads it back for a resumer. (#301)
- A step can raise `RetryableError` to control when its next attempt runs. (#302)
- Accept `specVersion` 7 sealed noop event logs. (#319)
- A workflow or step can attach plaintext metadata to its run with `set_attributes()`. (#303)
- Add a `share_sandboxes` parameter to `SandboxPolicy` to enable reusing already created sandboxes instead of creating a new one on each invocation. This speeds up workflows but means that modifications to global state may persist between invocations. (#310)
- Expose unstable API to serve workflow HTTP endpoint from your own web framework. (#294)
- Added semi-internal manifest API for TS tools and e2e test. (#296)
Bug Fixes
---------
- Fix failing or even crashing cipher calls inside the workflow sandbox. (#305)
- Support resuming hooks with payload in the queue message. (#300)
- Fixed nulls rejected by server, requiring Pydantic 2.12 or newer. (#321)
- Fixed workflow and step calls with both positional-or-keyword parameters and `*args` failing during replay because their arguments were recorded in an unbindable shape. (#312)
Internal
--------
- Construct the protocol models by Python field name. (#322)
vercel
------
0.11.0 - 2026-08-26
-------------------
Features
--------
- Expose `get_deadline()` for reading the current Function invocation deadline. (#306)
- Answer workflow health checks for both queue-based transport and HTTP. (#292)
- Add support to read the sealed (`encp`) workflow payloads (X25519 + AES-GCM) an outside writer addresses to a run, under the `encryption` extra. (#297)
Bug Fixes
---------
- Remove upper bounds on aggregate Sandbox and Workflow dependencies so sibling releases cannot make the `vercel` package un-installable. (#334)
- Start a workflow run even when its queue message arrives before the `run_created` event has landed. (#284)
Internal
--------
- The Workflows implementation now ships in the separate `vercel-workflow` distribution, which `vercel` depends on, so `vercel.workflow` imports keep working without installing anything extra. (#299)
vercel-apscheduler
------------------
0.3.0 - 2026-08-26
------------------
Breaking Changes
----------------
- The managed Redis backend was removed. The integration now always runs on its managed job store (Vercel Runtime Cache); a configured default `RedisJobStore` is rejected at import, `VERCEL_APSCHEDULER_BACKEND` accepts only `cache`, and the `redis` dependency is gone. The scheduler's durable identity now always derives from the builder-assigned subscriber id (previously the Redis `jobs_key`); the `scheduler_id` option still pins an identity explicitly. (#286)
vercel-celery
-------------
0.7.5 - 2026-08-26
------------------
- Update dependencies.
vercel-django-tasks
-------------------
0.7.0 - 2026-08-26
------------------
Features
--------
- Add a Vercel Queues backend for Django Tasks and use it by default when no task backends are configured. (#291)
vercel-dramatiq
---------------
0.7.4 - 2026-08-26
------------------
- Update dependencies.
scotttrinh added a commit that referenced this pull request Aug 31, 2026
vercel-headers
--------------
0.7.2 - 2026-08-31
------------------
Bug Fixes
---------
- Accept request objects with concrete header implementations in the IP address and geolocation type annotations. (#337)
vercel-internal-core
--------------------
0.1.3 - 2026-08-31
------------------
Internal
--------
- Support disabling HTTP timeouts for selected SDK operations while preserving the client default elsewhere. (#307)
vercel-oidc
-----------
0.8.1 - 2026-08-31
------------------
- Update dependencies.
vercel-connect
--------------
0.1.1 - 2026-08-31
------------------
- Update dependencies.
vercel-internal-telemetry
-------------------------
0.7.3 - 2026-08-31
------------------
- Update dependencies.
vercel-queue
------------
0.8.1 - 2026-08-31
------------------
Bug Fixes
---------
- Force embedded development servers to exit when graceful shutdown stalls. (#351)
Documentation
-------------
- Remove documentation and examples for `asgi_app` in preparation for its removal. (#309)
vercel-sandbox
--------------
0.5.0 - 2026-08-31
------------------
Features
--------
- Add sync and async `fork_sandbox(...)` support for creating a sandbox from an existing named sandbox with optional configuration overrides. (#257)
- Add `region` and `failover_regions` configuration for sandbox creation, forks, and updates, plus multi-region snapshot availability reporting. (#308)
- Forward private ``__``-prefixed parameters to the Sandbox API. (#350)
Bug Fixes
---------
- Allow Sandbox process waits and log streams to remain idle longer than the session HTTP timeout. (#307)
- Expose Linux process signals consistently on every SDK host platform. (#352)
vercel-cache
------------
0.7.3 - 2026-08-31
------------------
- Update dependencies.
vercel-workflow
---------------
0.10.0 - 2026-08-31
-------------------
Breaking Changes
----------------
- Make sleep() and retry delays treat numbers as seconds, not ms (#346)
- This matches Python standard library APIs. (#346)
- Use type annotations on workflows and step to allow passing Pydantic models and dataclasses. (#317)
- This is a breaking change, because type annotations will now be enforced. Passing a `dict` when the declaration expects a `list` will fail. (#317)
- Pydantic models and dataclasses can no longer be passed to `@serializable` or `register_serializable()`. Annotate the workflow or step parameter or return value with their type instead. (#317)
Features
--------
- Support `call_later`, `call_at`, and `now` in the event loop implementation. (#343)
- This enables use of `asyncio.sleep()` as well as `asyncio.timeout` and the `timeout` parameter of `asyncio.wait_for`. (#343)
- `get_workflow_metadata()` returns the current run's `WorkflowInfo` (run id, workflow name, start time, deployment URL, and feature flags), callable from a workflow body or a step body — mirroring the JS SDK's `getWorkflowMetadata()`. (#320)
- One current limitation is that `started_at` is `None` from inside a step. (#320)
- `BaseHook.wait()` accepts `metadata` to record on the hook, and `get_hook_by_token()` reads it back for a resumer. (#301)
- A step can raise `RetryableError` to control when its next attempt runs. (#302)
- Accept `specVersion` 7 sealed noop event logs. (#319)
- Failed run and step events now preserve serialized error classes, messages, stacks, and causes. Failed runs also expose a plaintext `errorCode`. (#304)
- A workflow or step can attach plaintext metadata to its run with `set_attributes()`. (#303)
- Add a `share_sandboxes` parameter to `SandboxPolicy` to enable reusing already created sandboxes instead of creating a new one on each invocation. This speeds up workflows but means that modifications to global state may persist between invocations. (#310)
- Support `timedelta` arguments for workflow `sleep()` and retry delays. (#342)
- Expose unstable API to serve workflow HTTP endpoint from your own web framework. (#294)
- Added semi-internal manifest API for TS tools and e2e test. (#296)
Bug Fixes
---------
- Fix failing or even crashing cipher calls inside the workflow sandbox. (#305)
- Fail a workflow run with `HookConflictError` when another run already owns its hook token instead of leaving it running indefinitely. (#327)
- Support resuming hooks with payload in the queue message. (#300)
- Fix some bugs involving hooks arriving when the workflow was not yet blocked on them. (#339)
- Fixed nulls rejected by server, requiring Pydantic 2.12 or newer. (#321)
- Prevent workflows from having side effects while suspending. (#332)
- `hook.dispose()` will now work properly in a `finally` block. (That is, the hook will be disposed only when the workflow is actually terminating, and not every time it gets replayed.) (#332)
- More reliably fail runs whose replay diverges from the event log. (#347)
- Runs will now fail even in the case where the main thread of execution is not directly blocked on the suspension that is erroring. (#347)
- Fixed workflow and step calls with both positional-or-keyword parameters and `*args` failing during replay because their arguments were recorded in an unbindable shape. (#312)
Internal
--------
- Remove a just-added return from a finally block. (#344)
- Correct internal workflow type annotations found by checking untyped function bodies. (#337)
- Refactored event replay. (#341)
- Construct the protocol models by Python field name. (#322)
vercel
------
0.11.0 - 2026-08-31
-------------------
Features
--------
- Expose `get_deadline()` for reading the current Function invocation deadline. (#306)
- Answer workflow health checks for both queue-based transport and HTTP. (#292)
- Add support to read the sealed (`encp`) workflow payloads (X25519 + AES-GCM) an outside writer addresses to a run, under the `encryption` extra. (#297)
Bug Fixes
---------
- Remove upper bounds on aggregate Sandbox and Workflow dependencies so sibling releases cannot make the `vercel` package un-installable. (#334)
- Start a workflow run even when its queue message arrives before the `run_created` event has landed. (#284)
Internal
--------
- The Workflows implementation now ships in the separate `vercel-workflow` distribution, which `vercel` depends on, so `vercel.workflow` imports keep working without installing anything extra. (#299)
vercel-apscheduler
------------------
0.3.0 - 2026-08-31
------------------
Breaking Changes
----------------
- The managed Redis backend was removed. The integration now always runs on its managed job store (Vercel Runtime Cache); a configured default `RedisJobStore` is rejected at import, `VERCEL_APSCHEDULER_BACKEND` accepts only `cache`, and the `redis` dependency is gone. The scheduler's durable identity now always derives from the builder-assigned subscriber id (previously the Redis `jobs_key`); the `scheduler_id` option still pins an identity explicitly. (#286)
vercel-celery
-------------
0.7.5 - 2026-08-31
------------------
- Update dependencies.
vercel-django-tasks
-------------------
0.7.0 - 2026-08-31
------------------
Features
--------
- Add a Vercel Queues backend for Django Tasks and use it by default when no task backends are configured. (#291)
vercel-dramatiq
---------------
0.7.4 - 2026-08-31
------------------
- Update dependencies.
scotttrinh added a commit that referenced this pull request Aug 31, 2026
vercel-headers
--------------
0.7.2 - 2026-08-31
------------------
Bug Fixes
---------
- Accept request objects with concrete header implementations in the IP address and geolocation type annotations. (#337)
vercel-internal-core
--------------------
0.1.3 - 2026-08-31
------------------
Internal
--------
- Support disabling HTTP timeouts for selected SDK operations while preserving the client default elsewhere. (#307)
vercel-oidc
-----------
0.8.1 - 2026-08-31
------------------
- Update dependencies.
vercel-connect
--------------
0.1.1 - 2026-08-31
------------------
- Update dependencies.
vercel-internal-telemetry
-------------------------
0.7.3 - 2026-08-31
------------------
- Update dependencies.
vercel-queue
------------
0.8.1 - 2026-08-31
------------------
Bug Fixes
---------
- Force embedded development servers to exit when graceful shutdown stalls. (#351)
Documentation
-------------
- Remove documentation and examples for `asgi_app` in preparation for its removal. (#309)
vercel-sandbox
--------------
0.5.0 - 2026-08-31
------------------
Features
--------
- Add sync and async `fork_sandbox(...)` support for creating a sandbox from an existing named sandbox with optional configuration overrides. (#257)
- Add `region` and `failover_regions` configuration for sandbox creation, forks, and updates, plus multi-region snapshot availability reporting. (#308)
- Forward private ``__``-prefixed parameters to the Sandbox API. (#350)
Bug Fixes
---------
- Allow Sandbox process waits and log streams to remain idle longer than the session HTTP timeout. (#307)
- Expose Linux process signals consistently on every SDK host platform. (#352)
vercel-cache
------------
0.7.3 - 2026-08-31
------------------
- Update dependencies.
vercel-workflow
---------------
0.10.0 - 2026-08-31
-------------------
Breaking Changes
----------------
- Make `await hook` never return `None`
- Raises a new `HookDisposedError` instead of returning `None` when the hook has been disposed. It is now typed to return `T` instead of `T | None`. `async for` over a hook will stop iterating on disposal, still.
- Make sleep() and retry delays treat numbers as seconds, not ms (#346)
- This matches Python standard library APIs. (#346)
- Use type annotations on workflows and step to allow passing Pydantic models and dataclasses. (#317)
- This is a breaking change, because type annotations will now be enforced. Passing a `dict` when the declaration expects a `list` will fail. (#317)
- Pydantic models and dataclasses can no longer be passed to `@serializable` or `register_serializable()`. Annotate the workflow or step parameter or return value with their type instead. (#317)
Features
--------
- Support `call_later`, `call_at`, and `now` in the event loop implementation. (#343)
- This enables use of `asyncio.sleep()` as well as `asyncio.timeout` and the `timeout` parameter of `asyncio.wait_for`. (#343)
- `get_workflow_metadata()` returns the current run's `WorkflowInfo` (run id, workflow name, start time, deployment URL, and feature flags), callable from a workflow body or a step body — mirroring the JS SDK's `getWorkflowMetadata()`. (#320)
- One current limitation is that `started_at` is `None` from inside a step. (#320)
- Make `HookEvent` an async context manager
- This matches TS, which supports `using`. ``` # disposes the hook on block exit async with SomeHook.wait(...) as hook: res = await hook ```
- `BaseHook.wait()` accepts `metadata` to record on the hook, and `get_hook_by_token()` reads it back for a resumer. (#301)
- A step can raise `RetryableError` to control when its next attempt runs. (#302)
- Accept `specVersion` 7 sealed noop event logs. (#319)
- Failed run and step events now preserve serialized error classes, messages, stacks, and causes. Failed runs also expose a plaintext `errorCode`. (#304)
- A workflow or step can attach plaintext metadata to its run with `set_attributes()`. (#303)
- Add a `share_sandboxes` parameter to `SandboxPolicy` to enable reusing already created sandboxes instead of creating a new one on each invocation. This speeds up workflows but means that modifications to global state may persist between invocations. (#310)
- Support `timedelta` arguments for workflow `sleep()` and retry delays. (#342)
- Expose unstable API to serve workflow HTTP endpoint from your own web framework. (#294)
- Added semi-internal manifest API for TS tools and e2e test. (#296)
Bug Fixes
---------
- Fix failing or even crashing cipher calls inside the workflow sandbox. (#305)
- Fail a workflow run with `HookConflictError` when another run already owns its hook token instead of leaving it running indefinitely. (#327)
- Support resuming hooks with payload in the queue message. (#300)
- Fix some bugs involving hooks arriving when the workflow was not yet blocked on them. (#339)
- Fixed nulls rejected by server, requiring Pydantic 2.12 or newer. (#321)
- Prevent workflows from having side effects while suspending. (#332)
- `hook.dispose()` will now work properly in a `finally` block. (That is, the hook will be disposed only when the workflow is actually terminating, and not every time it gets replayed.) (#332)
- More reliably fail runs whose replay diverges from the event log. (#347)
- Runs will now fail even in the case where the main thread of execution is not directly blocked on the suspension that is erroring. (#347)
- Fixed workflow and step calls with both positional-or-keyword parameters and `*args` failing during replay because their arguments were recorded in an unbindable shape. (#312)
Internal
--------
- Remove a just-added return from a finally block. (#344)
- Correct internal workflow type annotations found by checking untyped function bodies. (#337)
- Refactored event replay. (#341)
- Construct the protocol models by Python field name. (#322)
vercel
------
0.11.0 - 2026-08-31
-------------------
Features
--------
- Expose `get_deadline()` for reading the current Function invocation deadline. (#306)
- Answer workflow health checks for both queue-based transport and HTTP. (#292)
- Add support to read the sealed (`encp`) workflow payloads (X25519 + AES-GCM) an outside writer addresses to a run, under the `encryption` extra. (#297)
Bug Fixes
---------
- Remove upper bounds on aggregate Sandbox and Workflow dependencies so sibling releases cannot make the `vercel` package un-installable. (#334)
- Start a workflow run even when its queue message arrives before the `run_created` event has landed. (#284)
Internal
--------
- The Workflows implementation now ships in the separate `vercel-workflow` distribution, which `vercel` depends on, so `vercel.workflow` imports keep working without installing anything extra. (#299)
vercel-apscheduler
------------------
0.3.0 - 2026-08-31
------------------
Breaking Changes
----------------
- The managed Redis backend was removed. The integration now always runs on its managed job store (Vercel Runtime Cache); a configured default `RedisJobStore` is rejected at import, `VERCEL_APSCHEDULER_BACKEND` accepts only `cache`, and the `redis` dependency is gone. The scheduler's durable identity now always derives from the builder-assigned subscriber id (previously the Redis `jobs_key`); the `scheduler_id` option still pins an identity explicitly. (#286)
vercel-celery
-------------
0.7.5 - 2026-08-31
------------------
- Update dependencies.
vercel-django-tasks
-------------------
0.7.0 - 2026-08-31
------------------
Features
--------
- Add a Vercel Queues backend for Django Tasks and use it by default when no task backends are configured. (#291)
vercel-dramatiq
---------------
0.7.4 - 2026-08-31
------------------
- Update dependencies.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@pranaygp@fantix
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' workflow: read spec 7 and skip sealed noops by pranaygp · Pull Request #319 · vercel/vercel-py · GitHub
Skip to content

workflow: read spec 7 and skip sealed noops - #319

Merged
fantix merged 4 commits into
mainfrom
pgp/port-noop-event-to-py
Aug 24, 2026
Merged

workflow: read spec 7 and skip sealed noops#319
fantix merged 4 commits into
mainfrom
pgp/port-noop-event-to-py

Conversation

@pranaygp

Copy link
Copy Markdown
Contributor

Python half of vercel/workflow#3634 (Add support for 'noop' event type — spec version 7).

Why this is urgent for us

That PR moves SPEC_VERSION_CURRENT and SPEC_VERSION_MAX_SUPPORTED to 7 and switches both @workflow/world-local and @workflow/world-vercel to mintedSpecVersion(), which returns 7 by default. Our reader ceiling was 6, and it is a pydantic le= on BaseEvent.specVersion — so a spec-7 row does not degrade, it fails validation, and with it the whole page. The e2e-python conformance lane runs the TypeScript driver against workbench/python over a shared world-local data dir, so it is the first thing that breaks: the driver's run_created is now labelled 7 and the Python app cannot parse the log it is supposed to replay.

What spec 7 actually asks of a client

On the server, a spec-7 run's slot positions come from a per-run counter handed out before the write that fills them, so concurrent writers never race for a position. The price is that a writer that takes a position and dies leaves a hole, and the backend's read path fills provably-abandoned holes with a server-written noop so returned pages stay dense prefixes.

So the client contract is one thing, and it is read-only: a reader must know a noop occupies its slot and means nothing.

There is no writer half for us. A World only owes the sealing half if it pre-assigns positions ahead of the commit; our local world allocates each id at the commit that fills it, so it has no holes to seal and will never emit one.

The change

world.py

  • SPEC_VERSION_SUPPORTS_SEALED_LOG = 7, and SPEC_VERSION_MAX_SUPPORTED follows it. SPEC_VERSION_CURRENT stays 2 — we still stamp what we always stamped, and a run created elsewhere keeps its creator's version.
  • NoopEvent / NoopEventData, in the read union and deliberately not in CreateEventRequest. eventData is optional and open (extra="allow", matching the .passthrough() on the TS schema): nothing reads it, the shape belongs to whichever backend sealed the slot, and a reader whose only interest is skipping the row has no business rejecting a field it has not heard of.
  • is_sealed_noop_event(), mirroring isSealedNoopEvent — one home for the test.

runtime.pyget_all_workflow_run_events() drops seals, so the log the replay walks is the log without them.

That is the whole skip, and the reason it is one line at the loader rather than a continue in resume() is the clock. Everything downstream of that function is reconstructing what the run did — matching correlation ids to suspensions, looking for a terminal event, deciding which waits elapsed, and reading createdAt for now(). Nothing on this side walks positions, so there is no caller that wants the seal. Meanwhile a seal's createdAt is the sealer's wall clock, which can postdate every real event around it; now() dates the run from the last event the replay consumed, so a seal left in the list hands the body a time no event of the run happened at — and then, because the clock only moves forward, every later now() too. Dropping it at the boundary makes the equivalence the contract is about (a sealed log replays exactly like the same log whose holes their own writers filled) hold by construction, rather than by three call sites remembering. The cursor is untouched: it is the World's, and still points past every row that was read.

Tests

tests/unit/test_workflow_sealed_log_noop.py, 12 tests in four parts:

  • the row — parses out of the read union with eventData, without it, and with an eventData field we have never heard of; the predicate answers for seals only; NoopEvent is in Event and absent from CreateEventRequest; 7 is the ceiling and 8 is still rejected.
  • storage — a seal round-trips through the local world and numbering continues past it, mirroring slot-identity.test.ts's stores, lists, and numbers past a noop event.
  • the loader — seals at head, middle (consecutive) and tail are dropped with the order of everything else intact; a page of nothing but seals still pages on; the cursor survives.
  • replay — the real claim, driven end to end through workflow_handler: one recorded two-sleep run, twice, differing only in seals at the head, in the middle (including one splitting a wait_created/wait_completed pair), and at the tail. Same output, same events written back, and the three now() readings pinned literally so a change that broke both logs the same way cannot slip past the comparison. Plus the degenerate all-seals-before-the-run shape, where the first now() has to find the first real event.

Every seal in the file is stamped a minute into the future, so each of those clock assertions would pass by accident if a seal's createdAt were plausible. Reverting the one-line filter fails 5 of the 12.

test_workflow_local_world_format.py's two spec-version tests move 6 → 7; the ceiling assertion now derives its match from SPEC_VERSION_MAX_SUPPORTED instead of hardcoding the number, so the next bump only has to touch the constant.

Full suite: 1127 tests, 0 failures (test_the_api_token_is_config_then_env_then_oidc fails locally on a box with VERCEL_TOKEN exported, before and after this change). Lint and typecheck clean repo-wide.

Follow-up, not in this PR

workbench/python/pyproject.toml in vercel/workflow resolves vercel-workflow from PyPI (0.9.0 today) regardless of its [tool.uv.sources] rev, so the e2e-python lane only picks this up once it is released and the lock is refreshed there — or once that file git-pins vercel-workflow at a commit containing it, which is what the comment block above that source entry describes.

🤖 Generated with Claude Code

`@workflow/world-local` and `@workflow/world-vercel` both stamp
specVersion 7 now (vercel/workflow#3634), so every event a TypeScript
driver writes arrives labelled 7 and our reader ceiling of 6 rejected
the whole log -- including on the local world the Python e2e lane runs
on. Raise the ceiling, and add the one thing the version gates: a
`noop` row, written by a World's backend to occupy a slot whose writer
allocated the position and then died.
A noop parses out of the read union but is dropped from the log the
replay walks. Its `createdAt` is the sealer's wall clock, which can
postdate every real event around it, so letting it reach the
deterministic clock would make a log whose hole was sealed replay
differently from the same log whose hole its own writer filled.
Nothing here writes one: we allocate each event id at the commit that
fills it, so we have no holes to seal, and `noop` stays out of the
create union.
Co-Authored-By: Pranay Prakash <1797812+pranaygp@users.noreply.github.com>

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@fantixfantix left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM; let me trim off some AI in-code chats and merge this to unbreak the e2e test.

@fantix
fantixforce-pushed the pgp/port-noop-event-to-py branch from 3116c4e to abb6ce9CompareAugust 24, 2026 18:44
@fantix
fantix merged commit a5969aa into mainAug 24, 2026
14 checks passed
@fantix
fantix deleted the pgp/port-noop-event-to-py branch August 24, 2026 18:49
@scotttrinhscotttrinh mentioned this pull request Aug 26, 2026
scotttrinh added a commit that referenced this pull request Aug 26, 2026
vercel-internal-core
--------------------
0.1.3 - 2026-08-26
------------------
Internal
--------
- Support disabling HTTP timeouts for selected SDK operations while preserving the client default elsewhere. (#307)
vercel-connect
--------------
0.1.1 - 2026-08-26
------------------
- Update dependencies.
vercel-queue
------------
0.8.1 - 2026-08-26
------------------
Documentation
-------------
- Remove documentation and examples for `asgi_app` in preparation for its removal. (#309)
vercel-sandbox
--------------
0.5.0 - 2026-08-26
------------------
Features
--------
- Add sync and async `fork_sandbox(...)` support for creating a sandbox from an existing named sandbox with optional configuration overrides. (#257)
- Add `region` and `failover_regions` configuration for sandbox creation, forks, and updates, plus multi-region snapshot availability reporting. (#308)
Bug Fixes
---------
- Allow Sandbox process waits and log streams to remain idle longer than the session HTTP timeout. (#307)
vercel-workflow
---------------
0.10.0 - 2026-08-26
-------------------
Breaking Changes
----------------
- Use type annotations on workflows and step to allow passing Pydantic models and dataclasses. (#317)
- This is a breaking change, because type annotations will now be enforced. Passing a `dict` when the declaration expects a `list` will fail. (#317)
- Pydantic models and dataclasses can no longer be passed to `@serializable` or `register_serializable()`. Annotate the workflow or step parameter or return value with their type instead. (#317)
Features
--------
- `get_workflow_metadata()` returns the current run's `WorkflowInfo` (run id, workflow name, start time, deployment URL, and feature flags), callable from a workflow body or a step body — mirroring the JS SDK's `getWorkflowMetadata()`. (#320)
- One current limitation is that `started_at` is `None` from inside a step. (#320)
- `BaseHook.wait()` accepts `metadata` to record on the hook, and `get_hook_by_token()` reads it back for a resumer. (#301)
- A step can raise `RetryableError` to control when its next attempt runs. (#302)
- Accept `specVersion` 7 sealed noop event logs. (#319)
- A workflow or step can attach plaintext metadata to its run with `set_attributes()`. (#303)
- Add a `share_sandboxes` parameter to `SandboxPolicy` to enable reusing already created sandboxes instead of creating a new one on each invocation. This speeds up workflows but means that modifications to global state may persist between invocations. (#310)
- Expose unstable API to serve workflow HTTP endpoint from your own web framework. (#294)
- Added semi-internal manifest API for TS tools and e2e test. (#296)
Bug Fixes
---------
- Fix failing or even crashing cipher calls inside the workflow sandbox. (#305)
- Support resuming hooks with payload in the queue message. (#300)
- Fixed nulls rejected by server, requiring Pydantic 2.12 or newer. (#321)
- Fixed workflow and step calls with both positional-or-keyword parameters and `*args` failing during replay because their arguments were recorded in an unbindable shape. (#312)
Internal
--------
- Construct the protocol models by Python field name. (#322)
vercel
------
0.11.0 - 2026-08-26
-------------------
Features
--------
- Expose `get_deadline()` for reading the current Function invocation deadline. (#306)
- Answer workflow health checks for both queue-based transport and HTTP. (#292)
- Add support to read the sealed (`encp`) workflow payloads (X25519 + AES-GCM) an outside writer addresses to a run, under the `encryption` extra. (#297)
Bug Fixes
---------
- Remove upper bounds on aggregate Sandbox and Workflow dependencies so sibling releases cannot make the `vercel` package un-installable. (#334)
- Start a workflow run even when its queue message arrives before the `run_created` event has landed. (#284)
Internal
--------
- The Workflows implementation now ships in the separate `vercel-workflow` distribution, which `vercel` depends on, so `vercel.workflow` imports keep working without installing anything extra. (#299)
vercel-apscheduler
------------------
0.3.0 - 2026-08-26
------------------
Breaking Changes
----------------
- The managed Redis backend was removed. The integration now always runs on its managed job store (Vercel Runtime Cache); a configured default `RedisJobStore` is rejected at import, `VERCEL_APSCHEDULER_BACKEND` accepts only `cache`, and the `redis` dependency is gone. The scheduler's durable identity now always derives from the builder-assigned subscriber id (previously the Redis `jobs_key`); the `scheduler_id` option still pins an identity explicitly. (#286)
vercel-celery
-------------
0.7.5 - 2026-08-26
------------------
- Update dependencies.
vercel-django-tasks
-------------------
0.7.0 - 2026-08-26
------------------
Features
--------
- Add a Vercel Queues backend for Django Tasks and use it by default when no task backends are configured. (#291)
vercel-dramatiq
---------------
0.7.4 - 2026-08-26
------------------
- Update dependencies.
scotttrinh added a commit that referenced this pull request Aug 31, 2026
vercel-headers
--------------
0.7.2 - 2026-08-31
------------------
Bug Fixes
---------
- Accept request objects with concrete header implementations in the IP address and geolocation type annotations. (#337)
vercel-internal-core
--------------------
0.1.3 - 2026-08-31
------------------
Internal
--------
- Support disabling HTTP timeouts for selected SDK operations while preserving the client default elsewhere. (#307)
vercel-oidc
-----------
0.8.1 - 2026-08-31
------------------
- Update dependencies.
vercel-connect
--------------
0.1.1 - 2026-08-31
------------------
- Update dependencies.
vercel-internal-telemetry
-------------------------
0.7.3 - 2026-08-31
------------------
- Update dependencies.
vercel-queue
------------
0.8.1 - 2026-08-31
------------------
Bug Fixes
---------
- Force embedded development servers to exit when graceful shutdown stalls. (#351)
Documentation
-------------
- Remove documentation and examples for `asgi_app` in preparation for its removal. (#309)
vercel-sandbox
--------------
0.5.0 - 2026-08-31
------------------
Features
--------
- Add sync and async `fork_sandbox(...)` support for creating a sandbox from an existing named sandbox with optional configuration overrides. (#257)
- Add `region` and `failover_regions` configuration for sandbox creation, forks, and updates, plus multi-region snapshot availability reporting. (#308)
- Forward private ``__``-prefixed parameters to the Sandbox API. (#350)
Bug Fixes
---------
- Allow Sandbox process waits and log streams to remain idle longer than the session HTTP timeout. (#307)
- Expose Linux process signals consistently on every SDK host platform. (#352)
vercel-cache
------------
0.7.3 - 2026-08-31
------------------
- Update dependencies.
vercel-workflow
---------------
0.10.0 - 2026-08-31
-------------------
Breaking Changes
----------------
- Make sleep() and retry delays treat numbers as seconds, not ms (#346)
- This matches Python standard library APIs. (#346)
- Use type annotations on workflows and step to allow passing Pydantic models and dataclasses. (#317)
- This is a breaking change, because type annotations will now be enforced. Passing a `dict` when the declaration expects a `list` will fail. (#317)
- Pydantic models and dataclasses can no longer be passed to `@serializable` or `register_serializable()`. Annotate the workflow or step parameter or return value with their type instead. (#317)
Features
--------
- Support `call_later`, `call_at`, and `now` in the event loop implementation. (#343)
- This enables use of `asyncio.sleep()` as well as `asyncio.timeout` and the `timeout` parameter of `asyncio.wait_for`. (#343)
- `get_workflow_metadata()` returns the current run's `WorkflowInfo` (run id, workflow name, start time, deployment URL, and feature flags), callable from a workflow body or a step body — mirroring the JS SDK's `getWorkflowMetadata()`. (#320)
- One current limitation is that `started_at` is `None` from inside a step. (#320)
- `BaseHook.wait()` accepts `metadata` to record on the hook, and `get_hook_by_token()` reads it back for a resumer. (#301)
- A step can raise `RetryableError` to control when its next attempt runs. (#302)
- Accept `specVersion` 7 sealed noop event logs. (#319)
- Failed run and step events now preserve serialized error classes, messages, stacks, and causes. Failed runs also expose a plaintext `errorCode`. (#304)
- A workflow or step can attach plaintext metadata to its run with `set_attributes()`. (#303)
- Add a `share_sandboxes` parameter to `SandboxPolicy` to enable reusing already created sandboxes instead of creating a new one on each invocation. This speeds up workflows but means that modifications to global state may persist between invocations. (#310)
- Support `timedelta` arguments for workflow `sleep()` and retry delays. (#342)
- Expose unstable API to serve workflow HTTP endpoint from your own web framework. (#294)
- Added semi-internal manifest API for TS tools and e2e test. (#296)
Bug Fixes
---------
- Fix failing or even crashing cipher calls inside the workflow sandbox. (#305)
- Fail a workflow run with `HookConflictError` when another run already owns its hook token instead of leaving it running indefinitely. (#327)
- Support resuming hooks with payload in the queue message. (#300)
- Fix some bugs involving hooks arriving when the workflow was not yet blocked on them. (#339)
- Fixed nulls rejected by server, requiring Pydantic 2.12 or newer. (#321)
- Prevent workflows from having side effects while suspending. (#332)
- `hook.dispose()` will now work properly in a `finally` block. (That is, the hook will be disposed only when the workflow is actually terminating, and not every time it gets replayed.) (#332)
- More reliably fail runs whose replay diverges from the event log. (#347)
- Runs will now fail even in the case where the main thread of execution is not directly blocked on the suspension that is erroring. (#347)
- Fixed workflow and step calls with both positional-or-keyword parameters and `*args` failing during replay because their arguments were recorded in an unbindable shape. (#312)
Internal
--------
- Remove a just-added return from a finally block. (#344)
- Correct internal workflow type annotations found by checking untyped function bodies. (#337)
- Refactored event replay. (#341)
- Construct the protocol models by Python field name. (#322)
vercel
------
0.11.0 - 2026-08-31
-------------------
Features
--------
- Expose `get_deadline()` for reading the current Function invocation deadline. (#306)
- Answer workflow health checks for both queue-based transport and HTTP. (#292)
- Add support to read the sealed (`encp`) workflow payloads (X25519 + AES-GCM) an outside writer addresses to a run, under the `encryption` extra. (#297)
Bug Fixes
---------
- Remove upper bounds on aggregate Sandbox and Workflow dependencies so sibling releases cannot make the `vercel` package un-installable. (#334)
- Start a workflow run even when its queue message arrives before the `run_created` event has landed. (#284)
Internal
--------
- The Workflows implementation now ships in the separate `vercel-workflow` distribution, which `vercel` depends on, so `vercel.workflow` imports keep working without installing anything extra. (#299)
vercel-apscheduler
------------------
0.3.0 - 2026-08-31
------------------
Breaking Changes
----------------
- The managed Redis backend was removed. The integration now always runs on its managed job store (Vercel Runtime Cache); a configured default `RedisJobStore` is rejected at import, `VERCEL_APSCHEDULER_BACKEND` accepts only `cache`, and the `redis` dependency is gone. The scheduler's durable identity now always derives from the builder-assigned subscriber id (previously the Redis `jobs_key`); the `scheduler_id` option still pins an identity explicitly. (#286)
vercel-celery
-------------
0.7.5 - 2026-08-31
------------------
- Update dependencies.
vercel-django-tasks
-------------------
0.7.0 - 2026-08-31
------------------
Features
--------
- Add a Vercel Queues backend for Django Tasks and use it by default when no task backends are configured. (#291)
vercel-dramatiq
---------------
0.7.4 - 2026-08-31
------------------
- Update dependencies.
scotttrinh added a commit that referenced this pull request Aug 31, 2026
vercel-headers
--------------
0.7.2 - 2026-08-31
------------------
Bug Fixes
---------
- Accept request objects with concrete header implementations in the IP address and geolocation type annotations. (#337)
vercel-internal-core
--------------------
0.1.3 - 2026-08-31
------------------
Internal
--------
- Support disabling HTTP timeouts for selected SDK operations while preserving the client default elsewhere. (#307)
vercel-oidc
-----------
0.8.1 - 2026-08-31
------------------
- Update dependencies.
vercel-connect
--------------
0.1.1 - 2026-08-31
------------------
- Update dependencies.
vercel-internal-telemetry
-------------------------
0.7.3 - 2026-08-31
------------------
- Update dependencies.
vercel-queue
------------
0.8.1 - 2026-08-31
------------------
Bug Fixes
---------
- Force embedded development servers to exit when graceful shutdown stalls. (#351)
Documentation
-------------
- Remove documentation and examples for `asgi_app` in preparation for its removal. (#309)
vercel-sandbox
--------------
0.5.0 - 2026-08-31
------------------
Features
--------
- Add sync and async `fork_sandbox(...)` support for creating a sandbox from an existing named sandbox with optional configuration overrides. (#257)
- Add `region` and `failover_regions` configuration for sandbox creation, forks, and updates, plus multi-region snapshot availability reporting. (#308)
- Forward private ``__``-prefixed parameters to the Sandbox API. (#350)
Bug Fixes
---------
- Allow Sandbox process waits and log streams to remain idle longer than the session HTTP timeout. (#307)
- Expose Linux process signals consistently on every SDK host platform. (#352)
vercel-cache
------------
0.7.3 - 2026-08-31
------------------
- Update dependencies.
vercel-workflow
---------------
0.10.0 - 2026-08-31
-------------------
Breaking Changes
----------------
- Make `await hook` never return `None`
- Raises a new `HookDisposedError` instead of returning `None` when the hook has been disposed. It is now typed to return `T` instead of `T | None`. `async for` over a hook will stop iterating on disposal, still.
- Make sleep() and retry delays treat numbers as seconds, not ms (#346)
- This matches Python standard library APIs. (#346)
- Use type annotations on workflows and step to allow passing Pydantic models and dataclasses. (#317)
- This is a breaking change, because type annotations will now be enforced. Passing a `dict` when the declaration expects a `list` will fail. (#317)
- Pydantic models and dataclasses can no longer be passed to `@serializable` or `register_serializable()`. Annotate the workflow or step parameter or return value with their type instead. (#317)
Features
--------
- Support `call_later`, `call_at`, and `now` in the event loop implementation. (#343)
- This enables use of `asyncio.sleep()` as well as `asyncio.timeout` and the `timeout` parameter of `asyncio.wait_for`. (#343)
- `get_workflow_metadata()` returns the current run's `WorkflowInfo` (run id, workflow name, start time, deployment URL, and feature flags), callable from a workflow body or a step body — mirroring the JS SDK's `getWorkflowMetadata()`. (#320)
- One current limitation is that `started_at` is `None` from inside a step. (#320)
- Make `HookEvent` an async context manager
- This matches TS, which supports `using`. ``` # disposes the hook on block exit async with SomeHook.wait(...) as hook: res = await hook ```
- `BaseHook.wait()` accepts `metadata` to record on the hook, and `get_hook_by_token()` reads it back for a resumer. (#301)
- A step can raise `RetryableError` to control when its next attempt runs. (#302)
- Accept `specVersion` 7 sealed noop event logs. (#319)
- Failed run and step events now preserve serialized error classes, messages, stacks, and causes. Failed runs also expose a plaintext `errorCode`. (#304)
- A workflow or step can attach plaintext metadata to its run with `set_attributes()`. (#303)
- Add a `share_sandboxes` parameter to `SandboxPolicy` to enable reusing already created sandboxes instead of creating a new one on each invocation. This speeds up workflows but means that modifications to global state may persist between invocations. (#310)
- Support `timedelta` arguments for workflow `sleep()` and retry delays. (#342)
- Expose unstable API to serve workflow HTTP endpoint from your own web framework. (#294)
- Added semi-internal manifest API for TS tools and e2e test. (#296)
Bug Fixes
---------
- Fix failing or even crashing cipher calls inside the workflow sandbox. (#305)
- Fail a workflow run with `HookConflictError` when another run already owns its hook token instead of leaving it running indefinitely. (#327)
- Support resuming hooks with payload in the queue message. (#300)
- Fix some bugs involving hooks arriving when the workflow was not yet blocked on them. (#339)
- Fixed nulls rejected by server, requiring Pydantic 2.12 or newer. (#321)
- Prevent workflows from having side effects while suspending. (#332)
- `hook.dispose()` will now work properly in a `finally` block. (That is, the hook will be disposed only when the workflow is actually terminating, and not every time it gets replayed.) (#332)
- More reliably fail runs whose replay diverges from the event log. (#347)
- Runs will now fail even in the case where the main thread of execution is not directly blocked on the suspension that is erroring. (#347)
- Fixed workflow and step calls with both positional-or-keyword parameters and `*args` failing during replay because their arguments were recorded in an unbindable shape. (#312)
Internal
--------
- Remove a just-added return from a finally block. (#344)
- Correct internal workflow type annotations found by checking untyped function bodies. (#337)
- Refactored event replay. (#341)
- Construct the protocol models by Python field name. (#322)
vercel
------
0.11.0 - 2026-08-31
-------------------
Features
--------
- Expose `get_deadline()` for reading the current Function invocation deadline. (#306)
- Answer workflow health checks for both queue-based transport and HTTP. (#292)
- Add support to read the sealed (`encp`) workflow payloads (X25519 + AES-GCM) an outside writer addresses to a run, under the `encryption` extra. (#297)
Bug Fixes
---------
- Remove upper bounds on aggregate Sandbox and Workflow dependencies so sibling releases cannot make the `vercel` package un-installable. (#334)
- Start a workflow run even when its queue message arrives before the `run_created` event has landed. (#284)
Internal
--------
- The Workflows implementation now ships in the separate `vercel-workflow` distribution, which `vercel` depends on, so `vercel.workflow` imports keep working without installing anything extra. (#299)
vercel-apscheduler
------------------
0.3.0 - 2026-08-31
------------------
Breaking Changes
----------------
- The managed Redis backend was removed. The integration now always runs on its managed job store (Vercel Runtime Cache); a configured default `RedisJobStore` is rejected at import, `VERCEL_APSCHEDULER_BACKEND` accepts only `cache`, and the `redis` dependency is gone. The scheduler's durable identity now always derives from the builder-assigned subscriber id (previously the Redis `jobs_key`); the `scheduler_id` option still pins an identity explicitly. (#286)
vercel-celery
-------------
0.7.5 - 2026-08-31
------------------
- Update dependencies.
vercel-django-tasks
-------------------
0.7.0 - 2026-08-31
------------------
Features
--------
- Add a Vercel Queues backend for Django Tasks and use it by default when no task backends are configured. (#291)
vercel-dramatiq
---------------
0.7.4 - 2026-08-31
------------------
- Update dependencies.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@pranaygp@fantix
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' workflow: read spec 7 and skip sealed noops by pranaygp · Pull Request #319 · vercel/vercel-py · GitHub
Skip to content

workflow: read spec 7 and skip sealed noops - #319

Merged
fantix merged 4 commits into
mainfrom
pgp/port-noop-event-to-py
Aug 24, 2026
Merged

workflow: read spec 7 and skip sealed noops#319
fantix merged 4 commits into
mainfrom
pgp/port-noop-event-to-py

Conversation

@pranaygp

Copy link
Copy Markdown
Contributor

Python half of vercel/workflow#3634 (Add support for 'noop' event type — spec version 7).

Why this is urgent for us

That PR moves SPEC_VERSION_CURRENT and SPEC_VERSION_MAX_SUPPORTED to 7 and switches both @workflow/world-local and @workflow/world-vercel to mintedSpecVersion(), which returns 7 by default. Our reader ceiling was 6, and it is a pydantic le= on BaseEvent.specVersion — so a spec-7 row does not degrade, it fails validation, and with it the whole page. The e2e-python conformance lane runs the TypeScript driver against workbench/python over a shared world-local data dir, so it is the first thing that breaks: the driver's run_created is now labelled 7 and the Python app cannot parse the log it is supposed to replay.

What spec 7 actually asks of a client

On the server, a spec-7 run's slot positions come from a per-run counter handed out before the write that fills them, so concurrent writers never race for a position. The price is that a writer that takes a position and dies leaves a hole, and the backend's read path fills provably-abandoned holes with a server-written noop so returned pages stay dense prefixes.

So the client contract is one thing, and it is read-only: a reader must know a noop occupies its slot and means nothing.

There is no writer half for us. A World only owes the sealing half if it pre-assigns positions ahead of the commit; our local world allocates each id at the commit that fills it, so it has no holes to seal and will never emit one.

The change

world.py

  • SPEC_VERSION_SUPPORTS_SEALED_LOG = 7, and SPEC_VERSION_MAX_SUPPORTED follows it. SPEC_VERSION_CURRENT stays 2 — we still stamp what we always stamped, and a run created elsewhere keeps its creator's version.
  • NoopEvent / NoopEventData, in the read union and deliberately not in CreateEventRequest. eventData is optional and open (extra="allow", matching the .passthrough() on the TS schema): nothing reads it, the shape belongs to whichever backend sealed the slot, and a reader whose only interest is skipping the row has no business rejecting a field it has not heard of.
  • is_sealed_noop_event(), mirroring isSealedNoopEvent — one home for the test.

runtime.pyget_all_workflow_run_events() drops seals, so the log the replay walks is the log without them.

That is the whole skip, and the reason it is one line at the loader rather than a continue in resume() is the clock. Everything downstream of that function is reconstructing what the run did — matching correlation ids to suspensions, looking for a terminal event, deciding which waits elapsed, and reading createdAt for now(). Nothing on this side walks positions, so there is no caller that wants the seal. Meanwhile a seal's createdAt is the sealer's wall clock, which can postdate every real event around it; now() dates the run from the last event the replay consumed, so a seal left in the list hands the body a time no event of the run happened at — and then, because the clock only moves forward, every later now() too. Dropping it at the boundary makes the equivalence the contract is about (a sealed log replays exactly like the same log whose holes their own writers filled) hold by construction, rather than by three call sites remembering. The cursor is untouched: it is the World's, and still points past every row that was read.

Tests

tests/unit/test_workflow_sealed_log_noop.py, 12 tests in four parts:

  • the row — parses out of the read union with eventData, without it, and with an eventData field we have never heard of; the predicate answers for seals only; NoopEvent is in Event and absent from CreateEventRequest; 7 is the ceiling and 8 is still rejected.
  • storage — a seal round-trips through the local world and numbering continues past it, mirroring slot-identity.test.ts's stores, lists, and numbers past a noop event.
  • the loader — seals at head, middle (consecutive) and tail are dropped with the order of everything else intact; a page of nothing but seals still pages on; the cursor survives.
  • replay — the real claim, driven end to end through workflow_handler: one recorded two-sleep run, twice, differing only in seals at the head, in the middle (including one splitting a wait_created/wait_completed pair), and at the tail. Same output, same events written back, and the three now() readings pinned literally so a change that broke both logs the same way cannot slip past the comparison. Plus the degenerate all-seals-before-the-run shape, where the first now() has to find the first real event.

Every seal in the file is stamped a minute into the future, so each of those clock assertions would pass by accident if a seal's createdAt were plausible. Reverting the one-line filter fails 5 of the 12.

test_workflow_local_world_format.py's two spec-version tests move 6 → 7; the ceiling assertion now derives its match from SPEC_VERSION_MAX_SUPPORTED instead of hardcoding the number, so the next bump only has to touch the constant.

Full suite: 1127 tests, 0 failures (test_the_api_token_is_config_then_env_then_oidc fails locally on a box with VERCEL_TOKEN exported, before and after this change). Lint and typecheck clean repo-wide.

Follow-up, not in this PR

workbench/python/pyproject.toml in vercel/workflow resolves vercel-workflow from PyPI (0.9.0 today) regardless of its [tool.uv.sources] rev, so the e2e-python lane only picks this up once it is released and the lock is refreshed there — or once that file git-pins vercel-workflow at a commit containing it, which is what the comment block above that source entry describes.

🤖 Generated with Claude Code

`@workflow/world-local` and `@workflow/world-vercel` both stamp
specVersion 7 now (vercel/workflow#3634), so every event a TypeScript
driver writes arrives labelled 7 and our reader ceiling of 6 rejected
the whole log -- including on the local world the Python e2e lane runs
on. Raise the ceiling, and add the one thing the version gates: a
`noop` row, written by a World's backend to occupy a slot whose writer
allocated the position and then died.
A noop parses out of the read union but is dropped from the log the
replay walks. Its `createdAt` is the sealer's wall clock, which can
postdate every real event around it, so letting it reach the
deterministic clock would make a log whose hole was sealed replay
differently from the same log whose hole its own writer filled.
Nothing here writes one: we allocate each event id at the commit that
fills it, so we have no holes to seal, and `noop` stays out of the
create union.
Co-Authored-By: Pranay Prakash <1797812+pranaygp@users.noreply.github.com>

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@fantixfantix left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM; let me trim off some AI in-code chats and merge this to unbreak the e2e test.

@fantix
fantixforce-pushed the pgp/port-noop-event-to-py branch from 3116c4e to abb6ce9CompareAugust 24, 2026 18:44
@fantix
fantix merged commit a5969aa into mainAug 24, 2026
14 checks passed
@fantix
fantix deleted the pgp/port-noop-event-to-py branch August 24, 2026 18:49
@scotttrinhscotttrinh mentioned this pull request Aug 26, 2026
scotttrinh added a commit that referenced this pull request Aug 26, 2026
vercel-internal-core
--------------------
0.1.3 - 2026-08-26
------------------
Internal
--------
- Support disabling HTTP timeouts for selected SDK operations while preserving the client default elsewhere. (#307)
vercel-connect
--------------
0.1.1 - 2026-08-26
------------------
- Update dependencies.
vercel-queue
------------
0.8.1 - 2026-08-26
------------------
Documentation
-------------
- Remove documentation and examples for `asgi_app` in preparation for its removal. (#309)
vercel-sandbox
--------------
0.5.0 - 2026-08-26
------------------
Features
--------
- Add sync and async `fork_sandbox(...)` support for creating a sandbox from an existing named sandbox with optional configuration overrides. (#257)
- Add `region` and `failover_regions` configuration for sandbox creation, forks, and updates, plus multi-region snapshot availability reporting. (#308)
Bug Fixes
---------
- Allow Sandbox process waits and log streams to remain idle longer than the session HTTP timeout. (#307)
vercel-workflow
---------------
0.10.0 - 2026-08-26
-------------------
Breaking Changes
----------------
- Use type annotations on workflows and step to allow passing Pydantic models and dataclasses. (#317)
- This is a breaking change, because type annotations will now be enforced. Passing a `dict` when the declaration expects a `list` will fail. (#317)
- Pydantic models and dataclasses can no longer be passed to `@serializable` or `register_serializable()`. Annotate the workflow or step parameter or return value with their type instead. (#317)
Features
--------
- `get_workflow_metadata()` returns the current run's `WorkflowInfo` (run id, workflow name, start time, deployment URL, and feature flags), callable from a workflow body or a step body — mirroring the JS SDK's `getWorkflowMetadata()`. (#320)
- One current limitation is that `started_at` is `None` from inside a step. (#320)
- `BaseHook.wait()` accepts `metadata` to record on the hook, and `get_hook_by_token()` reads it back for a resumer. (#301)
- A step can raise `RetryableError` to control when its next attempt runs. (#302)
- Accept `specVersion` 7 sealed noop event logs. (#319)
- A workflow or step can attach plaintext metadata to its run with `set_attributes()`. (#303)
- Add a `share_sandboxes` parameter to `SandboxPolicy` to enable reusing already created sandboxes instead of creating a new one on each invocation. This speeds up workflows but means that modifications to global state may persist between invocations. (#310)
- Expose unstable API to serve workflow HTTP endpoint from your own web framework. (#294)
- Added semi-internal manifest API for TS tools and e2e test. (#296)
Bug Fixes
---------
- Fix failing or even crashing cipher calls inside the workflow sandbox. (#305)
- Support resuming hooks with payload in the queue message. (#300)
- Fixed nulls rejected by server, requiring Pydantic 2.12 or newer. (#321)
- Fixed workflow and step calls with both positional-or-keyword parameters and `*args` failing during replay because their arguments were recorded in an unbindable shape. (#312)
Internal
--------
- Construct the protocol models by Python field name. (#322)
vercel
------
0.11.0 - 2026-08-26
-------------------
Features
--------
- Expose `get_deadline()` for reading the current Function invocation deadline. (#306)
- Answer workflow health checks for both queue-based transport and HTTP. (#292)
- Add support to read the sealed (`encp`) workflow payloads (X25519 + AES-GCM) an outside writer addresses to a run, under the `encryption` extra. (#297)
Bug Fixes
---------
- Remove upper bounds on aggregate Sandbox and Workflow dependencies so sibling releases cannot make the `vercel` package un-installable. (#334)
- Start a workflow run even when its queue message arrives before the `run_created` event has landed. (#284)
Internal
--------
- The Workflows implementation now ships in the separate `vercel-workflow` distribution, which `vercel` depends on, so `vercel.workflow` imports keep working without installing anything extra. (#299)
vercel-apscheduler
------------------
0.3.0 - 2026-08-26
------------------
Breaking Changes
----------------
- The managed Redis backend was removed. The integration now always runs on its managed job store (Vercel Runtime Cache); a configured default `RedisJobStore` is rejected at import, `VERCEL_APSCHEDULER_BACKEND` accepts only `cache`, and the `redis` dependency is gone. The scheduler's durable identity now always derives from the builder-assigned subscriber id (previously the Redis `jobs_key`); the `scheduler_id` option still pins an identity explicitly. (#286)
vercel-celery
-------------
0.7.5 - 2026-08-26
------------------
- Update dependencies.
vercel-django-tasks
-------------------
0.7.0 - 2026-08-26
------------------
Features
--------
- Add a Vercel Queues backend for Django Tasks and use it by default when no task backends are configured. (#291)
vercel-dramatiq
---------------
0.7.4 - 2026-08-26
------------------
- Update dependencies.
scotttrinh added a commit that referenced this pull request Aug 31, 2026
vercel-headers
--------------
0.7.2 - 2026-08-31
------------------
Bug Fixes
---------
- Accept request objects with concrete header implementations in the IP address and geolocation type annotations. (#337)
vercel-internal-core
--------------------
0.1.3 - 2026-08-31
------------------
Internal
--------
- Support disabling HTTP timeouts for selected SDK operations while preserving the client default elsewhere. (#307)
vercel-oidc
-----------
0.8.1 - 2026-08-31
------------------
- Update dependencies.
vercel-connect
--------------
0.1.1 - 2026-08-31
------------------
- Update dependencies.
vercel-internal-telemetry
-------------------------
0.7.3 - 2026-08-31
------------------
- Update dependencies.
vercel-queue
------------
0.8.1 - 2026-08-31
------------------
Bug Fixes
---------
- Force embedded development servers to exit when graceful shutdown stalls. (#351)
Documentation
-------------
- Remove documentation and examples for `asgi_app` in preparation for its removal. (#309)
vercel-sandbox
--------------
0.5.0 - 2026-08-31
------------------
Features
--------
- Add sync and async `fork_sandbox(...)` support for creating a sandbox from an existing named sandbox with optional configuration overrides. (#257)
- Add `region` and `failover_regions` configuration for sandbox creation, forks, and updates, plus multi-region snapshot availability reporting. (#308)
- Forward private ``__``-prefixed parameters to the Sandbox API. (#350)
Bug Fixes
---------
- Allow Sandbox process waits and log streams to remain idle longer than the session HTTP timeout. (#307)
- Expose Linux process signals consistently on every SDK host platform. (#352)
vercel-cache
------------
0.7.3 - 2026-08-31
------------------
- Update dependencies.
vercel-workflow
---------------
0.10.0 - 2026-08-31
-------------------
Breaking Changes
----------------
- Make sleep() and retry delays treat numbers as seconds, not ms (#346)
- This matches Python standard library APIs. (#346)
- Use type annotations on workflows and step to allow passing Pydantic models and dataclasses. (#317)
- This is a breaking change, because type annotations will now be enforced. Passing a `dict` when the declaration expects a `list` will fail. (#317)
- Pydantic models and dataclasses can no longer be passed to `@serializable` or `register_serializable()`. Annotate the workflow or step parameter or return value with their type instead. (#317)
Features
--------
- Support `call_later`, `call_at`, and `now` in the event loop implementation. (#343)
- This enables use of `asyncio.sleep()` as well as `asyncio.timeout` and the `timeout` parameter of `asyncio.wait_for`. (#343)
- `get_workflow_metadata()` returns the current run's `WorkflowInfo` (run id, workflow name, start time, deployment URL, and feature flags), callable from a workflow body or a step body — mirroring the JS SDK's `getWorkflowMetadata()`. (#320)
- One current limitation is that `started_at` is `None` from inside a step. (#320)
- `BaseHook.wait()` accepts `metadata` to record on the hook, and `get_hook_by_token()` reads it back for a resumer. (#301)
- A step can raise `RetryableError` to control when its next attempt runs. (#302)
- Accept `specVersion` 7 sealed noop event logs. (#319)
- Failed run and step events now preserve serialized error classes, messages, stacks, and causes. Failed runs also expose a plaintext `errorCode`. (#304)
- A workflow or step can attach plaintext metadata to its run with `set_attributes()`. (#303)
- Add a `share_sandboxes` parameter to `SandboxPolicy` to enable reusing already created sandboxes instead of creating a new one on each invocation. This speeds up workflows but means that modifications to global state may persist between invocations. (#310)
- Support `timedelta` arguments for workflow `sleep()` and retry delays. (#342)
- Expose unstable API to serve workflow HTTP endpoint from your own web framework. (#294)
- Added semi-internal manifest API for TS tools and e2e test. (#296)
Bug Fixes
---------
- Fix failing or even crashing cipher calls inside the workflow sandbox. (#305)
- Fail a workflow run with `HookConflictError` when another run already owns its hook token instead of leaving it running indefinitely. (#327)
- Support resuming hooks with payload in the queue message. (#300)
- Fix some bugs involving hooks arriving when the workflow was not yet blocked on them. (#339)
- Fixed nulls rejected by server, requiring Pydantic 2.12 or newer. (#321)
- Prevent workflows from having side effects while suspending. (#332)
- `hook.dispose()` will now work properly in a `finally` block. (That is, the hook will be disposed only when the workflow is actually terminating, and not every time it gets replayed.) (#332)
- More reliably fail runs whose replay diverges from the event log. (#347)
- Runs will now fail even in the case where the main thread of execution is not directly blocked on the suspension that is erroring. (#347)
- Fixed workflow and step calls with both positional-or-keyword parameters and `*args` failing during replay because their arguments were recorded in an unbindable shape. (#312)
Internal
--------
- Remove a just-added return from a finally block. (#344)
- Correct internal workflow type annotations found by checking untyped function bodies. (#337)
- Refactored event replay. (#341)
- Construct the protocol models by Python field name. (#322)
vercel
------
0.11.0 - 2026-08-31
-------------------
Features
--------
- Expose `get_deadline()` for reading the current Function invocation deadline. (#306)
- Answer workflow health checks for both queue-based transport and HTTP. (#292)
- Add support to read the sealed (`encp`) workflow payloads (X25519 + AES-GCM) an outside writer addresses to a run, under the `encryption` extra. (#297)
Bug Fixes
---------
- Remove upper bounds on aggregate Sandbox and Workflow dependencies so sibling releases cannot make the `vercel` package un-installable. (#334)
- Start a workflow run even when its queue message arrives before the `run_created` event has landed. (#284)
Internal
--------
- The Workflows implementation now ships in the separate `vercel-workflow` distribution, which `vercel` depends on, so `vercel.workflow` imports keep working without installing anything extra. (#299)
vercel-apscheduler
------------------
0.3.0 - 2026-08-31
------------------
Breaking Changes
----------------
- The managed Redis backend was removed. The integration now always runs on its managed job store (Vercel Runtime Cache); a configured default `RedisJobStore` is rejected at import, `VERCEL_APSCHEDULER_BACKEND` accepts only `cache`, and the `redis` dependency is gone. The scheduler's durable identity now always derives from the builder-assigned subscriber id (previously the Redis `jobs_key`); the `scheduler_id` option still pins an identity explicitly. (#286)
vercel-celery
-------------
0.7.5 - 2026-08-31
------------------
- Update dependencies.
vercel-django-tasks
-------------------
0.7.0 - 2026-08-31
------------------
Features
--------
- Add a Vercel Queues backend for Django Tasks and use it by default when no task backends are configured. (#291)
vercel-dramatiq
---------------
0.7.4 - 2026-08-31
------------------
- Update dependencies.
scotttrinh added a commit that referenced this pull request Aug 31, 2026
vercel-headers
--------------
0.7.2 - 2026-08-31
------------------
Bug Fixes
---------
- Accept request objects with concrete header implementations in the IP address and geolocation type annotations. (#337)
vercel-internal-core
--------------------
0.1.3 - 2026-08-31
------------------
Internal
--------
- Support disabling HTTP timeouts for selected SDK operations while preserving the client default elsewhere. (#307)
vercel-oidc
-----------
0.8.1 - 2026-08-31
------------------
- Update dependencies.
vercel-connect
--------------
0.1.1 - 2026-08-31
------------------
- Update dependencies.
vercel-internal-telemetry
-------------------------
0.7.3 - 2026-08-31
------------------
- Update dependencies.
vercel-queue
------------
0.8.1 - 2026-08-31
------------------
Bug Fixes
---------
- Force embedded development servers to exit when graceful shutdown stalls. (#351)
Documentation
-------------
- Remove documentation and examples for `asgi_app` in preparation for its removal. (#309)
vercel-sandbox
--------------
0.5.0 - 2026-08-31
------------------
Features
--------
- Add sync and async `fork_sandbox(...)` support for creating a sandbox from an existing named sandbox with optional configuration overrides. (#257)
- Add `region` and `failover_regions` configuration for sandbox creation, forks, and updates, plus multi-region snapshot availability reporting. (#308)
- Forward private ``__``-prefixed parameters to the Sandbox API. (#350)
Bug Fixes
---------
- Allow Sandbox process waits and log streams to remain idle longer than the session HTTP timeout. (#307)
- Expose Linux process signals consistently on every SDK host platform. (#352)
vercel-cache
------------
0.7.3 - 2026-08-31
------------------
- Update dependencies.
vercel-workflow
---------------
0.10.0 - 2026-08-31
-------------------
Breaking Changes
----------------
- Make `await hook` never return `None`
- Raises a new `HookDisposedError` instead of returning `None` when the hook has been disposed. It is now typed to return `T` instead of `T | None`. `async for` over a hook will stop iterating on disposal, still.
- Make sleep() and retry delays treat numbers as seconds, not ms (#346)
- This matches Python standard library APIs. (#346)
- Use type annotations on workflows and step to allow passing Pydantic models and dataclasses. (#317)
- This is a breaking change, because type annotations will now be enforced. Passing a `dict` when the declaration expects a `list` will fail. (#317)
- Pydantic models and dataclasses can no longer be passed to `@serializable` or `register_serializable()`. Annotate the workflow or step parameter or return value with their type instead. (#317)
Features
--------
- Support `call_later`, `call_at`, and `now` in the event loop implementation. (#343)
- This enables use of `asyncio.sleep()` as well as `asyncio.timeout` and the `timeout` parameter of `asyncio.wait_for`. (#343)
- `get_workflow_metadata()` returns the current run's `WorkflowInfo` (run id, workflow name, start time, deployment URL, and feature flags), callable from a workflow body or a step body — mirroring the JS SDK's `getWorkflowMetadata()`. (#320)
- One current limitation is that `started_at` is `None` from inside a step. (#320)
- Make `HookEvent` an async context manager
- This matches TS, which supports `using`. ``` # disposes the hook on block exit async with SomeHook.wait(...) as hook: res = await hook ```
- `BaseHook.wait()` accepts `metadata` to record on the hook, and `get_hook_by_token()` reads it back for a resumer. (#301)
- A step can raise `RetryableError` to control when its next attempt runs. (#302)
- Accept `specVersion` 7 sealed noop event logs. (#319)
- Failed run and step events now preserve serialized error classes, messages, stacks, and causes. Failed runs also expose a plaintext `errorCode`. (#304)
- A workflow or step can attach plaintext metadata to its run with `set_attributes()`. (#303)
- Add a `share_sandboxes` parameter to `SandboxPolicy` to enable reusing already created sandboxes instead of creating a new one on each invocation. This speeds up workflows but means that modifications to global state may persist between invocations. (#310)
- Support `timedelta` arguments for workflow `sleep()` and retry delays. (#342)
- Expose unstable API to serve workflow HTTP endpoint from your own web framework. (#294)
- Added semi-internal manifest API for TS tools and e2e test. (#296)
Bug Fixes
---------
- Fix failing or even crashing cipher calls inside the workflow sandbox. (#305)
- Fail a workflow run with `HookConflictError` when another run already owns its hook token instead of leaving it running indefinitely. (#327)
- Support resuming hooks with payload in the queue message. (#300)
- Fix some bugs involving hooks arriving when the workflow was not yet blocked on them. (#339)
- Fixed nulls rejected by server, requiring Pydantic 2.12 or newer. (#321)
- Prevent workflows from having side effects while suspending. (#332)
- `hook.dispose()` will now work properly in a `finally` block. (That is, the hook will be disposed only when the workflow is actually terminating, and not every time it gets replayed.) (#332)
- More reliably fail runs whose replay diverges from the event log. (#347)
- Runs will now fail even in the case where the main thread of execution is not directly blocked on the suspension that is erroring. (#347)
- Fixed workflow and step calls with both positional-or-keyword parameters and `*args` failing during replay because their arguments were recorded in an unbindable shape. (#312)
Internal
--------
- Remove a just-added return from a finally block. (#344)
- Correct internal workflow type annotations found by checking untyped function bodies. (#337)
- Refactored event replay. (#341)
- Construct the protocol models by Python field name. (#322)
vercel
------
0.11.0 - 2026-08-31
-------------------
Features
--------
- Expose `get_deadline()` for reading the current Function invocation deadline. (#306)
- Answer workflow health checks for both queue-based transport and HTTP. (#292)
- Add support to read the sealed (`encp`) workflow payloads (X25519 + AES-GCM) an outside writer addresses to a run, under the `encryption` extra. (#297)
Bug Fixes
---------
- Remove upper bounds on aggregate Sandbox and Workflow dependencies so sibling releases cannot make the `vercel` package un-installable. (#334)
- Start a workflow run even when its queue message arrives before the `run_created` event has landed. (#284)
Internal
--------
- The Workflows implementation now ships in the separate `vercel-workflow` distribution, which `vercel` depends on, so `vercel.workflow` imports keep working without installing anything extra. (#299)
vercel-apscheduler
------------------
0.3.0 - 2026-08-31
------------------
Breaking Changes
----------------
- The managed Redis backend was removed. The integration now always runs on its managed job store (Vercel Runtime Cache); a configured default `RedisJobStore` is rejected at import, `VERCEL_APSCHEDULER_BACKEND` accepts only `cache`, and the `redis` dependency is gone. The scheduler's durable identity now always derives from the builder-assigned subscriber id (previously the Redis `jobs_key`); the `scheduler_id` option still pins an identity explicitly. (#286)
vercel-celery
-------------
0.7.5 - 2026-08-31
------------------
- Update dependencies.
vercel-django-tasks
-------------------
0.7.0 - 2026-08-31
------------------
Features
--------
- Add a Vercel Queues backend for Django Tasks and use it by default when no task backends are configured. (#291)
vercel-dramatiq
---------------
0.7.4 - 2026-08-31
------------------
- Update dependencies.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@pranaygp@fantix
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' workflow: read spec 7 and skip sealed noops by pranaygp · Pull Request #319 · vercel/vercel-py · GitHub
Skip to content

workflow: read spec 7 and skip sealed noops - #319

Merged
fantix merged 4 commits into
mainfrom
pgp/port-noop-event-to-py
Aug 24, 2026
Merged

workflow: read spec 7 and skip sealed noops#319
fantix merged 4 commits into
mainfrom
pgp/port-noop-event-to-py

Conversation

@pranaygp

Copy link
Copy Markdown
Contributor

Python half of vercel/workflow#3634 (Add support for 'noop' event type — spec version 7).

Why this is urgent for us

That PR moves SPEC_VERSION_CURRENT and SPEC_VERSION_MAX_SUPPORTED to 7 and switches both @workflow/world-local and @workflow/world-vercel to mintedSpecVersion(), which returns 7 by default. Our reader ceiling was 6, and it is a pydantic le= on BaseEvent.specVersion — so a spec-7 row does not degrade, it fails validation, and with it the whole page. The e2e-python conformance lane runs the TypeScript driver against workbench/python over a shared world-local data dir, so it is the first thing that breaks: the driver's run_created is now labelled 7 and the Python app cannot parse the log it is supposed to replay.

What spec 7 actually asks of a client

On the server, a spec-7 run's slot positions come from a per-run counter handed out before the write that fills them, so concurrent writers never race for a position. The price is that a writer that takes a position and dies leaves a hole, and the backend's read path fills provably-abandoned holes with a server-written noop so returned pages stay dense prefixes.

So the client contract is one thing, and it is read-only: a reader must know a noop occupies its slot and means nothing.

There is no writer half for us. A World only owes the sealing half if it pre-assigns positions ahead of the commit; our local world allocates each id at the commit that fills it, so it has no holes to seal and will never emit one.

The change

world.py

  • SPEC_VERSION_SUPPORTS_SEALED_LOG = 7, and SPEC_VERSION_MAX_SUPPORTED follows it. SPEC_VERSION_CURRENT stays 2 — we still stamp what we always stamped, and a run created elsewhere keeps its creator's version.
  • NoopEvent / NoopEventData, in the read union and deliberately not in CreateEventRequest. eventData is optional and open (extra="allow", matching the .passthrough() on the TS schema): nothing reads it, the shape belongs to whichever backend sealed the slot, and a reader whose only interest is skipping the row has no business rejecting a field it has not heard of.
  • is_sealed_noop_event(), mirroring isSealedNoopEvent — one home for the test.

runtime.pyget_all_workflow_run_events() drops seals, so the log the replay walks is the log without them.

That is the whole skip, and the reason it is one line at the loader rather than a continue in resume() is the clock. Everything downstream of that function is reconstructing what the run did — matching correlation ids to suspensions, looking for a terminal event, deciding which waits elapsed, and reading createdAt for now(). Nothing on this side walks positions, so there is no caller that wants the seal. Meanwhile a seal's createdAt is the sealer's wall clock, which can postdate every real event around it; now() dates the run from the last event the replay consumed, so a seal left in the list hands the body a time no event of the run happened at — and then, because the clock only moves forward, every later now() too. Dropping it at the boundary makes the equivalence the contract is about (a sealed log replays exactly like the same log whose holes their own writers filled) hold by construction, rather than by three call sites remembering. The cursor is untouched: it is the World's, and still points past every row that was read.

Tests

tests/unit/test_workflow_sealed_log_noop.py, 12 tests in four parts:

  • the row — parses out of the read union with eventData, without it, and with an eventData field we have never heard of; the predicate answers for seals only; NoopEvent is in Event and absent from CreateEventRequest; 7 is the ceiling and 8 is still rejected.
  • storage — a seal round-trips through the local world and numbering continues past it, mirroring slot-identity.test.ts's stores, lists, and numbers past a noop event.
  • the loader — seals at head, middle (consecutive) and tail are dropped with the order of everything else intact; a page of nothing but seals still pages on; the cursor survives.
  • replay — the real claim, driven end to end through workflow_handler: one recorded two-sleep run, twice, differing only in seals at the head, in the middle (including one splitting a wait_created/wait_completed pair), and at the tail. Same output, same events written back, and the three now() readings pinned literally so a change that broke both logs the same way cannot slip past the comparison. Plus the degenerate all-seals-before-the-run shape, where the first now() has to find the first real event.

Every seal in the file is stamped a minute into the future, so each of those clock assertions would pass by accident if a seal's createdAt were plausible. Reverting the one-line filter fails 5 of the 12.

test_workflow_local_world_format.py's two spec-version tests move 6 → 7; the ceiling assertion now derives its match from SPEC_VERSION_MAX_SUPPORTED instead of hardcoding the number, so the next bump only has to touch the constant.

Full suite: 1127 tests, 0 failures (test_the_api_token_is_config_then_env_then_oidc fails locally on a box with VERCEL_TOKEN exported, before and after this change). Lint and typecheck clean repo-wide.

Follow-up, not in this PR

workbench/python/pyproject.toml in vercel/workflow resolves vercel-workflow from PyPI (0.9.0 today) regardless of its [tool.uv.sources] rev, so the e2e-python lane only picks this up once it is released and the lock is refreshed there — or once that file git-pins vercel-workflow at a commit containing it, which is what the comment block above that source entry describes.

🤖 Generated with Claude Code

`@workflow/world-local` and `@workflow/world-vercel` both stamp
specVersion 7 now (vercel/workflow#3634), so every event a TypeScript
driver writes arrives labelled 7 and our reader ceiling of 6 rejected
the whole log -- including on the local world the Python e2e lane runs
on. Raise the ceiling, and add the one thing the version gates: a
`noop` row, written by a World's backend to occupy a slot whose writer
allocated the position and then died.
A noop parses out of the read union but is dropped from the log the
replay walks. Its `createdAt` is the sealer's wall clock, which can
postdate every real event around it, so letting it reach the
deterministic clock would make a log whose hole was sealed replay
differently from the same log whose hole its own writer filled.
Nothing here writes one: we allocate each event id at the commit that
fills it, so we have no holes to seal, and `noop` stays out of the
create union.
Co-Authored-By: Pranay Prakash <1797812+pranaygp@users.noreply.github.com>

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@fantixfantix left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM; let me trim off some AI in-code chats and merge this to unbreak the e2e test.

@fantix
fantixforce-pushed the pgp/port-noop-event-to-py branch from 3116c4e to abb6ce9CompareAugust 24, 2026 18:44
@fantix
fantix merged commit a5969aa into mainAug 24, 2026
14 checks passed
@fantix
fantix deleted the pgp/port-noop-event-to-py branch August 24, 2026 18:49
@scotttrinhscotttrinh mentioned this pull request Aug 26, 2026
scotttrinh added a commit that referenced this pull request Aug 26, 2026
vercel-internal-core
--------------------
0.1.3 - 2026-08-26
------------------
Internal
--------
- Support disabling HTTP timeouts for selected SDK operations while preserving the client default elsewhere. (#307)
vercel-connect
--------------
0.1.1 - 2026-08-26
------------------
- Update dependencies.
vercel-queue
------------
0.8.1 - 2026-08-26
------------------
Documentation
-------------
- Remove documentation and examples for `asgi_app` in preparation for its removal. (#309)
vercel-sandbox
--------------
0.5.0 - 2026-08-26
------------------
Features
--------
- Add sync and async `fork_sandbox(...)` support for creating a sandbox from an existing named sandbox with optional configuration overrides. (#257)
- Add `region` and `failover_regions` configuration for sandbox creation, forks, and updates, plus multi-region snapshot availability reporting. (#308)
Bug Fixes
---------
- Allow Sandbox process waits and log streams to remain idle longer than the session HTTP timeout. (#307)
vercel-workflow
---------------
0.10.0 - 2026-08-26
-------------------
Breaking Changes
----------------
- Use type annotations on workflows and step to allow passing Pydantic models and dataclasses. (#317)
- This is a breaking change, because type annotations will now be enforced. Passing a `dict` when the declaration expects a `list` will fail. (#317)
- Pydantic models and dataclasses can no longer be passed to `@serializable` or `register_serializable()`. Annotate the workflow or step parameter or return value with their type instead. (#317)
Features
--------
- `get_workflow_metadata()` returns the current run's `WorkflowInfo` (run id, workflow name, start time, deployment URL, and feature flags), callable from a workflow body or a step body — mirroring the JS SDK's `getWorkflowMetadata()`. (#320)
- One current limitation is that `started_at` is `None` from inside a step. (#320)
- `BaseHook.wait()` accepts `metadata` to record on the hook, and `get_hook_by_token()` reads it back for a resumer. (#301)
- A step can raise `RetryableError` to control when its next attempt runs. (#302)
- Accept `specVersion` 7 sealed noop event logs. (#319)
- A workflow or step can attach plaintext metadata to its run with `set_attributes()`. (#303)
- Add a `share_sandboxes` parameter to `SandboxPolicy` to enable reusing already created sandboxes instead of creating a new one on each invocation. This speeds up workflows but means that modifications to global state may persist between invocations. (#310)
- Expose unstable API to serve workflow HTTP endpoint from your own web framework. (#294)
- Added semi-internal manifest API for TS tools and e2e test. (#296)
Bug Fixes
---------
- Fix failing or even crashing cipher calls inside the workflow sandbox. (#305)
- Support resuming hooks with payload in the queue message. (#300)
- Fixed nulls rejected by server, requiring Pydantic 2.12 or newer. (#321)
- Fixed workflow and step calls with both positional-or-keyword parameters and `*args` failing during replay because their arguments were recorded in an unbindable shape. (#312)
Internal
--------
- Construct the protocol models by Python field name. (#322)
vercel
------
0.11.0 - 2026-08-26
-------------------
Features
--------
- Expose `get_deadline()` for reading the current Function invocation deadline. (#306)
- Answer workflow health checks for both queue-based transport and HTTP. (#292)
- Add support to read the sealed (`encp`) workflow payloads (X25519 + AES-GCM) an outside writer addresses to a run, under the `encryption` extra. (#297)
Bug Fixes
---------
- Remove upper bounds on aggregate Sandbox and Workflow dependencies so sibling releases cannot make the `vercel` package un-installable. (#334)
- Start a workflow run even when its queue message arrives before the `run_created` event has landed. (#284)
Internal
--------
- The Workflows implementation now ships in the separate `vercel-workflow` distribution, which `vercel` depends on, so `vercel.workflow` imports keep working without installing anything extra. (#299)
vercel-apscheduler
------------------
0.3.0 - 2026-08-26
------------------
Breaking Changes
----------------
- The managed Redis backend was removed. The integration now always runs on its managed job store (Vercel Runtime Cache); a configured default `RedisJobStore` is rejected at import, `VERCEL_APSCHEDULER_BACKEND` accepts only `cache`, and the `redis` dependency is gone. The scheduler's durable identity now always derives from the builder-assigned subscriber id (previously the Redis `jobs_key`); the `scheduler_id` option still pins an identity explicitly. (#286)
vercel-celery
-------------
0.7.5 - 2026-08-26
------------------
- Update dependencies.
vercel-django-tasks
-------------------
0.7.0 - 2026-08-26
------------------
Features
--------
- Add a Vercel Queues backend for Django Tasks and use it by default when no task backends are configured. (#291)
vercel-dramatiq
---------------
0.7.4 - 2026-08-26
------------------
- Update dependencies.
scotttrinh added a commit that referenced this pull request Aug 31, 2026
vercel-headers
--------------
0.7.2 - 2026-08-31
------------------
Bug Fixes
---------
- Accept request objects with concrete header implementations in the IP address and geolocation type annotations. (#337)
vercel-internal-core
--------------------
0.1.3 - 2026-08-31
------------------
Internal
--------
- Support disabling HTTP timeouts for selected SDK operations while preserving the client default elsewhere. (#307)
vercel-oidc
-----------
0.8.1 - 2026-08-31
------------------
- Update dependencies.
vercel-connect
--------------
0.1.1 - 2026-08-31
------------------
- Update dependencies.
vercel-internal-telemetry
-------------------------
0.7.3 - 2026-08-31
------------------
- Update dependencies.
vercel-queue
------------
0.8.1 - 2026-08-31
------------------
Bug Fixes
---------
- Force embedded development servers to exit when graceful shutdown stalls. (#351)
Documentation
-------------
- Remove documentation and examples for `asgi_app` in preparation for its removal. (#309)
vercel-sandbox
--------------
0.5.0 - 2026-08-31
------------------
Features
--------
- Add sync and async `fork_sandbox(...)` support for creating a sandbox from an existing named sandbox with optional configuration overrides. (#257)
- Add `region` and `failover_regions` configuration for sandbox creation, forks, and updates, plus multi-region snapshot availability reporting. (#308)
- Forward private ``__``-prefixed parameters to the Sandbox API. (#350)
Bug Fixes
---------
- Allow Sandbox process waits and log streams to remain idle longer than the session HTTP timeout. (#307)
- Expose Linux process signals consistently on every SDK host platform. (#352)
vercel-cache
------------
0.7.3 - 2026-08-31
------------------
- Update dependencies.
vercel-workflow
---------------
0.10.0 - 2026-08-31
-------------------
Breaking Changes
----------------
- Make sleep() and retry delays treat numbers as seconds, not ms (#346)
- This matches Python standard library APIs. (#346)
- Use type annotations on workflows and step to allow passing Pydantic models and dataclasses. (#317)
- This is a breaking change, because type annotations will now be enforced. Passing a `dict` when the declaration expects a `list` will fail. (#317)
- Pydantic models and dataclasses can no longer be passed to `@serializable` or `register_serializable()`. Annotate the workflow or step parameter or return value with their type instead. (#317)
Features
--------
- Support `call_later`, `call_at`, and `now` in the event loop implementation. (#343)
- This enables use of `asyncio.sleep()` as well as `asyncio.timeout` and the `timeout` parameter of `asyncio.wait_for`. (#343)
- `get_workflow_metadata()` returns the current run's `WorkflowInfo` (run id, workflow name, start time, deployment URL, and feature flags), callable from a workflow body or a step body — mirroring the JS SDK's `getWorkflowMetadata()`. (#320)
- One current limitation is that `started_at` is `None` from inside a step. (#320)
- `BaseHook.wait()` accepts `metadata` to record on the hook, and `get_hook_by_token()` reads it back for a resumer. (#301)
- A step can raise `RetryableError` to control when its next attempt runs. (#302)
- Accept `specVersion` 7 sealed noop event logs. (#319)
- Failed run and step events now preserve serialized error classes, messages, stacks, and causes. Failed runs also expose a plaintext `errorCode`. (#304)
- A workflow or step can attach plaintext metadata to its run with `set_attributes()`. (#303)
- Add a `share_sandboxes` parameter to `SandboxPolicy` to enable reusing already created sandboxes instead of creating a new one on each invocation. This speeds up workflows but means that modifications to global state may persist between invocations. (#310)
- Support `timedelta` arguments for workflow `sleep()` and retry delays. (#342)
- Expose unstable API to serve workflow HTTP endpoint from your own web framework. (#294)
- Added semi-internal manifest API for TS tools and e2e test. (#296)
Bug Fixes
---------
- Fix failing or even crashing cipher calls inside the workflow sandbox. (#305)
- Fail a workflow run with `HookConflictError` when another run already owns its hook token instead of leaving it running indefinitely. (#327)
- Support resuming hooks with payload in the queue message. (#300)
- Fix some bugs involving hooks arriving when the workflow was not yet blocked on them. (#339)
- Fixed nulls rejected by server, requiring Pydantic 2.12 or newer. (#321)
- Prevent workflows from having side effects while suspending. (#332)
- `hook.dispose()` will now work properly in a `finally` block. (That is, the hook will be disposed only when the workflow is actually terminating, and not every time it gets replayed.) (#332)
- More reliably fail runs whose replay diverges from the event log. (#347)
- Runs will now fail even in the case where the main thread of execution is not directly blocked on the suspension that is erroring. (#347)
- Fixed workflow and step calls with both positional-or-keyword parameters and `*args` failing during replay because their arguments were recorded in an unbindable shape. (#312)
Internal
--------
- Remove a just-added return from a finally block. (#344)
- Correct internal workflow type annotations found by checking untyped function bodies. (#337)
- Refactored event replay. (#341)
- Construct the protocol models by Python field name. (#322)
vercel
------
0.11.0 - 2026-08-31
-------------------
Features
--------
- Expose `get_deadline()` for reading the current Function invocation deadline. (#306)
- Answer workflow health checks for both queue-based transport and HTTP. (#292)
- Add support to read the sealed (`encp`) workflow payloads (X25519 + AES-GCM) an outside writer addresses to a run, under the `encryption` extra. (#297)
Bug Fixes
---------
- Remove upper bounds on aggregate Sandbox and Workflow dependencies so sibling releases cannot make the `vercel` package un-installable. (#334)
- Start a workflow run even when its queue message arrives before the `run_created` event has landed. (#284)
Internal
--------
- The Workflows implementation now ships in the separate `vercel-workflow` distribution, which `vercel` depends on, so `vercel.workflow` imports keep working without installing anything extra. (#299)
vercel-apscheduler
------------------
0.3.0 - 2026-08-31
------------------
Breaking Changes
----------------
- The managed Redis backend was removed. The integration now always runs on its managed job store (Vercel Runtime Cache); a configured default `RedisJobStore` is rejected at import, `VERCEL_APSCHEDULER_BACKEND` accepts only `cache`, and the `redis` dependency is gone. The scheduler's durable identity now always derives from the builder-assigned subscriber id (previously the Redis `jobs_key`); the `scheduler_id` option still pins an identity explicitly. (#286)
vercel-celery
-------------
0.7.5 - 2026-08-31
------------------
- Update dependencies.
vercel-django-tasks
-------------------
0.7.0 - 2026-08-31
------------------
Features
--------
- Add a Vercel Queues backend for Django Tasks and use it by default when no task backends are configured. (#291)
vercel-dramatiq
---------------
0.7.4 - 2026-08-31
------------------
- Update dependencies.
scotttrinh added a commit that referenced this pull request Aug 31, 2026
vercel-headers
--------------
0.7.2 - 2026-08-31
------------------
Bug Fixes
---------
- Accept request objects with concrete header implementations in the IP address and geolocation type annotations. (#337)
vercel-internal-core
--------------------
0.1.3 - 2026-08-31
------------------
Internal
--------
- Support disabling HTTP timeouts for selected SDK operations while preserving the client default elsewhere. (#307)
vercel-oidc
-----------
0.8.1 - 2026-08-31
------------------
- Update dependencies.
vercel-connect
--------------
0.1.1 - 2026-08-31
------------------
- Update dependencies.
vercel-internal-telemetry
-------------------------
0.7.3 - 2026-08-31
------------------
- Update dependencies.
vercel-queue
------------
0.8.1 - 2026-08-31
------------------
Bug Fixes
---------
- Force embedded development servers to exit when graceful shutdown stalls. (#351)
Documentation
-------------
- Remove documentation and examples for `asgi_app` in preparation for its removal. (#309)
vercel-sandbox
--------------
0.5.0 - 2026-08-31
------------------
Features
--------
- Add sync and async `fork_sandbox(...)` support for creating a sandbox from an existing named sandbox with optional configuration overrides. (#257)
- Add `region` and `failover_regions` configuration for sandbox creation, forks, and updates, plus multi-region snapshot availability reporting. (#308)
- Forward private ``__``-prefixed parameters to the Sandbox API. (#350)
Bug Fixes
---------
- Allow Sandbox process waits and log streams to remain idle longer than the session HTTP timeout. (#307)
- Expose Linux process signals consistently on every SDK host platform. (#352)
vercel-cache
------------
0.7.3 - 2026-08-31
------------------
- Update dependencies.
vercel-workflow
---------------
0.10.0 - 2026-08-31
-------------------
Breaking Changes
----------------
- Make `await hook` never return `None`
- Raises a new `HookDisposedError` instead of returning `None` when the hook has been disposed. It is now typed to return `T` instead of `T | None`. `async for` over a hook will stop iterating on disposal, still.
- Make sleep() and retry delays treat numbers as seconds, not ms (#346)
- This matches Python standard library APIs. (#346)
- Use type annotations on workflows and step to allow passing Pydantic models and dataclasses. (#317)
- This is a breaking change, because type annotations will now be enforced. Passing a `dict` when the declaration expects a `list` will fail. (#317)
- Pydantic models and dataclasses can no longer be passed to `@serializable` or `register_serializable()`. Annotate the workflow or step parameter or return value with their type instead. (#317)
Features
--------
- Support `call_later`, `call_at`, and `now` in the event loop implementation. (#343)
- This enables use of `asyncio.sleep()` as well as `asyncio.timeout` and the `timeout` parameter of `asyncio.wait_for`. (#343)
- `get_workflow_metadata()` returns the current run's `WorkflowInfo` (run id, workflow name, start time, deployment URL, and feature flags), callable from a workflow body or a step body — mirroring the JS SDK's `getWorkflowMetadata()`. (#320)
- One current limitation is that `started_at` is `None` from inside a step. (#320)
- Make `HookEvent` an async context manager
- This matches TS, which supports `using`. ``` # disposes the hook on block exit async with SomeHook.wait(...) as hook: res = await hook ```
- `BaseHook.wait()` accepts `metadata` to record on the hook, and `get_hook_by_token()` reads it back for a resumer. (#301)
- A step can raise `RetryableError` to control when its next attempt runs. (#302)
- Accept `specVersion` 7 sealed noop event logs. (#319)
- Failed run and step events now preserve serialized error classes, messages, stacks, and causes. Failed runs also expose a plaintext `errorCode`. (#304)
- A workflow or step can attach plaintext metadata to its run with `set_attributes()`. (#303)
- Add a `share_sandboxes` parameter to `SandboxPolicy` to enable reusing already created sandboxes instead of creating a new one on each invocation. This speeds up workflows but means that modifications to global state may persist between invocations. (#310)
- Support `timedelta` arguments for workflow `sleep()` and retry delays. (#342)
- Expose unstable API to serve workflow HTTP endpoint from your own web framework. (#294)
- Added semi-internal manifest API for TS tools and e2e test. (#296)
Bug Fixes
---------
- Fix failing or even crashing cipher calls inside the workflow sandbox. (#305)
- Fail a workflow run with `HookConflictError` when another run already owns its hook token instead of leaving it running indefinitely. (#327)
- Support resuming hooks with payload in the queue message. (#300)
- Fix some bugs involving hooks arriving when the workflow was not yet blocked on them. (#339)
- Fixed nulls rejected by server, requiring Pydantic 2.12 or newer. (#321)
- Prevent workflows from having side effects while suspending. (#332)
- `hook.dispose()` will now work properly in a `finally` block. (That is, the hook will be disposed only when the workflow is actually terminating, and not every time it gets replayed.) (#332)
- More reliably fail runs whose replay diverges from the event log. (#347)
- Runs will now fail even in the case where the main thread of execution is not directly blocked on the suspension that is erroring. (#347)
- Fixed workflow and step calls with both positional-or-keyword parameters and `*args` failing during replay because their arguments were recorded in an unbindable shape. (#312)
Internal
--------
- Remove a just-added return from a finally block. (#344)
- Correct internal workflow type annotations found by checking untyped function bodies. (#337)
- Refactored event replay. (#341)
- Construct the protocol models by Python field name. (#322)
vercel
------
0.11.0 - 2026-08-31
-------------------
Features
--------
- Expose `get_deadline()` for reading the current Function invocation deadline. (#306)
- Answer workflow health checks for both queue-based transport and HTTP. (#292)
- Add support to read the sealed (`encp`) workflow payloads (X25519 + AES-GCM) an outside writer addresses to a run, under the `encryption` extra. (#297)
Bug Fixes
---------
- Remove upper bounds on aggregate Sandbox and Workflow dependencies so sibling releases cannot make the `vercel` package un-installable. (#334)
- Start a workflow run even when its queue message arrives before the `run_created` event has landed. (#284)
Internal
--------
- The Workflows implementation now ships in the separate `vercel-workflow` distribution, which `vercel` depends on, so `vercel.workflow` imports keep working without installing anything extra. (#299)
vercel-apscheduler
------------------
0.3.0 - 2026-08-31
------------------
Breaking Changes
----------------
- The managed Redis backend was removed. The integration now always runs on its managed job store (Vercel Runtime Cache); a configured default `RedisJobStore` is rejected at import, `VERCEL_APSCHEDULER_BACKEND` accepts only `cache`, and the `redis` dependency is gone. The scheduler's durable identity now always derives from the builder-assigned subscriber id (previously the Redis `jobs_key`); the `scheduler_id` option still pins an identity explicitly. (#286)
vercel-celery
-------------
0.7.5 - 2026-08-31
------------------
- Update dependencies.
vercel-django-tasks
-------------------
0.7.0 - 2026-08-31
------------------
Features
--------
- Add a Vercel Queues backend for Django Tasks and use it by default when no task backends are configured. (#291)
vercel-dramatiq
---------------
0.7.4 - 2026-08-31
------------------
- Update dependencies.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@pranaygp@fantix
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' workflow: read spec 7 and skip sealed noops by pranaygp · Pull Request #319 · vercel/vercel-py · GitHub
Skip to content

workflow: read spec 7 and skip sealed noops - #319

Merged
fantix merged 4 commits into
mainfrom
pgp/port-noop-event-to-py
Aug 24, 2026
Merged

workflow: read spec 7 and skip sealed noops#319
fantix merged 4 commits into
mainfrom
pgp/port-noop-event-to-py

Conversation

@pranaygp

Copy link
Copy Markdown
Contributor

Python half of vercel/workflow#3634 (Add support for 'noop' event type — spec version 7).

Why this is urgent for us

That PR moves SPEC_VERSION_CURRENT and SPEC_VERSION_MAX_SUPPORTED to 7 and switches both @workflow/world-local and @workflow/world-vercel to mintedSpecVersion(), which returns 7 by default. Our reader ceiling was 6, and it is a pydantic le= on BaseEvent.specVersion — so a spec-7 row does not degrade, it fails validation, and with it the whole page. The e2e-python conformance lane runs the TypeScript driver against workbench/python over a shared world-local data dir, so it is the first thing that breaks: the driver's run_created is now labelled 7 and the Python app cannot parse the log it is supposed to replay.

What spec 7 actually asks of a client

On the server, a spec-7 run's slot positions come from a per-run counter handed out before the write that fills them, so concurrent writers never race for a position. The price is that a writer that takes a position and dies leaves a hole, and the backend's read path fills provably-abandoned holes with a server-written noop so returned pages stay dense prefixes.

So the client contract is one thing, and it is read-only: a reader must know a noop occupies its slot and means nothing.

There is no writer half for us. A World only owes the sealing half if it pre-assigns positions ahead of the commit; our local world allocates each id at the commit that fills it, so it has no holes to seal and will never emit one.

The change

world.py

  • SPEC_VERSION_SUPPORTS_SEALED_LOG = 7, and SPEC_VERSION_MAX_SUPPORTED follows it. SPEC_VERSION_CURRENT stays 2 — we still stamp what we always stamped, and a run created elsewhere keeps its creator's version.
  • NoopEvent / NoopEventData, in the read union and deliberately not in CreateEventRequest. eventData is optional and open (extra="allow", matching the .passthrough() on the TS schema): nothing reads it, the shape belongs to whichever backend sealed the slot, and a reader whose only interest is skipping the row has no business rejecting a field it has not heard of.
  • is_sealed_noop_event(), mirroring isSealedNoopEvent — one home for the test.

runtime.pyget_all_workflow_run_events() drops seals, so the log the replay walks is the log without them.

That is the whole skip, and the reason it is one line at the loader rather than a continue in resume() is the clock. Everything downstream of that function is reconstructing what the run did — matching correlation ids to suspensions, looking for a terminal event, deciding which waits elapsed, and reading createdAt for now(). Nothing on this side walks positions, so there is no caller that wants the seal. Meanwhile a seal's createdAt is the sealer's wall clock, which can postdate every real event around it; now() dates the run from the last event the replay consumed, so a seal left in the list hands the body a time no event of the run happened at — and then, because the clock only moves forward, every later now() too. Dropping it at the boundary makes the equivalence the contract is about (a sealed log replays exactly like the same log whose holes their own writers filled) hold by construction, rather than by three call sites remembering. The cursor is untouched: it is the World's, and still points past every row that was read.

Tests

tests/unit/test_workflow_sealed_log_noop.py, 12 tests in four parts:

  • the row — parses out of the read union with eventData, without it, and with an eventData field we have never heard of; the predicate answers for seals only; NoopEvent is in Event and absent from CreateEventRequest; 7 is the ceiling and 8 is still rejected.
  • storage — a seal round-trips through the local world and numbering continues past it, mirroring slot-identity.test.ts's stores, lists, and numbers past a noop event.
  • the loader — seals at head, middle (consecutive) and tail are dropped with the order of everything else intact; a page of nothing but seals still pages on; the cursor survives.
  • replay — the real claim, driven end to end through workflow_handler: one recorded two-sleep run, twice, differing only in seals at the head, in the middle (including one splitting a wait_created/wait_completed pair), and at the tail. Same output, same events written back, and the three now() readings pinned literally so a change that broke both logs the same way cannot slip past the comparison. Plus the degenerate all-seals-before-the-run shape, where the first now() has to find the first real event.

Every seal in the file is stamped a minute into the future, so each of those clock assertions would pass by accident if a seal's createdAt were plausible. Reverting the one-line filter fails 5 of the 12.

test_workflow_local_world_format.py's two spec-version tests move 6 → 7; the ceiling assertion now derives its match from SPEC_VERSION_MAX_SUPPORTED instead of hardcoding the number, so the next bump only has to touch the constant.

Full suite: 1127 tests, 0 failures (test_the_api_token_is_config_then_env_then_oidc fails locally on a box with VERCEL_TOKEN exported, before and after this change). Lint and typecheck clean repo-wide.

Follow-up, not in this PR

workbench/python/pyproject.toml in vercel/workflow resolves vercel-workflow from PyPI (0.9.0 today) regardless of its [tool.uv.sources] rev, so the e2e-python lane only picks this up once it is released and the lock is refreshed there — or once that file git-pins vercel-workflow at a commit containing it, which is what the comment block above that source entry describes.

🤖 Generated with Claude Code

`@workflow/world-local` and `@workflow/world-vercel` both stamp
specVersion 7 now (vercel/workflow#3634), so every event a TypeScript
driver writes arrives labelled 7 and our reader ceiling of 6 rejected
the whole log -- including on the local world the Python e2e lane runs
on. Raise the ceiling, and add the one thing the version gates: a
`noop` row, written by a World's backend to occupy a slot whose writer
allocated the position and then died.
A noop parses out of the read union but is dropped from the log the
replay walks. Its `createdAt` is the sealer's wall clock, which can
postdate every real event around it, so letting it reach the
deterministic clock would make a log whose hole was sealed replay
differently from the same log whose hole its own writer filled.
Nothing here writes one: we allocate each event id at the commit that
fills it, so we have no holes to seal, and `noop` stays out of the
create union.
Co-Authored-By: Pranay Prakash <1797812+pranaygp@users.noreply.github.com>

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@fantixfantix left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM; let me trim off some AI in-code chats and merge this to unbreak the e2e test.

@fantix
fantixforce-pushed the pgp/port-noop-event-to-py branch from 3116c4e to abb6ce9CompareAugust 24, 2026 18:44
@fantix
fantix merged commit a5969aa into mainAug 24, 2026
14 checks passed
@fantix
fantix deleted the pgp/port-noop-event-to-py branch August 24, 2026 18:49
@scotttrinhscotttrinh mentioned this pull request Aug 26, 2026
scotttrinh added a commit that referenced this pull request Aug 26, 2026
vercel-internal-core
--------------------
0.1.3 - 2026-08-26
------------------
Internal
--------
- Support disabling HTTP timeouts for selected SDK operations while preserving the client default elsewhere. (#307)
vercel-connect
--------------
0.1.1 - 2026-08-26
------------------
- Update dependencies.
vercel-queue
------------
0.8.1 - 2026-08-26
------------------
Documentation
-------------
- Remove documentation and examples for `asgi_app` in preparation for its removal. (#309)
vercel-sandbox
--------------
0.5.0 - 2026-08-26
------------------
Features
--------
- Add sync and async `fork_sandbox(...)` support for creating a sandbox from an existing named sandbox with optional configuration overrides. (#257)
- Add `region` and `failover_regions` configuration for sandbox creation, forks, and updates, plus multi-region snapshot availability reporting. (#308)
Bug Fixes
---------
- Allow Sandbox process waits and log streams to remain idle longer than the session HTTP timeout. (#307)
vercel-workflow
---------------
0.10.0 - 2026-08-26
-------------------
Breaking Changes
----------------
- Use type annotations on workflows and step to allow passing Pydantic models and dataclasses. (#317)
- This is a breaking change, because type annotations will now be enforced. Passing a `dict` when the declaration expects a `list` will fail. (#317)
- Pydantic models and dataclasses can no longer be passed to `@serializable` or `register_serializable()`. Annotate the workflow or step parameter or return value with their type instead. (#317)
Features
--------
- `get_workflow_metadata()` returns the current run's `WorkflowInfo` (run id, workflow name, start time, deployment URL, and feature flags), callable from a workflow body or a step body — mirroring the JS SDK's `getWorkflowMetadata()`. (#320)
- One current limitation is that `started_at` is `None` from inside a step. (#320)
- `BaseHook.wait()` accepts `metadata` to record on the hook, and `get_hook_by_token()` reads it back for a resumer. (#301)
- A step can raise `RetryableError` to control when its next attempt runs. (#302)
- Accept `specVersion` 7 sealed noop event logs. (#319)
- A workflow or step can attach plaintext metadata to its run with `set_attributes()`. (#303)
- Add a `share_sandboxes` parameter to `SandboxPolicy` to enable reusing already created sandboxes instead of creating a new one on each invocation. This speeds up workflows but means that modifications to global state may persist between invocations. (#310)
- Expose unstable API to serve workflow HTTP endpoint from your own web framework. (#294)
- Added semi-internal manifest API for TS tools and e2e test. (#296)
Bug Fixes
---------
- Fix failing or even crashing cipher calls inside the workflow sandbox. (#305)
- Support resuming hooks with payload in the queue message. (#300)
- Fixed nulls rejected by server, requiring Pydantic 2.12 or newer. (#321)
- Fixed workflow and step calls with both positional-or-keyword parameters and `*args` failing during replay because their arguments were recorded in an unbindable shape. (#312)
Internal
--------
- Construct the protocol models by Python field name. (#322)
vercel
------
0.11.0 - 2026-08-26
-------------------
Features
--------
- Expose `get_deadline()` for reading the current Function invocation deadline. (#306)
- Answer workflow health checks for both queue-based transport and HTTP. (#292)
- Add support to read the sealed (`encp`) workflow payloads (X25519 + AES-GCM) an outside writer addresses to a run, under the `encryption` extra. (#297)
Bug Fixes
---------
- Remove upper bounds on aggregate Sandbox and Workflow dependencies so sibling releases cannot make the `vercel` package un-installable. (#334)
- Start a workflow run even when its queue message arrives before the `run_created` event has landed. (#284)
Internal
--------
- The Workflows implementation now ships in the separate `vercel-workflow` distribution, which `vercel` depends on, so `vercel.workflow` imports keep working without installing anything extra. (#299)
vercel-apscheduler
------------------
0.3.0 - 2026-08-26
------------------
Breaking Changes
----------------
- The managed Redis backend was removed. The integration now always runs on its managed job store (Vercel Runtime Cache); a configured default `RedisJobStore` is rejected at import, `VERCEL_APSCHEDULER_BACKEND` accepts only `cache`, and the `redis` dependency is gone. The scheduler's durable identity now always derives from the builder-assigned subscriber id (previously the Redis `jobs_key`); the `scheduler_id` option still pins an identity explicitly. (#286)
vercel-celery
-------------
0.7.5 - 2026-08-26
------------------
- Update dependencies.
vercel-django-tasks
-------------------
0.7.0 - 2026-08-26
------------------
Features
--------
- Add a Vercel Queues backend for Django Tasks and use it by default when no task backends are configured. (#291)
vercel-dramatiq
---------------
0.7.4 - 2026-08-26
------------------
- Update dependencies.
scotttrinh added a commit that referenced this pull request Aug 31, 2026
vercel-headers
--------------
0.7.2 - 2026-08-31
------------------
Bug Fixes
---------
- Accept request objects with concrete header implementations in the IP address and geolocation type annotations. (#337)
vercel-internal-core
--------------------
0.1.3 - 2026-08-31
------------------
Internal
--------
- Support disabling HTTP timeouts for selected SDK operations while preserving the client default elsewhere. (#307)
vercel-oidc
-----------
0.8.1 - 2026-08-31
------------------
- Update dependencies.
vercel-connect
--------------
0.1.1 - 2026-08-31
------------------
- Update dependencies.
vercel-internal-telemetry
-------------------------
0.7.3 - 2026-08-31
------------------
- Update dependencies.
vercel-queue
------------
0.8.1 - 2026-08-31
------------------
Bug Fixes
---------
- Force embedded development servers to exit when graceful shutdown stalls. (#351)
Documentation
-------------
- Remove documentation and examples for `asgi_app` in preparation for its removal. (#309)
vercel-sandbox
--------------
0.5.0 - 2026-08-31
------------------
Features
--------
- Add sync and async `fork_sandbox(...)` support for creating a sandbox from an existing named sandbox with optional configuration overrides. (#257)
- Add `region` and `failover_regions` configuration for sandbox creation, forks, and updates, plus multi-region snapshot availability reporting. (#308)
- Forward private ``__``-prefixed parameters to the Sandbox API. (#350)
Bug Fixes
---------
- Allow Sandbox process waits and log streams to remain idle longer than the session HTTP timeout. (#307)
- Expose Linux process signals consistently on every SDK host platform. (#352)
vercel-cache
------------
0.7.3 - 2026-08-31
------------------
- Update dependencies.
vercel-workflow
---------------
0.10.0 - 2026-08-31
-------------------
Breaking Changes
----------------
- Make sleep() and retry delays treat numbers as seconds, not ms (#346)
- This matches Python standard library APIs. (#346)
- Use type annotations on workflows and step to allow passing Pydantic models and dataclasses. (#317)
- This is a breaking change, because type annotations will now be enforced. Passing a `dict` when the declaration expects a `list` will fail. (#317)
- Pydantic models and dataclasses can no longer be passed to `@serializable` or `register_serializable()`. Annotate the workflow or step parameter or return value with their type instead. (#317)
Features
--------
- Support `call_later`, `call_at`, and `now` in the event loop implementation. (#343)
- This enables use of `asyncio.sleep()` as well as `asyncio.timeout` and the `timeout` parameter of `asyncio.wait_for`. (#343)
- `get_workflow_metadata()` returns the current run's `WorkflowInfo` (run id, workflow name, start time, deployment URL, and feature flags), callable from a workflow body or a step body — mirroring the JS SDK's `getWorkflowMetadata()`. (#320)
- One current limitation is that `started_at` is `None` from inside a step. (#320)
- `BaseHook.wait()` accepts `metadata` to record on the hook, and `get_hook_by_token()` reads it back for a resumer. (#301)
- A step can raise `RetryableError` to control when its next attempt runs. (#302)
- Accept `specVersion` 7 sealed noop event logs. (#319)
- Failed run and step events now preserve serialized error classes, messages, stacks, and causes. Failed runs also expose a plaintext `errorCode`. (#304)
- A workflow or step can attach plaintext metadata to its run with `set_attributes()`. (#303)
- Add a `share_sandboxes` parameter to `SandboxPolicy` to enable reusing already created sandboxes instead of creating a new one on each invocation. This speeds up workflows but means that modifications to global state may persist between invocations. (#310)
- Support `timedelta` arguments for workflow `sleep()` and retry delays. (#342)
- Expose unstable API to serve workflow HTTP endpoint from your own web framework. (#294)
- Added semi-internal manifest API for TS tools and e2e test. (#296)
Bug Fixes
---------
- Fix failing or even crashing cipher calls inside the workflow sandbox. (#305)
- Fail a workflow run with `HookConflictError` when another run already owns its hook token instead of leaving it running indefinitely. (#327)
- Support resuming hooks with payload in the queue message. (#300)
- Fix some bugs involving hooks arriving when the workflow was not yet blocked on them. (#339)
- Fixed nulls rejected by server, requiring Pydantic 2.12 or newer. (#321)
- Prevent workflows from having side effects while suspending. (#332)
- `hook.dispose()` will now work properly in a `finally` block. (That is, the hook will be disposed only when the workflow is actually terminating, and not every time it gets replayed.) (#332)
- More reliably fail runs whose replay diverges from the event log. (#347)
- Runs will now fail even in the case where the main thread of execution is not directly blocked on the suspension that is erroring. (#347)
- Fixed workflow and step calls with both positional-or-keyword parameters and `*args` failing during replay because their arguments were recorded in an unbindable shape. (#312)
Internal
--------
- Remove a just-added return from a finally block. (#344)
- Correct internal workflow type annotations found by checking untyped function bodies. (#337)
- Refactored event replay. (#341)
- Construct the protocol models by Python field name. (#322)
vercel
------
0.11.0 - 2026-08-31
-------------------
Features
--------
- Expose `get_deadline()` for reading the current Function invocation deadline. (#306)
- Answer workflow health checks for both queue-based transport and HTTP. (#292)
- Add support to read the sealed (`encp`) workflow payloads (X25519 + AES-GCM) an outside writer addresses to a run, under the `encryption` extra. (#297)
Bug Fixes
---------
- Remove upper bounds on aggregate Sandbox and Workflow dependencies so sibling releases cannot make the `vercel` package un-installable. (#334)
- Start a workflow run even when its queue message arrives before the `run_created` event has landed. (#284)
Internal
--------
- The Workflows implementation now ships in the separate `vercel-workflow` distribution, which `vercel` depends on, so `vercel.workflow` imports keep working without installing anything extra. (#299)
vercel-apscheduler
------------------
0.3.0 - 2026-08-31
------------------
Breaking Changes
----------------
- The managed Redis backend was removed. The integration now always runs on its managed job store (Vercel Runtime Cache); a configured default `RedisJobStore` is rejected at import, `VERCEL_APSCHEDULER_BACKEND` accepts only `cache`, and the `redis` dependency is gone. The scheduler's durable identity now always derives from the builder-assigned subscriber id (previously the Redis `jobs_key`); the `scheduler_id` option still pins an identity explicitly. (#286)
vercel-celery
-------------
0.7.5 - 2026-08-31
------------------
- Update dependencies.
vercel-django-tasks
-------------------
0.7.0 - 2026-08-31
------------------
Features
--------
- Add a Vercel Queues backend for Django Tasks and use it by default when no task backends are configured. (#291)
vercel-dramatiq
---------------
0.7.4 - 2026-08-31
------------------
- Update dependencies.
scotttrinh added a commit that referenced this pull request Aug 31, 2026
vercel-headers
--------------
0.7.2 - 2026-08-31
------------------
Bug Fixes
---------
- Accept request objects with concrete header implementations in the IP address and geolocation type annotations. (#337)
vercel-internal-core
--------------------
0.1.3 - 2026-08-31
------------------
Internal
--------
- Support disabling HTTP timeouts for selected SDK operations while preserving the client default elsewhere. (#307)
vercel-oidc
-----------
0.8.1 - 2026-08-31
------------------
- Update dependencies.
vercel-connect
--------------
0.1.1 - 2026-08-31
------------------
- Update dependencies.
vercel-internal-telemetry
-------------------------
0.7.3 - 2026-08-31
------------------
- Update dependencies.
vercel-queue
------------
0.8.1 - 2026-08-31
------------------
Bug Fixes
---------
- Force embedded development servers to exit when graceful shutdown stalls. (#351)
Documentation
-------------
- Remove documentation and examples for `asgi_app` in preparation for its removal. (#309)
vercel-sandbox
--------------
0.5.0 - 2026-08-31
------------------
Features
--------
- Add sync and async `fork_sandbox(...)` support for creating a sandbox from an existing named sandbox with optional configuration overrides. (#257)
- Add `region` and `failover_regions` configuration for sandbox creation, forks, and updates, plus multi-region snapshot availability reporting. (#308)
- Forward private ``__``-prefixed parameters to the Sandbox API. (#350)
Bug Fixes
---------
- Allow Sandbox process waits and log streams to remain idle longer than the session HTTP timeout. (#307)
- Expose Linux process signals consistently on every SDK host platform. (#352)
vercel-cache
------------
0.7.3 - 2026-08-31
------------------
- Update dependencies.
vercel-workflow
---------------
0.10.0 - 2026-08-31
-------------------
Breaking Changes
----------------
- Make `await hook` never return `None`
- Raises a new `HookDisposedError` instead of returning `None` when the hook has been disposed. It is now typed to return `T` instead of `T | None`. `async for` over a hook will stop iterating on disposal, still.
- Make sleep() and retry delays treat numbers as seconds, not ms (#346)
- This matches Python standard library APIs. (#346)
- Use type annotations on workflows and step to allow passing Pydantic models and dataclasses. (#317)
- This is a breaking change, because type annotations will now be enforced. Passing a `dict` when the declaration expects a `list` will fail. (#317)
- Pydantic models and dataclasses can no longer be passed to `@serializable` or `register_serializable()`. Annotate the workflow or step parameter or return value with their type instead. (#317)
Features
--------
- Support `call_later`, `call_at`, and `now` in the event loop implementation. (#343)
- This enables use of `asyncio.sleep()` as well as `asyncio.timeout` and the `timeout` parameter of `asyncio.wait_for`. (#343)
- `get_workflow_metadata()` returns the current run's `WorkflowInfo` (run id, workflow name, start time, deployment URL, and feature flags), callable from a workflow body or a step body — mirroring the JS SDK's `getWorkflowMetadata()`. (#320)
- One current limitation is that `started_at` is `None` from inside a step. (#320)
- Make `HookEvent` an async context manager
- This matches TS, which supports `using`. ``` # disposes the hook on block exit async with SomeHook.wait(...) as hook: res = await hook ```
- `BaseHook.wait()` accepts `metadata` to record on the hook, and `get_hook_by_token()` reads it back for a resumer. (#301)
- A step can raise `RetryableError` to control when its next attempt runs. (#302)
- Accept `specVersion` 7 sealed noop event logs. (#319)
- Failed run and step events now preserve serialized error classes, messages, stacks, and causes. Failed runs also expose a plaintext `errorCode`. (#304)
- A workflow or step can attach plaintext metadata to its run with `set_attributes()`. (#303)
- Add a `share_sandboxes` parameter to `SandboxPolicy` to enable reusing already created sandboxes instead of creating a new one on each invocation. This speeds up workflows but means that modifications to global state may persist between invocations. (#310)
- Support `timedelta` arguments for workflow `sleep()` and retry delays. (#342)
- Expose unstable API to serve workflow HTTP endpoint from your own web framework. (#294)
- Added semi-internal manifest API for TS tools and e2e test. (#296)
Bug Fixes
---------
- Fix failing or even crashing cipher calls inside the workflow sandbox. (#305)
- Fail a workflow run with `HookConflictError` when another run already owns its hook token instead of leaving it running indefinitely. (#327)
- Support resuming hooks with payload in the queue message. (#300)
- Fix some bugs involving hooks arriving when the workflow was not yet blocked on them. (#339)
- Fixed nulls rejected by server, requiring Pydantic 2.12 or newer. (#321)
- Prevent workflows from having side effects while suspending. (#332)
- `hook.dispose()` will now work properly in a `finally` block. (That is, the hook will be disposed only when the workflow is actually terminating, and not every time it gets replayed.) (#332)
- More reliably fail runs whose replay diverges from the event log. (#347)
- Runs will now fail even in the case where the main thread of execution is not directly blocked on the suspension that is erroring. (#347)
- Fixed workflow and step calls with both positional-or-keyword parameters and `*args` failing during replay because their arguments were recorded in an unbindable shape. (#312)
Internal
--------
- Remove a just-added return from a finally block. (#344)
- Correct internal workflow type annotations found by checking untyped function bodies. (#337)
- Refactored event replay. (#341)
- Construct the protocol models by Python field name. (#322)
vercel
------
0.11.0 - 2026-08-31
-------------------
Features
--------
- Expose `get_deadline()` for reading the current Function invocation deadline. (#306)
- Answer workflow health checks for both queue-based transport and HTTP. (#292)
- Add support to read the sealed (`encp`) workflow payloads (X25519 + AES-GCM) an outside writer addresses to a run, under the `encryption` extra. (#297)
Bug Fixes
---------
- Remove upper bounds on aggregate Sandbox and Workflow dependencies so sibling releases cannot make the `vercel` package un-installable. (#334)
- Start a workflow run even when its queue message arrives before the `run_created` event has landed. (#284)
Internal
--------
- The Workflows implementation now ships in the separate `vercel-workflow` distribution, which `vercel` depends on, so `vercel.workflow` imports keep working without installing anything extra. (#299)
vercel-apscheduler
------------------
0.3.0 - 2026-08-31
------------------
Breaking Changes
----------------
- The managed Redis backend was removed. The integration now always runs on its managed job store (Vercel Runtime Cache); a configured default `RedisJobStore` is rejected at import, `VERCEL_APSCHEDULER_BACKEND` accepts only `cache`, and the `redis` dependency is gone. The scheduler's durable identity now always derives from the builder-assigned subscriber id (previously the Redis `jobs_key`); the `scheduler_id` option still pins an identity explicitly. (#286)
vercel-celery
-------------
0.7.5 - 2026-08-31
------------------
- Update dependencies.
vercel-django-tasks
-------------------
0.7.0 - 2026-08-31
------------------
Features
--------
- Add a Vercel Queues backend for Django Tasks and use it by default when no task backends are configured. (#291)
vercel-dramatiq
---------------
0.7.4 - 2026-08-31
------------------
- Update dependencies.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@pranaygp@fantix
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' workflow: read spec 7 and skip sealed noops by pranaygp · Pull Request #319 · vercel/vercel-py · GitHub
Skip to content

workflow: read spec 7 and skip sealed noops - #319

Merged
fantix merged 4 commits into
mainfrom
pgp/port-noop-event-to-py
Aug 24, 2026
Merged

workflow: read spec 7 and skip sealed noops#319
fantix merged 4 commits into
mainfrom
pgp/port-noop-event-to-py

Conversation

@pranaygp

Copy link
Copy Markdown
Contributor

Python half of vercel/workflow#3634 (Add support for 'noop' event type — spec version 7).

Why this is urgent for us

That PR moves SPEC_VERSION_CURRENT and SPEC_VERSION_MAX_SUPPORTED to 7 and switches both @workflow/world-local and @workflow/world-vercel to mintedSpecVersion(), which returns 7 by default. Our reader ceiling was 6, and it is a pydantic le= on BaseEvent.specVersion — so a spec-7 row does not degrade, it fails validation, and with it the whole page. The e2e-python conformance lane runs the TypeScript driver against workbench/python over a shared world-local data dir, so it is the first thing that breaks: the driver's run_created is now labelled 7 and the Python app cannot parse the log it is supposed to replay.

What spec 7 actually asks of a client

On the server, a spec-7 run's slot positions come from a per-run counter handed out before the write that fills them, so concurrent writers never race for a position. The price is that a writer that takes a position and dies leaves a hole, and the backend's read path fills provably-abandoned holes with a server-written noop so returned pages stay dense prefixes.

So the client contract is one thing, and it is read-only: a reader must know a noop occupies its slot and means nothing.

There is no writer half for us. A World only owes the sealing half if it pre-assigns positions ahead of the commit; our local world allocates each id at the commit that fills it, so it has no holes to seal and will never emit one.

The change

world.py

  • SPEC_VERSION_SUPPORTS_SEALED_LOG = 7, and SPEC_VERSION_MAX_SUPPORTED follows it. SPEC_VERSION_CURRENT stays 2 — we still stamp what we always stamped, and a run created elsewhere keeps its creator's version.
  • NoopEvent / NoopEventData, in the read union and deliberately not in CreateEventRequest. eventData is optional and open (extra="allow", matching the .passthrough() on the TS schema): nothing reads it, the shape belongs to whichever backend sealed the slot, and a reader whose only interest is skipping the row has no business rejecting a field it has not heard of.
  • is_sealed_noop_event(), mirroring isSealedNoopEvent — one home for the test.

runtime.pyget_all_workflow_run_events() drops seals, so the log the replay walks is the log without them.

That is the whole skip, and the reason it is one line at the loader rather than a continue in resume() is the clock. Everything downstream of that function is reconstructing what the run did — matching correlation ids to suspensions, looking for a terminal event, deciding which waits elapsed, and reading createdAt for now(). Nothing on this side walks positions, so there is no caller that wants the seal. Meanwhile a seal's createdAt is the sealer's wall clock, which can postdate every real event around it; now() dates the run from the last event the replay consumed, so a seal left in the list hands the body a time no event of the run happened at — and then, because the clock only moves forward, every later now() too. Dropping it at the boundary makes the equivalence the contract is about (a sealed log replays exactly like the same log whose holes their own writers filled) hold by construction, rather than by three call sites remembering. The cursor is untouched: it is the World's, and still points past every row that was read.

Tests

tests/unit/test_workflow_sealed_log_noop.py, 12 tests in four parts:

  • the row — parses out of the read union with eventData, without it, and with an eventData field we have never heard of; the predicate answers for seals only; NoopEvent is in Event and absent from CreateEventRequest; 7 is the ceiling and 8 is still rejected.
  • storage — a seal round-trips through the local world and numbering continues past it, mirroring slot-identity.test.ts's stores, lists, and numbers past a noop event.
  • the loader — seals at head, middle (consecutive) and tail are dropped with the order of everything else intact; a page of nothing but seals still pages on; the cursor survives.
  • replay — the real claim, driven end to end through workflow_handler: one recorded two-sleep run, twice, differing only in seals at the head, in the middle (including one splitting a wait_created/wait_completed pair), and at the tail. Same output, same events written back, and the three now() readings pinned literally so a change that broke both logs the same way cannot slip past the comparison. Plus the degenerate all-seals-before-the-run shape, where the first now() has to find the first real event.

Every seal in the file is stamped a minute into the future, so each of those clock assertions would pass by accident if a seal's createdAt were plausible. Reverting the one-line filter fails 5 of the 12.

test_workflow_local_world_format.py's two spec-version tests move 6 → 7; the ceiling assertion now derives its match from SPEC_VERSION_MAX_SUPPORTED instead of hardcoding the number, so the next bump only has to touch the constant.

Full suite: 1127 tests, 0 failures (test_the_api_token_is_config_then_env_then_oidc fails locally on a box with VERCEL_TOKEN exported, before and after this change). Lint and typecheck clean repo-wide.

Follow-up, not in this PR

workbench/python/pyproject.toml in vercel/workflow resolves vercel-workflow from PyPI (0.9.0 today) regardless of its [tool.uv.sources] rev, so the e2e-python lane only picks this up once it is released and the lock is refreshed there — or once that file git-pins vercel-workflow at a commit containing it, which is what the comment block above that source entry describes.

🤖 Generated with Claude Code

`@workflow/world-local` and `@workflow/world-vercel` both stamp
specVersion 7 now (vercel/workflow#3634), so every event a TypeScript
driver writes arrives labelled 7 and our reader ceiling of 6 rejected
the whole log -- including on the local world the Python e2e lane runs
on. Raise the ceiling, and add the one thing the version gates: a
`noop` row, written by a World's backend to occupy a slot whose writer
allocated the position and then died.
A noop parses out of the read union but is dropped from the log the
replay walks. Its `createdAt` is the sealer's wall clock, which can
postdate every real event around it, so letting it reach the
deterministic clock would make a log whose hole was sealed replay
differently from the same log whose hole its own writer filled.
Nothing here writes one: we allocate each event id at the commit that
fills it, so we have no holes to seal, and `noop` stays out of the
create union.
Co-Authored-By: Pranay Prakash <1797812+pranaygp@users.noreply.github.com>

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@fantixfantix left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM; let me trim off some AI in-code chats and merge this to unbreak the e2e test.

@fantix
fantixforce-pushed the pgp/port-noop-event-to-py branch from 3116c4e to abb6ce9CompareAugust 24, 2026 18:44
@fantix
fantix merged commit a5969aa into mainAug 24, 2026
14 checks passed
@fantix
fantix deleted the pgp/port-noop-event-to-py branch August 24, 2026 18:49
@scotttrinhscotttrinh mentioned this pull request Aug 26, 2026
scotttrinh added a commit that referenced this pull request Aug 26, 2026
vercel-internal-core
--------------------
0.1.3 - 2026-08-26
------------------
Internal
--------
- Support disabling HTTP timeouts for selected SDK operations while preserving the client default elsewhere. (#307)
vercel-connect
--------------
0.1.1 - 2026-08-26
------------------
- Update dependencies.
vercel-queue
------------
0.8.1 - 2026-08-26
------------------
Documentation
-------------
- Remove documentation and examples for `asgi_app` in preparation for its removal. (#309)
vercel-sandbox
--------------
0.5.0 - 2026-08-26
------------------
Features
--------
- Add sync and async `fork_sandbox(...)` support for creating a sandbox from an existing named sandbox with optional configuration overrides. (#257)
- Add `region` and `failover_regions` configuration for sandbox creation, forks, and updates, plus multi-region snapshot availability reporting. (#308)
Bug Fixes
---------
- Allow Sandbox process waits and log streams to remain idle longer than the session HTTP timeout. (#307)
vercel-workflow
---------------
0.10.0 - 2026-08-26
-------------------
Breaking Changes
----------------
- Use type annotations on workflows and step to allow passing Pydantic models and dataclasses. (#317)
- This is a breaking change, because type annotations will now be enforced. Passing a `dict` when the declaration expects a `list` will fail. (#317)
- Pydantic models and dataclasses can no longer be passed to `@serializable` or `register_serializable()`. Annotate the workflow or step parameter or return value with their type instead. (#317)
Features
--------
- `get_workflow_metadata()` returns the current run's `WorkflowInfo` (run id, workflow name, start time, deployment URL, and feature flags), callable from a workflow body or a step body — mirroring the JS SDK's `getWorkflowMetadata()`. (#320)
- One current limitation is that `started_at` is `None` from inside a step. (#320)
- `BaseHook.wait()` accepts `metadata` to record on the hook, and `get_hook_by_token()` reads it back for a resumer. (#301)
- A step can raise `RetryableError` to control when its next attempt runs. (#302)
- Accept `specVersion` 7 sealed noop event logs. (#319)
- A workflow or step can attach plaintext metadata to its run with `set_attributes()`. (#303)
- Add a `share_sandboxes` parameter to `SandboxPolicy` to enable reusing already created sandboxes instead of creating a new one on each invocation. This speeds up workflows but means that modifications to global state may persist between invocations. (#310)
- Expose unstable API to serve workflow HTTP endpoint from your own web framework. (#294)
- Added semi-internal manifest API for TS tools and e2e test. (#296)
Bug Fixes
---------
- Fix failing or even crashing cipher calls inside the workflow sandbox. (#305)
- Support resuming hooks with payload in the queue message. (#300)
- Fixed nulls rejected by server, requiring Pydantic 2.12 or newer. (#321)
- Fixed workflow and step calls with both positional-or-keyword parameters and `*args` failing during replay because their arguments were recorded in an unbindable shape. (#312)
Internal
--------
- Construct the protocol models by Python field name. (#322)
vercel
------
0.11.0 - 2026-08-26
-------------------
Features
--------
- Expose `get_deadline()` for reading the current Function invocation deadline. (#306)
- Answer workflow health checks for both queue-based transport and HTTP. (#292)
- Add support to read the sealed (`encp`) workflow payloads (X25519 + AES-GCM) an outside writer addresses to a run, under the `encryption` extra. (#297)
Bug Fixes
---------
- Remove upper bounds on aggregate Sandbox and Workflow dependencies so sibling releases cannot make the `vercel` package un-installable. (#334)
- Start a workflow run even when its queue message arrives before the `run_created` event has landed. (#284)
Internal
--------
- The Workflows implementation now ships in the separate `vercel-workflow` distribution, which `vercel` depends on, so `vercel.workflow` imports keep working without installing anything extra. (#299)
vercel-apscheduler
------------------
0.3.0 - 2026-08-26
------------------
Breaking Changes
----------------
- The managed Redis backend was removed. The integration now always runs on its managed job store (Vercel Runtime Cache); a configured default `RedisJobStore` is rejected at import, `VERCEL_APSCHEDULER_BACKEND` accepts only `cache`, and the `redis` dependency is gone. The scheduler's durable identity now always derives from the builder-assigned subscriber id (previously the Redis `jobs_key`); the `scheduler_id` option still pins an identity explicitly. (#286)
vercel-celery
-------------
0.7.5 - 2026-08-26
------------------
- Update dependencies.
vercel-django-tasks
-------------------
0.7.0 - 2026-08-26
------------------
Features
--------
- Add a Vercel Queues backend for Django Tasks and use it by default when no task backends are configured. (#291)
vercel-dramatiq
---------------
0.7.4 - 2026-08-26
------------------
- Update dependencies.
scotttrinh added a commit that referenced this pull request Aug 31, 2026
vercel-headers
--------------
0.7.2 - 2026-08-31
------------------
Bug Fixes
---------
- Accept request objects with concrete header implementations in the IP address and geolocation type annotations. (#337)
vercel-internal-core
--------------------
0.1.3 - 2026-08-31
------------------
Internal
--------
- Support disabling HTTP timeouts for selected SDK operations while preserving the client default elsewhere. (#307)
vercel-oidc
-----------
0.8.1 - 2026-08-31
------------------
- Update dependencies.
vercel-connect
--------------
0.1.1 - 2026-08-31
------------------
- Update dependencies.
vercel-internal-telemetry
-------------------------
0.7.3 - 2026-08-31
------------------
- Update dependencies.
vercel-queue
------------
0.8.1 - 2026-08-31
------------------
Bug Fixes
---------
- Force embedded development servers to exit when graceful shutdown stalls. (#351)
Documentation
-------------
- Remove documentation and examples for `asgi_app` in preparation for its removal. (#309)
vercel-sandbox
--------------
0.5.0 - 2026-08-31
------------------
Features
--------
- Add sync and async `fork_sandbox(...)` support for creating a sandbox from an existing named sandbox with optional configuration overrides. (#257)
- Add `region` and `failover_regions` configuration for sandbox creation, forks, and updates, plus multi-region snapshot availability reporting. (#308)
- Forward private ``__``-prefixed parameters to the Sandbox API. (#350)
Bug Fixes
---------
- Allow Sandbox process waits and log streams to remain idle longer than the session HTTP timeout. (#307)
- Expose Linux process signals consistently on every SDK host platform. (#352)
vercel-cache
------------
0.7.3 - 2026-08-31
------------------
- Update dependencies.
vercel-workflow
---------------
0.10.0 - 2026-08-31
-------------------
Breaking Changes
----------------
- Make sleep() and retry delays treat numbers as seconds, not ms (#346)
- This matches Python standard library APIs. (#346)
- Use type annotations on workflows and step to allow passing Pydantic models and dataclasses. (#317)
- This is a breaking change, because type annotations will now be enforced. Passing a `dict` when the declaration expects a `list` will fail. (#317)
- Pydantic models and dataclasses can no longer be passed to `@serializable` or `register_serializable()`. Annotate the workflow or step parameter or return value with their type instead. (#317)
Features
--------
- Support `call_later`, `call_at`, and `now` in the event loop implementation. (#343)
- This enables use of `asyncio.sleep()` as well as `asyncio.timeout` and the `timeout` parameter of `asyncio.wait_for`. (#343)
- `get_workflow_metadata()` returns the current run's `WorkflowInfo` (run id, workflow name, start time, deployment URL, and feature flags), callable from a workflow body or a step body — mirroring the JS SDK's `getWorkflowMetadata()`. (#320)
- One current limitation is that `started_at` is `None` from inside a step. (#320)
- `BaseHook.wait()` accepts `metadata` to record on the hook, and `get_hook_by_token()` reads it back for a resumer. (#301)
- A step can raise `RetryableError` to control when its next attempt runs. (#302)
- Accept `specVersion` 7 sealed noop event logs. (#319)
- Failed run and step events now preserve serialized error classes, messages, stacks, and causes. Failed runs also expose a plaintext `errorCode`. (#304)
- A workflow or step can attach plaintext metadata to its run with `set_attributes()`. (#303)
- Add a `share_sandboxes` parameter to `SandboxPolicy` to enable reusing already created sandboxes instead of creating a new one on each invocation. This speeds up workflows but means that modifications to global state may persist between invocations. (#310)
- Support `timedelta` arguments for workflow `sleep()` and retry delays. (#342)
- Expose unstable API to serve workflow HTTP endpoint from your own web framework. (#294)
- Added semi-internal manifest API for TS tools and e2e test. (#296)
Bug Fixes
---------
- Fix failing or even crashing cipher calls inside the workflow sandbox. (#305)
- Fail a workflow run with `HookConflictError` when another run already owns its hook token instead of leaving it running indefinitely. (#327)
- Support resuming hooks with payload in the queue message. (#300)
- Fix some bugs involving hooks arriving when the workflow was not yet blocked on them. (#339)
- Fixed nulls rejected by server, requiring Pydantic 2.12 or newer. (#321)
- Prevent workflows from having side effects while suspending. (#332)
- `hook.dispose()` will now work properly in a `finally` block. (That is, the hook will be disposed only when the workflow is actually terminating, and not every time it gets replayed.) (#332)
- More reliably fail runs whose replay diverges from the event log. (#347)
- Runs will now fail even in the case where the main thread of execution is not directly blocked on the suspension that is erroring. (#347)
- Fixed workflow and step calls with both positional-or-keyword parameters and `*args` failing during replay because their arguments were recorded in an unbindable shape. (#312)
Internal
--------
- Remove a just-added return from a finally block. (#344)
- Correct internal workflow type annotations found by checking untyped function bodies. (#337)
- Refactored event replay. (#341)
- Construct the protocol models by Python field name. (#322)
vercel
------
0.11.0 - 2026-08-31
-------------------
Features
--------
- Expose `get_deadline()` for reading the current Function invocation deadline. (#306)
- Answer workflow health checks for both queue-based transport and HTTP. (#292)
- Add support to read the sealed (`encp`) workflow payloads (X25519 + AES-GCM) an outside writer addresses to a run, under the `encryption` extra. (#297)
Bug Fixes
---------
- Remove upper bounds on aggregate Sandbox and Workflow dependencies so sibling releases cannot make the `vercel` package un-installable. (#334)
- Start a workflow run even when its queue message arrives before the `run_created` event has landed. (#284)
Internal
--------
- The Workflows implementation now ships in the separate `vercel-workflow` distribution, which `vercel` depends on, so `vercel.workflow` imports keep working without installing anything extra. (#299)
vercel-apscheduler
------------------
0.3.0 - 2026-08-31
------------------
Breaking Changes
----------------
- The managed Redis backend was removed. The integration now always runs on its managed job store (Vercel Runtime Cache); a configured default `RedisJobStore` is rejected at import, `VERCEL_APSCHEDULER_BACKEND` accepts only `cache`, and the `redis` dependency is gone. The scheduler's durable identity now always derives from the builder-assigned subscriber id (previously the Redis `jobs_key`); the `scheduler_id` option still pins an identity explicitly. (#286)
vercel-celery
-------------
0.7.5 - 2026-08-31
------------------
- Update dependencies.
vercel-django-tasks
-------------------
0.7.0 - 2026-08-31
------------------
Features
--------
- Add a Vercel Queues backend for Django Tasks and use it by default when no task backends are configured. (#291)
vercel-dramatiq
---------------
0.7.4 - 2026-08-31
------------------
- Update dependencies.
scotttrinh added a commit that referenced this pull request Aug 31, 2026
vercel-headers
--------------
0.7.2 - 2026-08-31
------------------
Bug Fixes
---------
- Accept request objects with concrete header implementations in the IP address and geolocation type annotations. (#337)
vercel-internal-core
--------------------
0.1.3 - 2026-08-31
------------------
Internal
--------
- Support disabling HTTP timeouts for selected SDK operations while preserving the client default elsewhere. (#307)
vercel-oidc
-----------
0.8.1 - 2026-08-31
------------------
- Update dependencies.
vercel-connect
--------------
0.1.1 - 2026-08-31
------------------
- Update dependencies.
vercel-internal-telemetry
-------------------------
0.7.3 - 2026-08-31
------------------
- Update dependencies.
vercel-queue
------------
0.8.1 - 2026-08-31
------------------
Bug Fixes
---------
- Force embedded development servers to exit when graceful shutdown stalls. (#351)
Documentation
-------------
- Remove documentation and examples for `asgi_app` in preparation for its removal. (#309)
vercel-sandbox
--------------
0.5.0 - 2026-08-31
------------------
Features
--------
- Add sync and async `fork_sandbox(...)` support for creating a sandbox from an existing named sandbox with optional configuration overrides. (#257)
- Add `region` and `failover_regions` configuration for sandbox creation, forks, and updates, plus multi-region snapshot availability reporting. (#308)
- Forward private ``__``-prefixed parameters to the Sandbox API. (#350)
Bug Fixes
---------
- Allow Sandbox process waits and log streams to remain idle longer than the session HTTP timeout. (#307)
- Expose Linux process signals consistently on every SDK host platform. (#352)
vercel-cache
------------
0.7.3 - 2026-08-31
------------------
- Update dependencies.
vercel-workflow
---------------
0.10.0 - 2026-08-31
-------------------
Breaking Changes
----------------
- Make `await hook` never return `None`
- Raises a new `HookDisposedError` instead of returning `None` when the hook has been disposed. It is now typed to return `T` instead of `T | None`. `async for` over a hook will stop iterating on disposal, still.
- Make sleep() and retry delays treat numbers as seconds, not ms (#346)
- This matches Python standard library APIs. (#346)
- Use type annotations on workflows and step to allow passing Pydantic models and dataclasses. (#317)
- This is a breaking change, because type annotations will now be enforced. Passing a `dict` when the declaration expects a `list` will fail. (#317)
- Pydantic models and dataclasses can no longer be passed to `@serializable` or `register_serializable()`. Annotate the workflow or step parameter or return value with their type instead. (#317)
Features
--------
- Support `call_later`, `call_at`, and `now` in the event loop implementation. (#343)
- This enables use of `asyncio.sleep()` as well as `asyncio.timeout` and the `timeout` parameter of `asyncio.wait_for`. (#343)
- `get_workflow_metadata()` returns the current run's `WorkflowInfo` (run id, workflow name, start time, deployment URL, and feature flags), callable from a workflow body or a step body — mirroring the JS SDK's `getWorkflowMetadata()`. (#320)
- One current limitation is that `started_at` is `None` from inside a step. (#320)
- Make `HookEvent` an async context manager
- This matches TS, which supports `using`. ``` # disposes the hook on block exit async with SomeHook.wait(...) as hook: res = await hook ```
- `BaseHook.wait()` accepts `metadata` to record on the hook, and `get_hook_by_token()` reads it back for a resumer. (#301)
- A step can raise `RetryableError` to control when its next attempt runs. (#302)
- Accept `specVersion` 7 sealed noop event logs. (#319)
- Failed run and step events now preserve serialized error classes, messages, stacks, and causes. Failed runs also expose a plaintext `errorCode`. (#304)
- A workflow or step can attach plaintext metadata to its run with `set_attributes()`. (#303)
- Add a `share_sandboxes` parameter to `SandboxPolicy` to enable reusing already created sandboxes instead of creating a new one on each invocation. This speeds up workflows but means that modifications to global state may persist between invocations. (#310)
- Support `timedelta` arguments for workflow `sleep()` and retry delays. (#342)
- Expose unstable API to serve workflow HTTP endpoint from your own web framework. (#294)
- Added semi-internal manifest API for TS tools and e2e test. (#296)
Bug Fixes
---------
- Fix failing or even crashing cipher calls inside the workflow sandbox. (#305)
- Fail a workflow run with `HookConflictError` when another run already owns its hook token instead of leaving it running indefinitely. (#327)
- Support resuming hooks with payload in the queue message. (#300)
- Fix some bugs involving hooks arriving when the workflow was not yet blocked on them. (#339)
- Fixed nulls rejected by server, requiring Pydantic 2.12 or newer. (#321)
- Prevent workflows from having side effects while suspending. (#332)
- `hook.dispose()` will now work properly in a `finally` block. (That is, the hook will be disposed only when the workflow is actually terminating, and not every time it gets replayed.) (#332)
- More reliably fail runs whose replay diverges from the event log. (#347)
- Runs will now fail even in the case where the main thread of execution is not directly blocked on the suspension that is erroring. (#347)
- Fixed workflow and step calls with both positional-or-keyword parameters and `*args` failing during replay because their arguments were recorded in an unbindable shape. (#312)
Internal
--------
- Remove a just-added return from a finally block. (#344)
- Correct internal workflow type annotations found by checking untyped function bodies. (#337)
- Refactored event replay. (#341)
- Construct the protocol models by Python field name. (#322)
vercel
------
0.11.0 - 2026-08-31
-------------------
Features
--------
- Expose `get_deadline()` for reading the current Function invocation deadline. (#306)
- Answer workflow health checks for both queue-based transport and HTTP. (#292)
- Add support to read the sealed (`encp`) workflow payloads (X25519 + AES-GCM) an outside writer addresses to a run, under the `encryption` extra. (#297)
Bug Fixes
---------
- Remove upper bounds on aggregate Sandbox and Workflow dependencies so sibling releases cannot make the `vercel` package un-installable. (#334)
- Start a workflow run even when its queue message arrives before the `run_created` event has landed. (#284)
Internal
--------
- The Workflows implementation now ships in the separate `vercel-workflow` distribution, which `vercel` depends on, so `vercel.workflow` imports keep working without installing anything extra. (#299)
vercel-apscheduler
------------------
0.3.0 - 2026-08-31
------------------
Breaking Changes
----------------
- The managed Redis backend was removed. The integration now always runs on its managed job store (Vercel Runtime Cache); a configured default `RedisJobStore` is rejected at import, `VERCEL_APSCHEDULER_BACKEND` accepts only `cache`, and the `redis` dependency is gone. The scheduler's durable identity now always derives from the builder-assigned subscriber id (previously the Redis `jobs_key`); the `scheduler_id` option still pins an identity explicitly. (#286)
vercel-celery
-------------
0.7.5 - 2026-08-31
------------------
- Update dependencies.
vercel-django-tasks
-------------------
0.7.0 - 2026-08-31
------------------
Features
--------
- Add a Vercel Queues backend for Django Tasks and use it by default when no task backends are configured. (#291)
vercel-dramatiq
---------------
0.7.4 - 2026-08-31
------------------
- Update dependencies.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@pranaygp@fantix
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); workflow: read spec 7 and skip sealed noops by pranaygp · Pull Request #319 · vercel/vercel-py · GitHub
Skip to content

workflow: read spec 7 and skip sealed noops - #319

Merged
fantix merged 4 commits into
mainfrom
pgp/port-noop-event-to-py
Aug 24, 2026
Merged

workflow: read spec 7 and skip sealed noops#319
fantix merged 4 commits into
mainfrom
pgp/port-noop-event-to-py

Conversation

@pranaygp

Copy link
Copy Markdown
Contributor

Python half of vercel/workflow#3634 (Add support for 'noop' event type — spec version 7).

Why this is urgent for us

That PR moves SPEC_VERSION_CURRENT and SPEC_VERSION_MAX_SUPPORTED to 7 and switches both @workflow/world-local and @workflow/world-vercel to mintedSpecVersion(), which returns 7 by default. Our reader ceiling was 6, and it is a pydantic le= on BaseEvent.specVersion — so a spec-7 row does not degrade, it fails validation, and with it the whole page. The e2e-python conformance lane runs the TypeScript driver against workbench/python over a shared world-local data dir, so it is the first thing that breaks: the driver's run_created is now labelled 7 and the Python app cannot parse the log it is supposed to replay.

What spec 7 actually asks of a client

On the server, a spec-7 run's slot positions come from a per-run counter handed out before the write that fills them, so concurrent writers never race for a position. The price is that a writer that takes a position and dies leaves a hole, and the backend's read path fills provably-abandoned holes with a server-written noop so returned pages stay dense prefixes.

So the client contract is one thing, and it is read-only: a reader must know a noop occupies its slot and means nothing.

There is no writer half for us. A World only owes the sealing half if it pre-assigns positions ahead of the commit; our local world allocates each id at the commit that fills it, so it has no holes to seal and will never emit one.

The change

world.py

  • SPEC_VERSION_SUPPORTS_SEALED_LOG = 7, and SPEC_VERSION_MAX_SUPPORTED follows it. SPEC_VERSION_CURRENT stays 2 — we still stamp what we always stamped, and a run created elsewhere keeps its creator's version.
  • NoopEvent / NoopEventData, in the read union and deliberately not in CreateEventRequest. eventData is optional and open (extra="allow", matching the .passthrough() on the TS schema): nothing reads it, the shape belongs to whichever backend sealed the slot, and a reader whose only interest is skipping the row has no business rejecting a field it has not heard of.
  • is_sealed_noop_event(), mirroring isSealedNoopEvent — one home for the test.

runtime.pyget_all_workflow_run_events() drops seals, so the log the replay walks is the log without them.

That is the whole skip, and the reason it is one line at the loader rather than a continue in resume() is the clock. Everything downstream of that function is reconstructing what the run did — matching correlation ids to suspensions, looking for a terminal event, deciding which waits elapsed, and reading createdAt for now(). Nothing on this side walks positions, so there is no caller that wants the seal. Meanwhile a seal's createdAt is the sealer's wall clock, which can postdate every real event around it; now() dates the run from the last event the replay consumed, so a seal left in the list hands the body a time no event of the run happened at — and then, because the clock only moves forward, every later now() too. Dropping it at the boundary makes the equivalence the contract is about (a sealed log replays exactly like the same log whose holes their own writers filled) hold by construction, rather than by three call sites remembering. The cursor is untouched: it is the World's, and still points past every row that was read.

Tests

tests/unit/test_workflow_sealed_log_noop.py, 12 tests in four parts:

  • the row — parses out of the read union with eventData, without it, and with an eventData field we have never heard of; the predicate answers for seals only; NoopEvent is in Event and absent from CreateEventRequest; 7 is the ceiling and 8 is still rejected.
  • storage — a seal round-trips through the local world and numbering continues past it, mirroring slot-identity.test.ts's stores, lists, and numbers past a noop event.
  • the loader — seals at head, middle (consecutive) and tail are dropped with the order of everything else intact; a page of nothing but seals still pages on; the cursor survives.
  • replay — the real claim, driven end to end through workflow_handler: one recorded two-sleep run, twice, differing only in seals at the head, in the middle (including one splitting a wait_created/wait_completed pair), and at the tail. Same output, same events written back, and the three now() readings pinned literally so a change that broke both logs the same way cannot slip past the comparison. Plus the degenerate all-seals-before-the-run shape, where the first now() has to find the first real event.

Every seal in the file is stamped a minute into the future, so each of those clock assertions would pass by accident if a seal's createdAt were plausible. Reverting the one-line filter fails 5 of the 12.

test_workflow_local_world_format.py's two spec-version tests move 6 → 7; the ceiling assertion now derives its match from SPEC_VERSION_MAX_SUPPORTED instead of hardcoding the number, so the next bump only has to touch the constant.

Full suite: 1127 tests, 0 failures (test_the_api_token_is_config_then_env_then_oidc fails locally on a box with VERCEL_TOKEN exported, before and after this change). Lint and typecheck clean repo-wide.

Follow-up, not in this PR

workbench/python/pyproject.toml in vercel/workflow resolves vercel-workflow from PyPI (0.9.0 today) regardless of its [tool.uv.sources] rev, so the e2e-python lane only picks this up once it is released and the lock is refreshed there — or once that file git-pins vercel-workflow at a commit containing it, which is what the comment block above that source entry describes.

🤖 Generated with Claude Code

`@workflow/world-local` and `@workflow/world-vercel` both stamp
specVersion 7 now (vercel/workflow#3634), so every event a TypeScript
driver writes arrives labelled 7 and our reader ceiling of 6 rejected
the whole log -- including on the local world the Python e2e lane runs
on. Raise the ceiling, and add the one thing the version gates: a
`noop` row, written by a World's backend to occupy a slot whose writer
allocated the position and then died.
A noop parses out of the read union but is dropped from the log the
replay walks. Its `createdAt` is the sealer's wall clock, which can
postdate every real event around it, so letting it reach the
deterministic clock would make a log whose hole was sealed replay
differently from the same log whose hole its own writer filled.
Nothing here writes one: we allocate each event id at the commit that
fills it, so we have no holes to seal, and `noop` stays out of the
create union.
Co-Authored-By: Pranay Prakash <1797812+pranaygp@users.noreply.github.com>

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@fantixfantix left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM; let me trim off some AI in-code chats and merge this to unbreak the e2e test.

@fantix
fantixforce-pushed the pgp/port-noop-event-to-py branch from 3116c4e to abb6ce9CompareAugust 24, 2026 18:44
@fantix
fantix merged commit a5969aa into mainAug 24, 2026
14 checks passed
@fantix
fantix deleted the pgp/port-noop-event-to-py branch August 24, 2026 18:49
@scotttrinhscotttrinh mentioned this pull request Aug 26, 2026
scotttrinh added a commit that referenced this pull request Aug 26, 2026
vercel-internal-core
--------------------
0.1.3 - 2026-08-26
------------------
Internal
--------
- Support disabling HTTP timeouts for selected SDK operations while preserving the client default elsewhere. (#307)
vercel-connect
--------------
0.1.1 - 2026-08-26
------------------
- Update dependencies.
vercel-queue
------------
0.8.1 - 2026-08-26
------------------
Documentation
-------------
- Remove documentation and examples for `asgi_app` in preparation for its removal. (#309)
vercel-sandbox
--------------
0.5.0 - 2026-08-26
------------------
Features
--------
- Add sync and async `fork_sandbox(...)` support for creating a sandbox from an existing named sandbox with optional configuration overrides. (#257)
- Add `region` and `failover_regions` configuration for sandbox creation, forks, and updates, plus multi-region snapshot availability reporting. (#308)
Bug Fixes
---------
- Allow Sandbox process waits and log streams to remain idle longer than the session HTTP timeout. (#307)
vercel-workflow
---------------
0.10.0 - 2026-08-26
-------------------
Breaking Changes
----------------
- Use type annotations on workflows and step to allow passing Pydantic models and dataclasses. (#317)
- This is a breaking change, because type annotations will now be enforced. Passing a `dict` when the declaration expects a `list` will fail. (#317)
- Pydantic models and dataclasses can no longer be passed to `@serializable` or `register_serializable()`. Annotate the workflow or step parameter or return value with their type instead. (#317)
Features
--------
- `get_workflow_metadata()` returns the current run's `WorkflowInfo` (run id, workflow name, start time, deployment URL, and feature flags), callable from a workflow body or a step body — mirroring the JS SDK's `getWorkflowMetadata()`. (#320)
- One current limitation is that `started_at` is `None` from inside a step. (#320)
- `BaseHook.wait()` accepts `metadata` to record on the hook, and `get_hook_by_token()` reads it back for a resumer. (#301)
- A step can raise `RetryableError` to control when its next attempt runs. (#302)
- Accept `specVersion` 7 sealed noop event logs. (#319)
- A workflow or step can attach plaintext metadata to its run with `set_attributes()`. (#303)
- Add a `share_sandboxes` parameter to `SandboxPolicy` to enable reusing already created sandboxes instead of creating a new one on each invocation. This speeds up workflows but means that modifications to global state may persist between invocations. (#310)
- Expose unstable API to serve workflow HTTP endpoint from your own web framework. (#294)
- Added semi-internal manifest API for TS tools and e2e test. (#296)
Bug Fixes
---------
- Fix failing or even crashing cipher calls inside the workflow sandbox. (#305)
- Support resuming hooks with payload in the queue message. (#300)
- Fixed nulls rejected by server, requiring Pydantic 2.12 or newer. (#321)
- Fixed workflow and step calls with both positional-or-keyword parameters and `*args` failing during replay because their arguments were recorded in an unbindable shape. (#312)
Internal
--------
- Construct the protocol models by Python field name. (#322)
vercel
------
0.11.0 - 2026-08-26
-------------------
Features
--------
- Expose `get_deadline()` for reading the current Function invocation deadline. (#306)
- Answer workflow health checks for both queue-based transport and HTTP. (#292)
- Add support to read the sealed (`encp`) workflow payloads (X25519 + AES-GCM) an outside writer addresses to a run, under the `encryption` extra. (#297)
Bug Fixes
---------
- Remove upper bounds on aggregate Sandbox and Workflow dependencies so sibling releases cannot make the `vercel` package un-installable. (#334)
- Start a workflow run even when its queue message arrives before the `run_created` event has landed. (#284)
Internal
--------
- The Workflows implementation now ships in the separate `vercel-workflow` distribution, which `vercel` depends on, so `vercel.workflow` imports keep working without installing anything extra. (#299)
vercel-apscheduler
------------------
0.3.0 - 2026-08-26
------------------
Breaking Changes
----------------
- The managed Redis backend was removed. The integration now always runs on its managed job store (Vercel Runtime Cache); a configured default `RedisJobStore` is rejected at import, `VERCEL_APSCHEDULER_BACKEND` accepts only `cache`, and the `redis` dependency is gone. The scheduler's durable identity now always derives from the builder-assigned subscriber id (previously the Redis `jobs_key`); the `scheduler_id` option still pins an identity explicitly. (#286)
vercel-celery
-------------
0.7.5 - 2026-08-26
------------------
- Update dependencies.
vercel-django-tasks
-------------------
0.7.0 - 2026-08-26
------------------
Features
--------
- Add a Vercel Queues backend for Django Tasks and use it by default when no task backends are configured. (#291)
vercel-dramatiq
---------------
0.7.4 - 2026-08-26
------------------
- Update dependencies.
scotttrinh added a commit that referenced this pull request Aug 31, 2026
vercel-headers
--------------
0.7.2 - 2026-08-31
------------------
Bug Fixes
---------
- Accept request objects with concrete header implementations in the IP address and geolocation type annotations. (#337)
vercel-internal-core
--------------------
0.1.3 - 2026-08-31
------------------
Internal
--------
- Support disabling HTTP timeouts for selected SDK operations while preserving the client default elsewhere. (#307)
vercel-oidc
-----------
0.8.1 - 2026-08-31
------------------
- Update dependencies.
vercel-connect
--------------
0.1.1 - 2026-08-31
------------------
- Update dependencies.
vercel-internal-telemetry
-------------------------
0.7.3 - 2026-08-31
------------------
- Update dependencies.
vercel-queue
------------
0.8.1 - 2026-08-31
------------------
Bug Fixes
---------
- Force embedded development servers to exit when graceful shutdown stalls. (#351)
Documentation
-------------
- Remove documentation and examples for `asgi_app` in preparation for its removal. (#309)
vercel-sandbox
--------------
0.5.0 - 2026-08-31
------------------
Features
--------
- Add sync and async `fork_sandbox(...)` support for creating a sandbox from an existing named sandbox with optional configuration overrides. (#257)
- Add `region` and `failover_regions` configuration for sandbox creation, forks, and updates, plus multi-region snapshot availability reporting. (#308)
- Forward private ``__``-prefixed parameters to the Sandbox API. (#350)
Bug Fixes
---------
- Allow Sandbox process waits and log streams to remain idle longer than the session HTTP timeout. (#307)
- Expose Linux process signals consistently on every SDK host platform. (#352)
vercel-cache
------------
0.7.3 - 2026-08-31
------------------
- Update dependencies.
vercel-workflow
---------------
0.10.0 - 2026-08-31
-------------------
Breaking Changes
----------------
- Make sleep() and retry delays treat numbers as seconds, not ms (#346)
- This matches Python standard library APIs. (#346)
- Use type annotations on workflows and step to allow passing Pydantic models and dataclasses. (#317)
- This is a breaking change, because type annotations will now be enforced. Passing a `dict` when the declaration expects a `list` will fail. (#317)
- Pydantic models and dataclasses can no longer be passed to `@serializable` or `register_serializable()`. Annotate the workflow or step parameter or return value with their type instead. (#317)
Features
--------
- Support `call_later`, `call_at`, and `now` in the event loop implementation. (#343)
- This enables use of `asyncio.sleep()` as well as `asyncio.timeout` and the `timeout` parameter of `asyncio.wait_for`. (#343)
- `get_workflow_metadata()` returns the current run's `WorkflowInfo` (run id, workflow name, start time, deployment URL, and feature flags), callable from a workflow body or a step body — mirroring the JS SDK's `getWorkflowMetadata()`. (#320)
- One current limitation is that `started_at` is `None` from inside a step. (#320)
- `BaseHook.wait()` accepts `metadata` to record on the hook, and `get_hook_by_token()` reads it back for a resumer. (#301)
- A step can raise `RetryableError` to control when its next attempt runs. (#302)
- Accept `specVersion` 7 sealed noop event logs. (#319)
- Failed run and step events now preserve serialized error classes, messages, stacks, and causes. Failed runs also expose a plaintext `errorCode`. (#304)
- A workflow or step can attach plaintext metadata to its run with `set_attributes()`. (#303)
- Add a `share_sandboxes` parameter to `SandboxPolicy` to enable reusing already created sandboxes instead of creating a new one on each invocation. This speeds up workflows but means that modifications to global state may persist between invocations. (#310)
- Support `timedelta` arguments for workflow `sleep()` and retry delays. (#342)
- Expose unstable API to serve workflow HTTP endpoint from your own web framework. (#294)
- Added semi-internal manifest API for TS tools and e2e test. (#296)
Bug Fixes
---------
- Fix failing or even crashing cipher calls inside the workflow sandbox. (#305)
- Fail a workflow run with `HookConflictError` when another run already owns its hook token instead of leaving it running indefinitely. (#327)
- Support resuming hooks with payload in the queue message. (#300)
- Fix some bugs involving hooks arriving when the workflow was not yet blocked on them. (#339)
- Fixed nulls rejected by server, requiring Pydantic 2.12 or newer. (#321)
- Prevent workflows from having side effects while suspending. (#332)
- `hook.dispose()` will now work properly in a `finally` block. (That is, the hook will be disposed only when the workflow is actually terminating, and not every time it gets replayed.) (#332)
- More reliably fail runs whose replay diverges from the event log. (#347)
- Runs will now fail even in the case where the main thread of execution is not directly blocked on the suspension that is erroring. (#347)
- Fixed workflow and step calls with both positional-or-keyword parameters and `*args` failing during replay because their arguments were recorded in an unbindable shape. (#312)
Internal
--------
- Remove a just-added return from a finally block. (#344)
- Correct internal workflow type annotations found by checking untyped function bodies. (#337)
- Refactored event replay. (#341)
- Construct the protocol models by Python field name. (#322)
vercel
------
0.11.0 - 2026-08-31
-------------------
Features
--------
- Expose `get_deadline()` for reading the current Function invocation deadline. (#306)
- Answer workflow health checks for both queue-based transport and HTTP. (#292)
- Add support to read the sealed (`encp`) workflow payloads (X25519 + AES-GCM) an outside writer addresses to a run, under the `encryption` extra. (#297)
Bug Fixes
---------
- Remove upper bounds on aggregate Sandbox and Workflow dependencies so sibling releases cannot make the `vercel` package un-installable. (#334)
- Start a workflow run even when its queue message arrives before the `run_created` event has landed. (#284)
Internal
--------
- The Workflows implementation now ships in the separate `vercel-workflow` distribution, which `vercel` depends on, so `vercel.workflow` imports keep working without installing anything extra. (#299)
vercel-apscheduler
------------------
0.3.0 - 2026-08-31
------------------
Breaking Changes
----------------
- The managed Redis backend was removed. The integration now always runs on its managed job store (Vercel Runtime Cache); a configured default `RedisJobStore` is rejected at import, `VERCEL_APSCHEDULER_BACKEND` accepts only `cache`, and the `redis` dependency is gone. The scheduler's durable identity now always derives from the builder-assigned subscriber id (previously the Redis `jobs_key`); the `scheduler_id` option still pins an identity explicitly. (#286)
vercel-celery
-------------
0.7.5 - 2026-08-31
------------------
- Update dependencies.
vercel-django-tasks
-------------------
0.7.0 - 2026-08-31
------------------
Features
--------
- Add a Vercel Queues backend for Django Tasks and use it by default when no task backends are configured. (#291)
vercel-dramatiq
---------------
0.7.4 - 2026-08-31
------------------
- Update dependencies.
scotttrinh added a commit that referenced this pull request Aug 31, 2026
vercel-headers
--------------
0.7.2 - 2026-08-31
------------------
Bug Fixes
---------
- Accept request objects with concrete header implementations in the IP address and geolocation type annotations. (#337)
vercel-internal-core
--------------------
0.1.3 - 2026-08-31
------------------
Internal
--------
- Support disabling HTTP timeouts for selected SDK operations while preserving the client default elsewhere. (#307)
vercel-oidc
-----------
0.8.1 - 2026-08-31
------------------
- Update dependencies.
vercel-connect
--------------
0.1.1 - 2026-08-31
------------------
- Update dependencies.
vercel-internal-telemetry
-------------------------
0.7.3 - 2026-08-31
------------------
- Update dependencies.
vercel-queue
------------
0.8.1 - 2026-08-31
------------------
Bug Fixes
---------
- Force embedded development servers to exit when graceful shutdown stalls. (#351)
Documentation
-------------
- Remove documentation and examples for `asgi_app` in preparation for its removal. (#309)
vercel-sandbox
--------------
0.5.0 - 2026-08-31
------------------
Features
--------
- Add sync and async `fork_sandbox(...)` support for creating a sandbox from an existing named sandbox with optional configuration overrides. (#257)
- Add `region` and `failover_regions` configuration for sandbox creation, forks, and updates, plus multi-region snapshot availability reporting. (#308)
- Forward private ``__``-prefixed parameters to the Sandbox API. (#350)
Bug Fixes
---------
- Allow Sandbox process waits and log streams to remain idle longer than the session HTTP timeout. (#307)
- Expose Linux process signals consistently on every SDK host platform. (#352)
vercel-cache
------------
0.7.3 - 2026-08-31
------------------
- Update dependencies.
vercel-workflow
---------------
0.10.0 - 2026-08-31
-------------------
Breaking Changes
----------------
- Make `await hook` never return `None`
- Raises a new `HookDisposedError` instead of returning `None` when the hook has been disposed. It is now typed to return `T` instead of `T | None`. `async for` over a hook will stop iterating on disposal, still.
- Make sleep() and retry delays treat numbers as seconds, not ms (#346)
- This matches Python standard library APIs. (#346)
- Use type annotations on workflows and step to allow passing Pydantic models and dataclasses. (#317)
- This is a breaking change, because type annotations will now be enforced. Passing a `dict` when the declaration expects a `list` will fail. (#317)
- Pydantic models and dataclasses can no longer be passed to `@serializable` or `register_serializable()`. Annotate the workflow or step parameter or return value with their type instead. (#317)
Features
--------
- Support `call_later`, `call_at`, and `now` in the event loop implementation. (#343)
- This enables use of `asyncio.sleep()` as well as `asyncio.timeout` and the `timeout` parameter of `asyncio.wait_for`. (#343)
- `get_workflow_metadata()` returns the current run's `WorkflowInfo` (run id, workflow name, start time, deployment URL, and feature flags), callable from a workflow body or a step body — mirroring the JS SDK's `getWorkflowMetadata()`. (#320)
- One current limitation is that `started_at` is `None` from inside a step. (#320)
- Make `HookEvent` an async context manager
- This matches TS, which supports `using`. ``` # disposes the hook on block exit async with SomeHook.wait(...) as hook: res = await hook ```
- `BaseHook.wait()` accepts `metadata` to record on the hook, and `get_hook_by_token()` reads it back for a resumer. (#301)
- A step can raise `RetryableError` to control when its next attempt runs. (#302)
- Accept `specVersion` 7 sealed noop event logs. (#319)
- Failed run and step events now preserve serialized error classes, messages, stacks, and causes. Failed runs also expose a plaintext `errorCode`. (#304)
- A workflow or step can attach plaintext metadata to its run with `set_attributes()`. (#303)
- Add a `share_sandboxes` parameter to `SandboxPolicy` to enable reusing already created sandboxes instead of creating a new one on each invocation. This speeds up workflows but means that modifications to global state may persist between invocations. (#310)
- Support `timedelta` arguments for workflow `sleep()` and retry delays. (#342)
- Expose unstable API to serve workflow HTTP endpoint from your own web framework. (#294)
- Added semi-internal manifest API for TS tools and e2e test. (#296)
Bug Fixes
---------
- Fix failing or even crashing cipher calls inside the workflow sandbox. (#305)
- Fail a workflow run with `HookConflictError` when another run already owns its hook token instead of leaving it running indefinitely. (#327)
- Support resuming hooks with payload in the queue message. (#300)
- Fix some bugs involving hooks arriving when the workflow was not yet blocked on them. (#339)
- Fixed nulls rejected by server, requiring Pydantic 2.12 or newer. (#321)
- Prevent workflows from having side effects while suspending. (#332)
- `hook.dispose()` will now work properly in a `finally` block. (That is, the hook will be disposed only when the workflow is actually terminating, and not every time it gets replayed.) (#332)
- More reliably fail runs whose replay diverges from the event log. (#347)
- Runs will now fail even in the case where the main thread of execution is not directly blocked on the suspension that is erroring. (#347)
- Fixed workflow and step calls with both positional-or-keyword parameters and `*args` failing during replay because their arguments were recorded in an unbindable shape. (#312)
Internal
--------
- Remove a just-added return from a finally block. (#344)
- Correct internal workflow type annotations found by checking untyped function bodies. (#337)
- Refactored event replay. (#341)
- Construct the protocol models by Python field name. (#322)
vercel
------
0.11.0 - 2026-08-31
-------------------
Features
--------
- Expose `get_deadline()` for reading the current Function invocation deadline. (#306)
- Answer workflow health checks for both queue-based transport and HTTP. (#292)
- Add support to read the sealed (`encp`) workflow payloads (X25519 + AES-GCM) an outside writer addresses to a run, under the `encryption` extra. (#297)
Bug Fixes
---------
- Remove upper bounds on aggregate Sandbox and Workflow dependencies so sibling releases cannot make the `vercel` package un-installable. (#334)
- Start a workflow run even when its queue message arrives before the `run_created` event has landed. (#284)
Internal
--------
- The Workflows implementation now ships in the separate `vercel-workflow` distribution, which `vercel` depends on, so `vercel.workflow` imports keep working without installing anything extra. (#299)
vercel-apscheduler
------------------
0.3.0 - 2026-08-31
------------------
Breaking Changes
----------------
- The managed Redis backend was removed. The integration now always runs on its managed job store (Vercel Runtime Cache); a configured default `RedisJobStore` is rejected at import, `VERCEL_APSCHEDULER_BACKEND` accepts only `cache`, and the `redis` dependency is gone. The scheduler's durable identity now always derives from the builder-assigned subscriber id (previously the Redis `jobs_key`); the `scheduler_id` option still pins an identity explicitly. (#286)
vercel-celery
-------------
0.7.5 - 2026-08-31
------------------
- Update dependencies.
vercel-django-tasks
-------------------
0.7.0 - 2026-08-31
------------------
Features
--------
- Add a Vercel Queues backend for Django Tasks and use it by default when no task backends are configured. (#291)
vercel-dramatiq
---------------
0.7.4 - 2026-08-31
------------------
- Update dependencies.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@pranaygp@fantix