Skip to content

feat: workflow attributes (V1)#1933

Draft
pranaygp wants to merge 4 commits intomainfrom
pgp/attributes
Draft

feat: workflow attributes (V1)#1933
pranaygp wants to merge 4 commits intomainfrom
pgp/attributes

Conversation

@pranaygp
Copy link
Copy Markdown
Contributor

@pranaygp pranaygp commented May 5, 2026

🚧 Draft for review: this PR contains only the plan (in this body), skipped e2e tests, and docs scaffolding. No implementation yet. Iterating on the design before code lands.


Workflow Attributes — V1 Plan

Context

We want a first-class attributes feature for workflow runs: small, plaintext, indexed, mutable key/value metadata that user code (workflow body or step body) can set at runtime and that observability (web, CLI, list APIs) can search and filter on.

This is an MVP toward the Attributes RFC (#132), but deliberately scoped down: no idempotency (unique:true), no reserved $-namespace tags, no nested types — just Record<string, string>. The shape of the event log, the schema, and the world API are designed so the omitted features can be layered on later without breaking changes.

The user's goals, in priority order:

  1. Set attributes on a run at runtime, from either workflow code or step code.
  2. Read those attributes back inside the run.
  3. Persist them on the run, plaintext, not behind a remote ref — so they can be displayed and indexed without decryption.
  4. List/filter runs by attribute (for observability).
  5. Enumerate available attribute keys, and values for a given key — to power filter-builder UIs (key dropdown → value dropdown).

Scope (decided)

  • This PR/sprint = Phase 1 only: world interface + @workflow/core SDK + world-local + world-vercel client wiring + tests. workflow-server, Tinybird, Web, and CLI come in follow-up phases. The plan still calls out the contract those later phases need to honor.
  • Step writes fire immediately (uniform with workflow). The event log additionally captures writer context so each attr_set event records whether it came from the workflow body or a specific (stepId, attempt).
  • start() accepts initial attributes in V1. Carried on the run_created event and materialized onto the run at birth.

Design

Storage shape

Add an optional attributes: Record<string, string> field directly on the WorkflowRun entity. Stored unencrypted, alongside workflowName / status / errorCode (other plaintext metadata fields). NOT a ref. Bounded by validated limits so it fits cheaply on the run row.

V1 limits (validated in @workflow/core, defended again in the world):

  • key: 1–256 chars, must not start with $ (reserved for future system keys)
  • value: ≤ 256 bytes UTF-8
  • max ~64 attributes per run

The run row holds only the current snapshot. The full history (who wrote what, when) lives in the event log. UIs that want a "who set this" annotation fetch events on demand.

Mutation model — event-sourced, with writer context

A single new event type, designed up-front to carry one-or-many changes per event:

// new event in @workflow/world
attr_set:
  eventData: {
    changes: Array<{ key: string; value: string | null }>;  // null = unset
    writer:
      | { type: 'workflow' }
      | { type: 'step'; stepId: string; attempt: number };
  }

The changes array means setAttribute and setAttributes (the batch helper) share one event type:

  • setAttribute('phase', 'done') → emits attr_set { changes: [{key:'phase', value:'done'}], writer }.
  • setAttribute('phase', undefined) → emits attr_set { changes: [{key:'phase', value:null}], writer }.
  • setAttributes({phase:'done', orderId:'ord_123'}) → emits attr_set { changes: [{key:'phase',value:'done'}, {key:'orderId',value:'ord_123'}], writer }.

No separate attr_set_many event needed; the single-key case is just a one-element array. If the user passes an empty record the runtime short-circuits and emits nothing.

Both workflow code and step code emit the same attr_set event; only the writer discriminator differs. writer lets observability render "set by step processOrder (attempt 2)" vs. "set by workflow body" in the timeline. It is never read by application code via getAttribute() — that just returns the current snapshot string. Step retries that re-call setAttribute re-emit fresh attr_set events (no client-side coalescing); the event log faithfully records which attempt produced which write.

The naming attr_set follows the existing {entity}_{action} event-naming convention (run_created, step_started, hook_disposed, etc.), abbreviated to keep the prefix in line with the others.

The world atomically applies all changes in a single event to the run's attributes map. Last-write-wins ordered by event arrival (the existing event-write transaction wraps the entity update, so the order is well-defined; in the rare case where ULID order and arrival order diverge the per-key snapshot follows arrival order). This shape gives us:

  • Determinism for replay (the workflow VM rebuilds its attributes view from the event log, replaying each event's changes array in order).
  • An audit trail with writer attribution.
  • A clean extension point: future versions can add eventData.unique, eventData.expectedValue, etc. (per-change or per-event), and an attr_conflict event (parallel to hook_conflict), without breaking the shape.

Storage materialization in DDB-backed worlds

This applies to workflow-server (Phase 2), but the world API is shaped to be cheap to implement against it.

attributes is stored as a native DDB Map type on the run row, not a JSON-serialized string. Concretely, in workflow-server/lib/data/electrodb.ts:

attributes: {
  type: CustomAttributeType<Record<string, string>>('map'),
  // No JSON.stringify in setter — store natively so DDB can address attributes.#k
}

This is the prerequisite for the rest:

  • Single-event, multi-change apply: each attr_set event becomes one UpdateItem whose UpdateExpression is built from the event's changes array. Sets and unsets fold into one expression:

    UpdateExpression: "SET #attrs.#k1 = :v1, #attrs.#k2 = :v2 REMOVE #attrs.#k3"
    ExpressionAttributeNames: { "#attrs": "attributes",
                                "#k1": "phase", "#k2": "orderId", "#k3": "stale" }
    ExpressionAttributeValues: { ":v1": "done", ":v2": "ord_123" }
    

    Atomic on the run row. The existing event-write transaction (events table + run row) wraps it; no extra TransactWriteItems beyond what every other event type already pays. DDB's per-request limits (~256 expression names / values) are well above our V1 cap of 64 attributes per run, so even a full-replace setAttributes of every key fits in a single request.

  • No read-modify-write: the merge happens inside DDB's expression engine. No optimistic locking needed for the simple LWW-by-arrival semantic V1 ships with.

  • Server gets the full post-merge map for free: ReturnValues: 'ALL_NEW' on the UpdateItem returns the entire run row, including the freshly-merged attributes. The server uses that to emit Tinybird usage facts (Phase 3) without an extra read.

  • Migration: existing rows have no attributes field. New run_created events seed attributes: {} (or attributes: <init> if the start call provided one). On read, missing field → empty record.

A future tighter ordering guarantee (per-key ULID stamps + conditional update) can be added later without changing the event log shape.

Tinybird/ClickHouse path (Phase 3, contracted in this plan)

The world API doesn't dictate the wire format to Tinybird, but we record the intended shape so the server team has a target:

  • One usage fact per attr_set event (the usage fact may carry multiple changes if the event was a batch): { teamId, projectId, runId, eventId, ts, changes: Array<{key, value: string|null}>, writer_type, writer_step_id?, writer_attempt?, attributes_full: Record<string,string> }.
  • attributes_full is the post-merge snapshot from DDB ALL_NEW. Storing it per-event makes ClickHouse queries trivial: "latest row per runId, filter on attributes_full[key] = value" with no joins.
  • Trade-off: storing the full snapshot per event is denormalized. If CH storage cost matters, an alternative is per-key emission ({ runId, key, value, isDeleted, version=eventId } into a ReplacingMergeTree(version) keyed on (runId, key)); the trade is harder multi-key AND-filter queries. We'd default to the denormalized full-snapshot table.

This means the event log stays delta-only even though Tinybird stores full snapshots — the server is the only place that needs the merge logic, and DDB does it for it.

Client-side write semantics — always delta, server emits async

Final shape:

  • The client (workflow runtime + step runtime) always sends a delta-shaped attr_set event whose eventData.changes is one or more { key, value } entries. There is no two-mode protocol; workflow and step writers are uniform on the wire and only differ via the writer discriminator.
  • The server applies the delta to the DDB run row via a single UpdateItem (SET #attrs.#k = :v or REMOVE #attrs.#k) with ReturnValues: 'ALL_NEW', which returns the freshly-merged full attributes map in the same RPC.
  • The server acks the client request as soon as the DDB write succeeds. The Tinybird usage-fact emission carrying the post-merge attributes_full is fired off-thread via waitUntil / the existing workflowUsageTracker queue (Phase 3) — it does not block the response back to the runtime/SDK.

This means the runtime sees the same latency as any other lightweight event (e.g. step_started), and the analytics path is decoupled. If usage-fact emission fails or lags, the run state in DDB is still authoritative.

The client never has to compute or ship a full snapshot; whether the user calls setAttribute once or 100 times in a row, every event is bounded in size. High-volume use is still rate-bounded by per-event DDB cost, but the wire / replay shape stays simple.

Initial attributes at start()

start(workflow, input, { attributes }) accepts an attributes?: Record<string,string> option, validated client-side with the same rules as setAttribute. They are carried on run_created event:

run_created:
  eventData: {
    deploymentId, workflowName, input, executionContext,
    attributes?: Record<string, string>;   // NEW
  }

The world materializes them directly onto the new run — no attr_set events are emitted for them (they are the run's birth state, not a mutation). This keeps the event log minimal for the common case where attributes are only set at start.

Runtime SDK

Four new exports from @workflow/core (re-exported by workflow):

function setAttribute(key: string, value: string | undefined): Promise<void>
// undefined === unset (sends value: null in the event)

function setAttributes(attrs: Record<string, string | undefined>): Promise<void>
// batch helper; emits ONE attr_set event with all changes; merge semantics
// (only listed keys are touched; others remain). Empty record is a no-op.

function getAttribute(key: string): string | undefined
function getAttributes(): Record<string, string>

setAttribute(k, v) is implemented as setAttributes({[k]: v}) so they share the wire path; the convenience name is just sugar for the common case.

Available in both workflow context and step context. Implementation paths:

  • Step context (Node.js): setAttributes calls world.events.create(runId, { eventType: 'attr_set', eventData: { changes, writer: { type:'step', stepId, attempt } } }) directly. The step's contextStorage carries a local mutable attributes map seeded from the run snapshot at step start, so reads within the step body see local writes (read-your-writes).

  • Workflow context (sandboxed VM): bridged through the controller the same way sleep / createHook are. The workflow VM holds a local attributes map rebuilt from the event log on replay; setAttributes enqueues an event request (with writer: { type: 'workflow' }) through the host bridge, then optimistically updates the local map.

Validation (length, $ prefix, total count after merge) lives in a shared helper in @workflow/core so both contexts produce identical errors. Violations throw FatalError and are checked before emitting the event so an invalid batch never lands on the wire.

World interface additions

Extend @workflow/world Storage.runs:

runs: {
  // existing get/list...
  list(params: ListWorkflowRunsParams & {
    attributes?: Record<string, string>   // AND-combined exact match
  }): ...

  // new — for filter-builder UIs
  listAttributeKeys(params?: {
    prefix?: string;
    pagination?: PaginationOptions;
  }): Promise<PaginatedResponse<{ key: string; runCount?: number }>>;

  listAttributeValues(params: {
    key: string;
    prefix?: string;
    pagination?: PaginationOptions;
  }): Promise<PaginatedResponse<{ value: string; runCount?: number }>>;
}

Filter is folded into the existing runs.list rather than a separate listByAttribute, so it composes with workflowName / status / pagination.

World implementations (Phase 1)

  • @workflow/world-local: full implementation. The event-application pipeline gains an attr_set handler (iterate changes, set or delete on run.attributes); run_created honors initial attributes. runs.list filter is an in-memory Object.entries match; listAttributeKeys / listAttributeValues scan all run files and aggregate.
  • @workflow/world-vercel: client-side HTTP wrappers only. Sends attributes[key]=value query params on runs.list; calls new endpoints for keys/values. The actual server impl lands in Phase 2 (workflow-server).

The wire contract for Phase 2 / Phase 3 is documented in this PR's world-vercel README so the server team has a clear handoff:

  • GET /v3/runs?attributes[key]=value&... — extends existing list endpoint.
  • GET /v3/attributes/keys?prefix=...&limit=...&cursor=...
  • GET /v3/attributes/values?key=K&prefix=...&limit=...&cursor=...
  • POST /v3/runs/:id/events already accepts new event types — attr_set slots in.
  • POST /v3/runs run_created body extended with attributes.

What V1 does NOT include

  • unique: true / atomic conflict semantics.
  • Reserved $-prefixed system keys (we just block them in V1 to keep the namespace clean).
  • Non-string values (number / boolean / Date).
  • Setting attributes on entities other than runs (steps, hooks).
  • Server-side fuzzy substring search on values (V1 = exact match + prefix on key/value enumeration).
  • workflow-server DDB column / event handler / list filter.
  • Tinybird emission.
  • UI work in packages/web / packages/cli.

Critical files

@workflow/world

  • packages/world/src/runs.ts — add attributes?: Record<string,string> to WorkflowRunBaseSchema; add attributes filter to ListWorkflowRunsParams; add ListAttributeKeysParams, ListAttributeValuesParams, response types.
  • packages/world/src/events.ts — add attr_set to EventTypeSchema + new AttrSetEventSchema (with changes: Array<{key,value:string|null}> and the writer discriminator) in the event union. Extend RunCreatedEventSchema with optional attributes. Note: NOT added to EVENT_DATA_REF_FIELDS since the data is plaintext.
  • packages/world/src/interfaces.ts — extend Storage.runs with listAttributeKeys / listAttributeValues.

@workflow/core

  • New packages/core/src/attributes.tssetAttribute, setAttributes, getAttribute, getAttributes, plus validateAttributeKey/Value and the shared "build attr_set eventData from a record + writer" helper. Dispatches to a context-aware backend (step vs workflow).
  • packages/core/src/index.ts — export the four functions.
  • packages/core/src/step/context-storage.ts — extend the step context with a mutable attributes map (seeded from the run snapshot at step start) and an event-emitting setAttributes impl that tags writer = { type:'step', stepId, attempt }.
  • New packages/core/src/workflow/attributes.ts — VM-side bridge to the controller, mirroring workflow/sleep.ts. Tags writer = { type:'workflow' }.
  • packages/core/src/runtime.ts / runtime/start.ts / runtime/resume-hook.ts — handle attr_set events when replaying the event log to reconstruct the workflow VM's attributes view (apply the full changes array); thread start() { attributes } through to the run_created event payload.

@workflow/world-local

  • packages/world-local/src/storage.ts — handle attr_set event in the event-application pipeline (apply each change in changes array, set or remove on run.attributes); honor attributes from run_created; extend list filter to honor attributes.
  • New keys/values aggregation methods on the local storage.

@workflow/world-vercel

  • packages/world-vercel/src/runs.ts — extend listWorkflowRuns to send attributes[key]=value query params; add listAttributeKeys / listAttributeValues HTTP wrappers.
  • packages/world-vercel/src/index.ts — wire new methods into the World object.

Reused utilities (do not re-implement)

  • world.events.create is already the universal event-emitting path — attr_set slots in directly.
  • contextStorage (packages/core/src/step/context-storage.ts) is the right home for step-side per-call state, including the stepId / attempt needed for writer attribution.
  • WORKFLOW_CONTEXT_SYMBOL / VM-bridge pattern in packages/core/src/workflow/sleep.ts and workflow/create-hook.ts is the template for the workflow-VM side.
  • FatalError from @workflow/errors for validation failures.
  • EVENT_DATA_REF_FIELDS in events.ts — leave unchanged; attributes are plaintext.

Test plan

  • Unit tests in packages/core for:
    • validation (length, $ prefix, max count) producing FatalError. Total-count validation must consider the post-merge state, not just the incoming batch.
    • read-your-writes within a step body — both setAttribute and setAttributes.
    • setAttributes({}) is a no-op (no event emitted).
    • replay determinism (workflow VM sees same attributes pre/post replay across run_created.attributes + multiple attr_set events, including events with multi-change arrays).
    • writer attribution: events emitted from a step carry writer:{type:'step',...}; from workflow body carry writer:{type:'workflow'}.
    • retry: a step that fails and retries emits a fresh attr_set event on the second attempt with writer.attempt = 2.
  • world-local integration tests:
    • setAttribute → events.list → run.attributes round-trip.
    • setAttributes({a, b}) → events.list produces a single attr_set event with two changes entries.
    • start({ attributes }) materializes onto the run with no attr_set events.
    • runs.list({ attributes }) filter (AND semantics, multiple matches, no match).
    • listAttributeKeys / listAttributeValues enumeration with prefix.
  • world-vercel: contract tests against a mock backend covering URL/query shape for the new endpoints.
  • E2E in workbench/nextjs-turbopack: a workflow that
    1. Starts with start(input, { attributes: { tenant: 't1' } }).
    2. Workflow body calls setAttribute('phase', 'init'), awaits a step that calls setAttributes({ orderId: 'ord_123', stepKind: 'process' }), then setAttribute('phase', 'done').
    3. Asserts via world.runs.get(runId) that attributes === { tenant: 't1', phase: 'done', orderId: 'ord_123', stepKind: 'process' }.
    4. Asserts the step's writes appear as a single attr_set event with two changes entries (world.events.list).
    5. Asserts world.runs.list({ attributes: { orderId: 'ord_123' } }) returns the run.
    6. Asserts world.runs.listAttributeKeys() includes tenant, phase, orderId, stepKind.

Verification

  • pnpm typecheck && pnpm lint && pnpm test from the repo root.
  • E2E against workbench/nextjs-turbopack per CLAUDE.md instructions, hitting the test outline above.
  • pnpm changeset add with minor bumps for @workflow/core, @workflow/world, @workflow/world-local, @workflow/world-vercel, and workflow (fixed with core).

Completing the unfinished spec message

The user's draft fragment, completed:

MVP Goals:

  • Set from inside a run, single key:
    setAttribute(key: string, value: string | undefined)undefined means unset.
  • Set from inside a run, batch:
    setAttributes(attrs: Record<string, string | undefined>) — merges into the existing map; only listed keys change. undefined means unset for that key.
  • Get one or all from inside a run:
    getAttribute(key): string | undefined, getAttributes(): Record<string, string>.
  • All four are available in both workflow and step contexts.
  • Set from outside a run, at start time:
    start(workflow, input, { attributes: { tenantId: 't_1' } }). Carried on run_created, no separate event.
  • List runs by attribute (composed into the existing list, not a new method):
    world.runs.list({ attributes: { key: value, ... }, ...rest }) — AND-combined exact match.
    world-vercel + front will implement the heavy version via Tinybird (Phase 3); workflow-server lands a DDB-scan placeholder in Phase 2.
  • Enumerate keys for a filter-builder dropdown:
    world.runs.listAttributeKeys({ prefix?, pagination? }).
  • Enumerate values for a chosen key:
    world.runs.listAttributeValues({ key, prefix?, pagination? }).
    Both return { key|value, runCount? } so the UI can show "(N runs)" hints. Tinybird-backed in production; DDB-scan placeholder in Phase 2.

Mutation flows through a single new event type attr_set with
eventData: { changes: Array<{ key, value: string | null }>, writer: { type:'workflow' } | { type:'step', stepId, attempt } }. setAttribute is just a one-element-changes case of the same event; the batch helper never produces a different event shape. The world applies all changes atomically to run.attributes (LWW by event arrival). The writer field powers observability ("set by step X attempt 2" vs "set by workflow body") but is never surfaced to getAttribute() callers.

The run snapshot stores only the current state (attributes: Record<string,string>) on the run row, as a native DDB Map (not a JSON-serialized blob), so the per-event update is UpdateItem SET #attrs.#k = :v (or REMOVE) — atomic on the run row, no extra TransactWriteItems beyond the one already wrapping every event. Writer history lives in the event log. The run row is plaintext (not in a ref) so it can be returned cheaply by runs.get/list and indexed by Tinybird without decryption.

Tinybird emission is decoupled from the request path. After the DDB write acks, the server fires a usage fact carrying the post-merge attributes_full (returned for free via ReturnValues: 'ALL_NEW') on the existing workflowUsageTracker queue. The client never waits on it.

V1 is intentionally string-only, no unique:true, no $-reserved keys (just blocked at validation). The event schema, run schema, and world surface are designed so each of those drops in additively without a breaking change.

Opens a draft PR for plan + skipped e2e tests + docs review.
No implementation yet. Full plan in PR body.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 5, 2026

⚠️ No Changeset found

Latest commit: 07a4cff

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented May 5, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
example-nextjs-workflow-turbopack Error Error May 5, 2026 0:31am
example-nextjs-workflow-webpack Ready Ready Preview, Comment May 5, 2026 0:31am
example-workflow Ready Ready Preview, Comment May 5, 2026 0:31am
workbench-astro-workflow Ready Ready Preview, Comment May 5, 2026 0:31am
workbench-express-workflow Ready Ready Preview, Comment May 5, 2026 0:31am
workbench-fastify-workflow Ready Ready Preview, Comment May 5, 2026 0:31am
workbench-hono-workflow Ready Ready Preview, Comment May 5, 2026 0:31am
workbench-nitro-workflow Ready Ready Preview, Comment May 5, 2026 0:31am
workbench-nuxt-workflow Ready Ready Preview, Comment May 5, 2026 0:31am
workbench-sveltekit-workflow Ready Ready Preview, Comment May 5, 2026 0:31am
workbench-tanstack-start-workflow Ready Ready Preview, Comment May 5, 2026 0:31am
workbench-vite-workflow Ready Ready Preview, Comment May 5, 2026 0:31am
workflow-docs Ready Ready Preview, Comment, Open in v0 May 5, 2026 0:31am
workflow-swc-playground Ready Ready Preview, Comment May 5, 2026 0:31am
workflow-tarballs Ready Ready Preview, Comment May 5, 2026 0:31am
workflow-web Ready Ready Preview, Comment May 5, 2026 0:31am

@vercel vercel Bot temporarily deployed to Preview – workflow-docs May 5, 2026 03:49 Inactive
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 5, 2026

🧪 E2E Test Results

All tests passed

Summary

Passed Failed Skipped Total
✅ ▲ Vercel Production 1073 0 217 1290
✅ 💻 Local Development 1219 0 200 1419
✅ 📦 Local Production 1219 0 200 1419
✅ 🐘 Local Postgres 1219 0 200 1419
✅ 📋 Other 727 0 176 903
Total 5457 0 993 6450

Details by Category

✅ ▲ Vercel Production
App Passed Failed Skipped
✅ astro 103 0 26
✅ example 103 0 26
✅ express 103 0 26
✅ fastify 103 0 26
✅ hono 103 0 26
✅ nextjs-webpack 127 0 2
✅ nitro 103 0 26
✅ nuxt 103 0 26
✅ sveltekit 122 0 7
✅ vite 103 0 26
✅ 💻 Local Development
App Passed Failed Skipped
✅ astro-stable 104 0 25
✅ express-stable 104 0 25
✅ fastify-stable 104 0 25
✅ hono-stable 104 0 25
✅ nextjs-webpack-canary 110 0 19
✅ nextjs-webpack-stable-lazy-discovery-disabled 129 0 0
✅ nextjs-webpack-stable-lazy-discovery-enabled 129 0 0
✅ nitro-stable 104 0 25
✅ nuxt-stable 104 0 25
✅ sveltekit-stable 123 0 6
✅ vite-stable 104 0 25
✅ 📦 Local Production
App Passed Failed Skipped
✅ astro-stable 104 0 25
✅ express-stable 104 0 25
✅ fastify-stable 104 0 25
✅ hono-stable 104 0 25
✅ nextjs-webpack-canary 110 0 19
✅ nextjs-webpack-stable-lazy-discovery-disabled 129 0 0
✅ nextjs-webpack-stable-lazy-discovery-enabled 129 0 0
✅ nitro-stable 104 0 25
✅ nuxt-stable 104 0 25
✅ sveltekit-stable 123 0 6
✅ vite-stable 104 0 25
✅ 🐘 Local Postgres
App Passed Failed Skipped
✅ astro-stable 104 0 25
✅ express-stable 104 0 25
✅ fastify-stable 104 0 25
✅ hono-stable 104 0 25
✅ nextjs-webpack-canary 110 0 19
✅ nextjs-webpack-stable-lazy-discovery-disabled 129 0 0
✅ nextjs-webpack-stable-lazy-discovery-enabled 129 0 0
✅ nitro-stable 104 0 25
✅ nuxt-stable 104 0 25
✅ sveltekit-stable 123 0 6
✅ vite-stable 104 0 25
✅ 📋 Other
App Passed Failed Skipped
✅ e2e-local-dev-nest-stable 104 0 25
✅ e2e-local-dev-tanstack-start- 104 0 25
✅ e2e-local-postgres-nest-stable 104 0 25
✅ e2e-local-postgres-tanstack-start- 104 0 25
✅ e2e-local-prod-nest-stable 104 0 25
✅ e2e-local-prod-tanstack-start- 104 0 25
✅ e2e-vercel-prod-tanstack-start 103 0 26

📋 View full workflow run


Some E2E test jobs failed:

  • Vercel Prod: failure
  • Local Dev: cancelled
  • Local Prod: failure
  • Local Postgres: failure
  • Windows: failure

Check the workflow run for details.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 5, 2026

📊 Benchmark Results

📈 Comparing against baseline from main branch. Green 🟢 = faster, Red 🔺 = slower.

workflow with no steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 0.032s (-26.2% 🟢) 1.005s (~) 0.973s 10 1.00x
🐘 Postgres Express 0.049s (-16.0% 🟢) 1.014s (~) 0.965s 10 1.53x
🐘 Postgres Nitro 0.050s (-47.1% 🟢) 1.010s (-3.2%) 0.960s 10 1.58x
💻 Local Express ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 0.281s (+19.6% 🔺) 2.373s (+11.1% 🔺) 2.091s 10 1.00x
▲ Vercel Nitro 0.444s (+8.4% 🔺) 2.524s (+0.6%) 2.080s 10 1.58x

🔍 Observability: Express | Nitro

workflow with 1 step

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 1.067s (-5.7% 🟢) 2.006s (~) 0.939s 10 1.00x
🐘 Postgres Nitro 1.082s (-5.1% 🟢) 2.010s (~) 0.928s 10 1.01x
🐘 Postgres Express 1.093s (-4.6%) 2.010s (~) 0.917s 10 1.02x
💻 Local Express ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 1.616s (-58.5% 🟢) 4.317s (-26.9% 🟢) 2.702s 10 1.00x
▲ Vercel Express 1.731s (-7.7% 🟢) 4.172s (+9.6% 🔺) 2.440s 10 1.07x

🔍 Observability: Nitro | Express

workflow with 10 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 10.413s (-4.2%) 11.018s (~) 0.605s 3 1.00x
💻 Local Nitro 10.416s (-4.8%) 11.022s (~) 0.606s 3 1.00x
🐘 Postgres Express 10.447s (-4.7%) 11.018s (~) 0.571s 3 1.00x
💻 Local Express ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 13.708s (-42.2% 🟢) 18.372s (-26.9% 🟢) 4.664s 2 1.00x
▲ Vercel Express 14.100s (-17.0% 🟢) 16.193s (-19.1% 🟢) 2.093s 2 1.03x

🔍 Observability: Nitro | Express

workflow with 25 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 13.455s (-7.8% 🟢) 14.014s (-6.7% 🟢) 0.559s 5 1.00x
💻 Local Nitro 13.497s (-10.4% 🟢) 14.027s (-12.5% 🟢) 0.530s 5 1.00x
🐘 Postgres Express 13.533s (-7.2% 🟢) 14.019s (-6.7% 🟢) 0.486s 5 1.01x
💻 Local Express ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 22.079s (-56.1% 🟢) 24.108s (-54.1% 🟢) 2.028s 3 1.00x
▲ Vercel Nitro 23.386s (-63.7% 🟢) 25.648s (-61.5% 🟢) 2.262s 3 1.06x

🔍 Observability: Express | Nitro

workflow with 50 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 11.917s (-29.0% 🟢) 12.273s (-27.9% 🟢) 0.356s 8 1.00x
🐘 Postgres Nitro 12.020s (-13.9% 🟢) 12.644s (-11.6% 🟢) 0.624s 8 1.01x
🐘 Postgres Express 12.044s (-14.0% 🟢) 12.644s (-13.4% 🟢) 0.601s 8 1.01x
💻 Local Express ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 31.868s (-92.5% 🟢) 34.417s (-91.9% 🟢) 2.548s 3 1.00x
▲ Vercel Express 31.891s (-73.7% 🟢) 34.028s (-72.5% 🟢) 2.137s 3 1.00x

🔍 Observability: Nitro | Express

Promise.all with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.151s (-9.7% 🟢) 2.008s (~) 0.856s 15 1.00x
🐘 Postgres Express 1.162s (-7.8% 🟢) 2.008s (~) 0.846s 15 1.01x
💻 Local Nitro 1.188s (-27.2% 🟢) 2.006s (-3.3%) 0.818s 15 1.03x
💻 Local Express ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 2.633s (-6.6% 🟢) 4.296s (-0.6%) 1.663s 7 1.00x
▲ Vercel Express 3.144s (+9.9% 🔺) 4.817s (+4.2%) 1.673s 7 1.19x

🔍 Observability: Nitro | Express

Promise.all with 25 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.239s (-47.3% 🟢) 2.007s (-33.3% 🟢) 0.768s 15 1.00x
🐘 Postgres Express 1.242s (-47.4% 🟢) 2.007s (-33.3% 🟢) 0.765s 15 1.00x
💻 Local Nitro 1.708s (-45.7% 🟢) 2.006s (-48.4% 🟢) 0.298s 15 1.38x
💻 Local Express ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 3.566s (-1.5%) 5.290s (+3.5%) 1.725s 6 1.00x
▲ Vercel Nitro 4.447s (+9.7% 🔺) 6.226s (+5.1% 🔺) 1.779s 5 1.25x

🔍 Observability: Express | Nitro

Promise.all with 50 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.356s (-61.0% 🟢) 2.008s (-49.9% 🟢) 0.652s 15 1.00x
🐘 Postgres Express 1.384s (-60.3% 🟢) 2.008s (-49.9% 🟢) 0.624s 15 1.02x
💻 Local Nitro 4.892s (-41.4% 🟢) 5.514s (-38.9% 🟢) 0.622s 6 3.61x
💻 Local Express ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 5.427s (+28.0% 🔺) 7.613s (+24.3% 🔺) 2.186s 4 1.00x
▲ Vercel Nitro 5.536s (+57.0% 🔺) 7.241s (+30.8% 🔺) 1.705s 5 1.02x

🔍 Observability: Express | Nitro

Promise.race with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.156s (-8.1% 🟢) 2.008s (~) 0.853s 15 1.00x
🐘 Postgres Nitro 1.171s (-6.8% 🟢) 2.008s (~) 0.837s 15 1.01x
💻 Local Nitro 1.385s (-25.8% 🟢) 2.006s (-14.3% 🟢) 0.622s 15 1.20x
💻 Local Express ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 2.421s (-6.2% 🟢) 4.128s (-5.1% 🟢) 1.707s 8 1.00x
▲ Vercel Nitro 2.581s (+5.0%) 4.252s (+2.0%) 1.671s 8 1.07x

🔍 Observability: Express | Nitro

Promise.race with 25 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.232s (-47.3% 🟢) 2.009s (-33.3% 🟢) 0.777s 15 1.00x
🐘 Postgres Express 1.247s (-46.7% 🟢) 2.009s (-33.3% 🟢) 0.762s 15 1.01x
💻 Local Nitro 1.890s (-38.3% 🟢) 2.316s (-40.4% 🟢) 0.425s 13 1.53x
💻 Local Express ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 3.292s (+3.1%) 5.210s (+8.7% 🔺) 1.918s 6 1.00x
▲ Vercel Nitro 3.486s (+7.8% 🔺) 5.410s (+6.6% 🔺) 1.924s 6 1.06x

🔍 Observability: Express | Nitro

Promise.race with 50 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.394s (-60.2% 🟢) 2.008s (-49.9% 🟢) 0.614s 15 1.00x
🐘 Postgres Nitro 1.396s (-59.9% 🟢) 2.007s (-49.9% 🟢) 0.611s 15 1.00x
💻 Local Nitro 5.261s (-42.5% 🟢) 5.681s (-43.3% 🟢) 0.420s 6 3.77x
💻 Local Express ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 5.512s (-14.1% 🟢) 7.664s (-6.3% 🟢) 2.152s 4 1.00x
▲ Vercel Nitro 5.761s (+13.1% 🔺) 7.541s (+10.6% 🔺) 1.780s 5 1.05x

🔍 Observability: Express | Nitro

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 0.456s (-44.4% 🟢) 1.007s (~) 0.550s 60 1.00x
💻 Local Nitro 0.473s (-51.8% 🟢) 1.004s (-8.2% 🟢) 0.531s 60 1.04x
🐘 Postgres Express 0.476s (-43.3% 🟢) 1.007s (-1.6%) 0.531s 60 1.04x
💻 Local Express ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 5.089s (-76.9% 🟢) 7.183s (-70.1% 🟢) 2.094s 9 1.00x
▲ Vercel Express 5.437s (-71.4% 🟢) 7.360s (-65.5% 🟢) 1.923s 9 1.07x

🔍 Observability: Nitro | Express

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.063s (-44.8% 🟢) 1.738s (-17.3% 🟢) 0.675s 52 1.00x
🐘 Postgres Express 1.124s (-43.1% 🟢) 1.986s (-12.0% 🟢) 0.862s 46 1.06x
💻 Local Nitro 1.317s (-56.6% 🟢) 2.122s (-43.5% 🟢) 0.805s 43 1.24x
💻 Local Express ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 13.398s (-66.1% 🟢) 15.794s (-61.8% 🟢) 2.396s 6 1.00x
▲ Vercel Express 14.078s (-59.2% 🟢) 16.281s (-55.8% 🟢) 2.203s 6 1.05x

🔍 Observability: Nitro | Express

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 2.048s (-48.7% 🟢) 2.605s (-40.4% 🟢) 0.556s 47 1.00x
🐘 Postgres Nitro 2.180s (-46.9% 🟢) 2.866s (-37.7% 🟢) 0.686s 42 1.06x
💻 Local Nitro 2.704s (-70.9% 🟢) 3.007s (-70.0% 🟢) 0.303s 40 1.32x
💻 Local Express ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 38.039s (-60.7% 🟢) 41.076s (-58.3% 🟢) 3.037s 3 1.00x
▲ Vercel Express 40.167s (-69.1% 🟢) 43.063s (-67.4% 🟢) 2.897s 3 1.06x

🔍 Observability: Nitro | Express

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 0.185s (-34.6% 🟢) 1.005s (~) 0.820s 60 1.00x
🐘 Postgres Express 0.190s (-32.9% 🟢) 1.006s (~) 0.816s 60 1.02x
💻 Local Nitro 0.448s (-25.9% 🟢) 1.004s (-1.7%) 0.556s 60 2.42x
💻 Local Express ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 2.198s (+12.5% 🔺) 3.989s (+9.7% 🔺) 1.791s 16 1.00x
▲ Vercel Nitro 2.290s (+37.9% 🔺) 4.173s (+24.5% 🔺) 1.882s 15 1.04x

🔍 Observability: Express | Nitro

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 0.320s (-35.5% 🟢) 1.007s (~) 0.686s 90 1.00x
🐘 Postgres Express 0.330s (-35.4% 🟢) 1.006s (~) 0.677s 90 1.03x
💻 Local Nitro 2.313s (-8.9% 🟢) 2.977s (-1.1%) 0.664s 31 7.22x
💻 Local Express ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 9.507s (+212.0% 🔺) 11.715s (+143.7% 🔺) 2.209s 8 1.00x
▲ Vercel Nitro 10.331s (+220.3% 🔺) 12.386s (+156.9% 🔺) 2.055s 8 1.09x

🔍 Observability: Express | Nitro

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 0.648s (-18.0% 🟢) 1.006s (~) 0.358s 120 1.00x
🐘 Postgres Express 0.660s (-19.4% 🟢) 1.006s (-1.1%) 0.347s 120 1.02x
💻 Local Nitro 10.264s (-8.3% 🟢) 10.778s (-7.6% 🟢) 0.515s 12 15.83x
💻 Local Express ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 18.660s (+141.6% 🔺) 20.764s (+120.9% 🔺) 2.103s 6 1.00x
▲ Vercel Express 27.135s (+265.7% 🔺) 29.583s (+220.0% 🔺) 2.448s 5 1.45x

🔍 Observability: Nitro | Express

Stream Benchmarks (includes TTFB metrics)
workflow with stream

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 1.138s (+432.6% 🔺) 2.005s (+99.6% 🔺) 0.010s (-18.4% 🟢) 2.017s (+98.0% 🔺) 0.879s 10 1.00x
🐘 Postgres Nitro 1.143s (+457.7% 🔺) 1.996s (+99.6% 🔺) 0.002s (+6.7% 🔺) 2.011s (+98.8% 🔺) 0.868s 10 1.00x
🐘 Postgres Express 1.149s (+460.2% 🔺) 2.000s (+100.2% 🔺) 0.002s (~) 2.011s (+98.9% 🔺) 0.862s 10 1.01x
💻 Local Express ⚠️ missing - - - - -

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 2.321s (-39.4% 🟢) 3.496s (-33.7% 🟢) 3.471s (+367.7% 🔺) 7.495s (+15.6% 🔺) 5.174s 10 1.00x
▲ Vercel Express 2.322s (-7.3% 🟢) 3.411s (-16.6% 🟢) 2.358s (+145.4% 🔺) 6.310s (+12.9% 🔺) 3.988s 10 1.00x

🔍 Observability: Nitro | Express

stream pipeline with 5 transform steps (1MB)

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.521s (+143.7% 🔺) 2.006s (+99.2% 🔺) 0.004s (-4.9%) 2.026s (+98.1% 🔺) 0.505s 30 1.00x
💻 Local Nitro 1.523s (+81.6% 🔺) 2.010s (+98.6% 🔺) 0.009s (~) 2.022s (+81.2% 🔺) 0.498s 30 1.00x
🐘 Postgres Express 1.535s (+143.6% 🔺) 2.000s (+98.8% 🔺) 0.004s (-6.9% 🟢) 2.023s (+97.7% 🔺) 0.488s 30 1.01x
💻 Local Express ⚠️ missing - - - - -

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 5.852s (-10.0% 🟢) 7.646s (-4.6%) 0.308s (-24.7% 🟢) 8.525s (-3.5%) 2.674s 8 1.00x
▲ Vercel Nitro 5.976s (-79.7% 🟢) 7.838s (-74.6% 🟢) 0.273s (+144.1% 🔺) 8.657s (-72.8% 🟢) 2.681s 7 1.02x

🔍 Observability: Express | Nitro

10 parallel streams (1MB each)

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 0.668s (-31.0% 🟢) 1.030s (-17.5% 🟢) 0.000s (-15.8% 🟢) 1.059s (-15.8% 🟢) 0.390s 57 1.00x
🐘 Postgres Express 0.703s (-26.8% 🟢) 1.049s (-17.9% 🟢) 0.000s (-19.3% 🟢) 1.060s (-18.8% 🟢) 0.357s 57 1.05x
💻 Local Nitro 1.398s (+14.3% 🔺) 2.015s (~) 0.000s (+100.0% 🔺) 2.017s (~) 0.619s 30 2.09x
💻 Local Express ⚠️ missing - - - - -

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 3.711s (-0.8%) 5.290s (+3.7%) 0.001s (+400.0% 🔺) 5.810s (+5.1% 🔺) 2.099s 11 1.00x
▲ Vercel Nitro 3.796s (+24.4% 🔺) 5.356s (+21.9% 🔺) 0.001s (+1436.4% 🔺) 5.857s (+21.8% 🔺) 2.061s 11 1.02x

🔍 Observability: Express | Nitro

fan-out fan-in 10 streams (1MB each)

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.339s (-25.3% 🟢) 2.065s (-3.6%) 0.000s (-100.0% 🟢) 2.079s (-4.4%) 0.741s 29 1.00x
🐘 Postgres Express 1.478s (-16.6% 🟢) 2.143s (-1.6%) 0.000s (NaN%) 2.170s (-1.3%) 0.692s 28 1.10x
💻 Local Nitro 3.127s (-7.7% 🟢) 3.907s (-3.1%) 0.001s (+17.2% 🔺) 3.909s (-3.1%) 0.782s 16 2.34x
💻 Local Express ⚠️ missing - - - - -

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 5.527s (+35.0% 🔺) 7.304s (+35.9% 🔺) 0.000s (-100.0% 🟢) 7.806s (+34.7% 🔺) 2.279s 8 1.00x
▲ Vercel Express 6.012s (+31.1% 🔺) 7.661s (+27.2% 🔺) 0.001s (+Infinity% 🔺) 8.169s (+26.5% 🔺) 2.157s 8 1.09x

🔍 Observability: Nitro | Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World 🥇 Fastest Framework Wins
💻 Local Nitro 21/21
🐘 Postgres Nitro 17/21
▲ Vercel Express 11/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework 🥇 Fastest World Wins
Express 🐘 Postgres 21/21
Nitro 🐘 Postgres 17/21
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

  • 💻 Local: In-memory filesystem world (local development)
  • 🐘 Postgres: PostgreSQL database world (local development)
  • ▲ Vercel: Vercel production/preview deployment
  • 🌐 Turso: Community world (local development)
  • 🌐 MongoDB: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Jazz: Community world (local development)

📋 View full workflow run


Some benchmark jobs failed:

  • Local: failure
  • Postgres: failure
  • Vercel: failure

Check the workflow run for details.

⚠️ Community world benchmarks failed (non-blocking):

  • Community Worlds: failure

Check the workflow run for details.

Adds the docs and skipped e2e tests for review *before* implementation.
No runtime changes — all four function shims (setAttribute, setAttributes,
getAttribute, getAttributes), the runs.list filter, and the keys/values
enumeration endpoints are referenced as if they exist, with the necessary
ambient declarations and `// @setup` blocks to keep the docs typecheck and
the workspace typecheck green.

Docs:
  - docs/content/docs/foundations/attributes.mdx — concept guide.
  - docs/content/docs/api-reference/workflow/{set-attribute,set-attributes,
    get-attribute,get-attributes}.mdx — API reference for each export.
  - Wires the new pages into the foundations + api-reference index Cards
    and meta.json.
  - packages/docs-typecheck/src/docs-globals.d.ts — temporary ambient
    declarations marked `TODO(attributes V1)` for the unimplemented APIs
    so the docs samples typecheck.

Tests:
  - workbench/example/workflows/9_attributes.ts — workflow + step that
    drive every code path the e2e suite asserts on.
  - workbench/{nextjs-turbopack,nextjs-webpack,nitro-v3}/workflows/
    9_attributes.ts — the standard symlink fan-out.
  - packages/core/e2e/attributes.test.ts — full describe.skip suite
    covering: round-trip, batch single-event, start({attributes}) initial
    state, writer attribution (workflow vs step + attempt), retry
    attribution, replay determinism, runs.list AND-filter, and
    listAttributeKeys / listAttributeValues with prefix.

Each `as any` / `as unknown as ...` and ambient shim carries a
`TODO(attributes)` marker so the cleanup is greppable when the
implementation lands.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
pranaygp and others added 2 commits May 5, 2026 21:09
* origin/main:
  feat: serializable AbortController/AbortSignal (#1301)
  Auto-remove workflow packages from serverExternalPackages (#1481)
  Add missing changeset for Zod 4.4.x compatibility fix in @workflow/world (#1939)
  Push backport branch via GraphQL createCommitOnBranch for signed commits (#1937)
  Fix backport workflow opencode permission and surface AI failures (#1936)
  Restructure backport workflow with AI-driven decisions (#1934)
Mirrors the cancellation / AbortSignal pattern landed on main in #1301:
attribute docs are v5-only since the underlying API will only ship in
the v5 (workflow 5.x) branch.

  - Add `preRelease: true` to the frontmatter of all 5 attribute docs
    (foundations/attributes + 4 api-reference/workflow/*-attribute*).
    The v5 docs setup (docs/lib/geistdocs/version-source.ts) filters
    these out of the v4 sidebar tree and rewrites their URLs to /v5.
  - Drop the corresponding Cards from foundations/index.mdx and
    api-reference/workflow/index.mdx so v4 readers don't get broken
    links. v5 readers discover the pages via the sidebar.
  - Drop the inline "V1 preview" Callouts now that the v5 layout
    renders its own pre-release banner for preRelease pages.

foundations/meta.json already lists `attributes` (alongside the
freshly-merged `cancellation`); keep it there for the v5 sidebar.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to 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.

1 participant