Pranay Prakash d07c668495 [world-postgres] Honor $retention: 0 when a run finishes
A run started with `experimental_retention: 0` carries the reserved
`$retention: '0'` attribute, and a World that implements retention is
expected to delete that run's user payloads once it reaches a terminal
state. world-postgres was ignoring the attribute entirely, so the option
was silently a no-op here: the SDK documents the Postgres World as
implementing retention, and it did not.

The purge is one transaction issued after the terminal event row commits
and before the terminal NOTIFY.

- After the event row, because the terminal event is itself
  payload-bearing — `run_completed` carries the run's output — so
  anything earlier leaves that one payload behind.
- Before the NOTIFY, so a waiter woken by it re-reads an already-expired
  run rather than catching the output on its way out.

One transaction is also what makes the ordering rule the Vercel World has
to hand-sequence (data must become unreadable no later than it becomes
unrecoverable) a non-issue here: `expired_at` and the last cleared byte
commit together, so no reader can observe a half-purged run. It cannot,
however, be folded into the terminal run UPDATE itself, because that
UPDATE commits before the event insert — so a crash in between leaves a
terminal run holding data with no `expired_at`, exactly the window the
Vercel World has, and nothing retries it.

Both halves of every payload column are cleared. Each CBOR column has a
legacy JSONB twin beside it and the read paths fall back to the twin
(`value.output ||= value.outputJson`), so clearing only `*_cbor` would
leave the payload in place and resurrect it on the next read. Payload
columns are set through raw SQL `NULL` rather than a JS `null`: the CBOR
codec's `toDriver` would otherwise encode `null` into a one-byte CBOR
value and store that.

`expired_at` is stamped because it is what the CLI and web UI gate their
`<data expired>` rendering on. Without it the deletion is silent, and a
purged run reads back as one that never had any input.

Rows are kept — a purged run stays listable and traceable, matching the
Vercel World's contract — and so is plaintext metadata (status, name,
attributes, errorCode, executionContext, hook tokens). `executionContext`
in particular is excluded deliberately: the reference implementation
purges input/output/error and nothing else, and widening that here would
delete something the contract does not ask us to.

Stream chunk rows are blanked rather than deleted. The reader closes on
the `eof` row and `streams.list()` enumerates from these rows, so
deleting them would turn a finished stream into one that never
terminates and a run's stream list into an empty one. An empty `bytea`
carries no user data and keeps both behaviors.

Every value other than the literal `'0'` keeps the data, including a
well-formed non-zero duration. That is the load-bearing half: the unit
`$retention` is measured in is deliberately undecided, so an SDK that
starts sending a unit-bearing value to a World that predates the decision
must get the safe answer. The failure mode worth engineering against is
not a purge that does not fire, it is a purge that fires on a value
nobody meant as "delete my data".

Purge failures are caught and logged rather than thrown: a run must still
be able to finish. The cost of a failure is a run that keeps data it
asked to have deleted, which is loud in the log and safe on disk.

No migration: `expired_at` has existed since 0002_add_expired_at.sql and
nothing wrote it until now.
2026-08-28 12:27:09 -07:00
2025-10-23 12:07:52 +03:00
2025-10-23 12:07:52 +03:00
2025-10-23 12:07:52 +03:00
2026-02-05 16:19:16 -08:00
2025-10-23 12:07:52 +03:00
2026-08-14 16:56:05 -04:00
2025-10-23 12:07:52 +03:00

Workflow SDK logo

Workflow SDK

Vercel logo npm version License Join the community on GitHub

Workflow SDK makes TypeScript and JavaScript functions durable. It persists workflow progress, retries failed steps, and provides built-in observability. Workflows can suspend without using compute while they wait.

Quick start

Install the SDK in an existing project:

npm install workflow

Configure the integration for your framework. For example, with Next.js:

// next.config.ts
import { withWorkflow } from 'workflow/next';

export default withWorkflow({});

Then start a workflow from an API route, Server Action, or other server-side code:

import { start } from 'workflow/api';
import { onboardUser } from './workflows/onboard-user';

await start(onboardUser, ['hello@example.com']);

Run your app, then open the local observability UI in another terminal:

npm run dev
npx workflow web

Choose your framework in the getting-started guides.

Note

The workflow package includes its full documentation, so coding agents can read version-matched guides locally from node_modules/workflow/docs.

Run anywhere

Local development uses the bundled backend with no configuration. Deploy to Vercel for managed storage, queuing, scaling, and observability. To self-host, use the Postgres backend or implement a custom World.

The Worlds page lists maintainer-curated third-party Worlds, including self-hosted and managed options. Submit your World by updating the Worlds Manifest.

Community

The Workflow SDK community lives on GitHub Discussions, where you can ask questions, share ideas, and show what you have built.

Contributing

Contributions are welcome. Use issues and discussions to collaborate with the team and wider community. By participating, you agree to our Code of Conduct.

Security

If you find a security vulnerability in Workflow SDK, disclose it responsibly instead of opening a public issue.

To participate in our Open Source Software Bug Bounty program, please email responsible.disclosure@vercel.com. We will add you to the program and provide further instructions for submitting your report.

S
Description
workflow: Creates durable, resumable workflows using Vercel's Workflow SDK. Use when building workflows that need to survive restarts, pause for external events, retry…; workflow-init: Install and configure Vercel Workflow SDK before it exists in node_modules. Use when the user asks to "install workflow", "set up workflow", "add durable…
Readme 140 MiB
Languages
TypeScript 88.8%
JavaScript 5.9%
Rust 4%
CSS 0.5%
Python 0.4%
Other 0.3%