Skip to content

[core] Allow settings attributes from inside step functions - #2157

Merged
VaguelySerious merged 1 commit into
mainfrom
peter/step-attributes
May 29, 2026
Merged

[core] Allow settings attributes from inside step functions#2157
VaguelySerious merged 1 commit into
mainfrom
peter/step-attributes

Conversation

@VaguelySerious

@VaguelySeriousVaguelySerious commented May 29, 2026

Copy link
Copy Markdown
Member

Expands on #2134 to allow running experimental_setAttributes() from inside a "use step" function.

Shares SDK-side attribute normalization/validation between workflow and step entrypoints. Add unit and e2e coverage for step-side writes, and updates docs.

This is forward-compatible with event-based attributes if we accept attributes set from a step-level potentially racing with other calls, and accept not supporting deterministic getAttribute calls from within a workflow context. This will need to be documented when we move to the new implementation.

@vercel

vercelBot commented May 29, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 79bbfce

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 16 packages
NameType
@workflow/corePatch
workflowPatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
@workflow/world-testingPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

Not sure what this means? Click here to learn what changesets are.

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

@github-actions

github-actionsBot commented May 29, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express0.041s (-7.0% 🟢)1.007s (~)0.966s101.00x
💻 LocalNitro0.042s (-2.6%)1.006s (~)0.964s101.02x
🐘 PostgresExpress0.055s (-4.8%)1.012s (~)0.956s101.34x
🐘 PostgresNitro0.059s (-38.4% 🟢)1.012s (-3.0%)0.953s101.42x
🐘 PostgresNext.js (Turbopack)0.061s1.013s0.952s101.47x
💻 LocalNext.js (Turbopack)0.063s1.006s0.943s101.53x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.347s (+47.6% 🔺)2.211s (+3.5%)1.863s101.00x
▲ VercelNext.js (Turbopack)0.544s (+116.3% 🔺)2.346s (+0.6%)1.802s101.57x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.086s (-3.5%)2.006s (~)0.920s101.00x
💻 LocalNitro1.097s (-3.0%)2.006s (~)0.909s101.01x
🐘 PostgresExpress1.107s (-3.4%)2.009s (~)0.902s101.02x
💻 LocalNext.js (Turbopack)1.131s2.007s0.875s101.04x
🐘 PostgresNext.js (Turbopack)1.148s2.008s0.860s101.06x
🐘 PostgresNitro1.236s (+8.4% 🔺)2.141s (+6.5% 🔺)0.904s101.14x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)1.588s (-22.0% 🟢)3.265s (-14.8% 🟢)1.677s101.00x
▲ VercelExpress1.664s (-11.2% 🟢)3.733s (-2.0%)2.068s101.05x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express10.503s (-3.8%)11.022s (~)0.519s31.00x
💻 LocalNitro10.526s (-3.8%)11.023s (~)0.497s31.00x
🐘 PostgresNitro10.529s (-3.1%)11.022s (~)0.493s31.00x
🐘 PostgresExpress10.545s (-3.8%)11.018s (~)0.473s31.00x
💻 LocalNext.js (Turbopack)10.785s11.022s0.237s31.03x
🐘 PostgresNext.js (Turbopack)11.049s11.377s0.328s31.05x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express13.588s (-20.0% 🟢)15.386s (-23.1% 🟢)1.798s21.00x
▲ VercelNext.js (Turbopack)13.804s (-20.3% 🟢)15.183s (-21.7% 🟢)1.379s21.02x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro13.707s (-9.0% 🟢)14.027s (-12.5% 🟢)0.320s51.00x
💻 LocalExpress13.742s (-8.2% 🟢)14.027s (-6.7% 🟢)0.284s51.00x
🐘 PostgresExpress13.838s (-5.1% 🟢)14.017s (-6.7% 🟢)0.179s51.01x
🐘 PostgresNitro13.968s (-4.3%)14.220s (-5.4% 🟢)0.253s51.02x
💻 LocalNext.js (Turbopack)14.294s15.029s0.735s41.04x
🐘 PostgresNext.js (Turbopack)14.648s15.039s0.391s41.07x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express21.658s (-57.0% 🟢)24.002s (-54.3% 🟢)2.344s31.00x
▲ VercelNext.js (Turbopack)22.778s (-56.7% 🟢)24.044s (-56.0% 🟢)1.266s31.05x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro12.381s (-11.4% 🟢)13.017s (-9.0% 🟢)0.636s71.00x
💻 LocalExpress12.430s (-25.1% 🟢)13.025s (-23.5% 🟢)0.595s71.00x
💻 LocalNitro12.436s (-25.9% 🟢)13.023s (-23.5% 🟢)0.587s71.00x
🐘 PostgresExpress12.735s (-9.1% 🟢)13.161s (-9.8% 🟢)0.426s71.03x
💻 LocalNext.js (Turbopack)13.552s14.028s0.475s71.09x
🐘 PostgresNext.js (Turbopack)15.224s15.527s0.303s61.23x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express32.528s (-73.2% 🟢)34.863s (-71.8% 🟢)2.335s31.00x
▲ VercelNext.js (Turbopack)33.524s (-91.5% 🟢)35.875s (-90.9% 🟢)2.351s31.03x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.177s (-6.6% 🟢)2.008s (~)0.831s151.00x
🐘 PostgresNitro1.183s (-7.2% 🟢)2.006s (~)0.824s151.00x
💻 LocalNitro1.216s (-25.5% 🟢)2.006s (-3.3%)0.790s151.03x
💻 LocalExpress1.231s (-17.3% 🟢)2.006s (~)0.775s151.05x
🐘 PostgresNext.js (Turbopack)1.279s2.014s0.735s151.09x
💻 LocalNext.js (Turbopack)1.338s2.006s0.668s151.14x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.781s (-18.1% 🟢)4.314s (-12.5% 🟢)1.533s71.00x
▲ VercelExpress3.517s (+23.0% 🔺)5.310s (+14.8% 🔺)1.792s71.26x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.233s (-47.8% 🟢)2.006s (-33.3% 🟢)0.773s151.00x
🐘 PostgresNitro1.258s (-46.5% 🟢)2.008s (-33.3% 🟢)0.751s151.02x
🐘 PostgresNext.js (Turbopack)1.460s2.178s0.718s141.18x
💻 LocalExpress1.715s (-41.9% 🟢)2.006s (-41.9% 🟢)0.291s151.39x
💻 LocalNitro1.759s (-44.0% 🟢)2.006s (-48.4% 🟢)0.248s151.43x
💻 LocalNext.js (Turbopack)1.897s2.222s0.324s141.54x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)3.937s (-44.6% 🟢)5.503s (-38.2% 🟢)1.567s61.00x
▲ VercelExpress4.903s (+35.5% 🔺)6.440s (+26.0% 🔺)1.538s51.25x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.343s (-61.5% 🟢)2.007s (-50.0% 🟢)0.664s151.00x
🐘 PostgresNitro1.412s (-59.4% 🟢)2.009s (-49.9% 🟢)0.597s151.05x
🐘 PostgresNext.js (Turbopack)1.847s2.475s0.627s131.38x
💻 LocalNitro4.680s (-43.9% 🟢)5.178s (-42.6% 🟢)0.498s63.48x
💻 LocalNext.js (Turbopack)4.971s5.344s0.373s63.70x
💻 LocalExpress5.031s (-39.7% 🟢)5.681s (-37.1% 🟢)0.649s63.75x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.918s (+39.6% 🔺)7.648s (+24.8% 🔺)1.730s41.00x
▲ VercelNext.js (Turbopack)9.703s (+8.8% 🔺)11.652s (+6.3% 🔺)1.948s31.64x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.180s (-6.1% 🟢)2.007s (~)0.827s151.00x
🐘 PostgresNitro1.184s (-5.8% 🟢)2.008s (~)0.823s151.00x
🐘 PostgresNext.js (Turbopack)1.379s2.031s0.652s151.17x
💻 LocalNext.js (Turbopack)1.423s2.073s0.650s151.21x
💻 LocalNitro1.554s (-16.7% 🟢)2.006s (-14.3% 🟢)0.453s151.32x
💻 LocalExpress1.569s (-17.2% 🟢)2.073s (-12.3% 🟢)0.504s151.33x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.512s (-2.7%)4.051s (-6.9% 🟢)1.539s81.00x
▲ VercelNext.js (Turbopack)2.736s (-6.7% 🟢)4.245s (-8.6% 🟢)1.509s81.09x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.228s (-47.6% 🟢)2.010s (-33.3% 🟢)0.782s151.00x
🐘 PostgresNitro1.267s (-45.8% 🟢)2.009s (-33.3% 🟢)0.741s151.03x
🐘 PostgresNext.js (Turbopack)1.610s2.110s0.501s151.31x
💻 LocalExpress1.896s (-39.5% 🟢)2.294s (-39.0% 🟢)0.398s141.54x
💻 LocalNext.js (Turbopack)2.024s2.675s0.651s121.65x
💻 LocalNitro2.062s (-32.7% 🟢)2.508s (-35.5% 🟢)0.446s121.68x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.804s (+19.2% 🔺)5.424s (+13.2% 🔺)1.619s61.00x
▲ VercelNext.js (Turbopack)4.447s (+41.5% 🔺)5.763s (+27.4% 🔺)1.315s61.17x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.334s (-61.9% 🟢)2.007s (-49.9% 🟢)0.674s151.00x
🐘 PostgresNitro1.383s (-60.3% 🟢)2.010s (-49.9% 🟢)0.627s151.04x
🐘 PostgresNext.js (Turbopack)1.845s2.517s0.672s121.38x
💻 LocalNitro5.586s (-38.9% 🟢)6.213s (-38.0% 🟢)0.627s54.19x
💻 LocalNext.js (Turbopack)5.624s6.215s0.591s54.22x
💻 LocalExpress5.679s (-35.5% 🟢)6.216s (-33.0% 🟢)0.538s54.26x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)5.652s (-16.3% 🟢)7.583s (-11.3% 🟢)1.930s51.00x
▲ VercelExpress5.927s (-7.6% 🟢)7.693s (-5.9% 🟢)1.766s41.05x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.576s (-31.4% 🟢)1.023s (~)0.448s591.00x
🐘 PostgresNitro0.578s (-29.5% 🟢)1.023s (+1.7%)0.445s591.00x
💻 LocalExpress0.585s (-40.6% 🟢)1.005s (-6.6% 🟢)0.420s601.02x
💻 LocalNitro0.590s (-39.9% 🟢)1.005s (-8.2% 🟢)0.415s601.02x
🐘 PostgresNext.js (Turbopack)0.834s1.145s0.311s531.45x
💻 LocalNext.js (Turbopack)0.861s1.039s0.178s581.50x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)5.613s (-61.3% 🟢)6.842s (-57.5% 🟢)1.229s91.00x
▲ VercelExpress5.725s (-69.9% 🟢)7.533s (-64.7% 🟢)1.808s91.02x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.341s (-30.4% 🟢)2.030s (-3.3%)0.689s451.00x
🐘 PostgresExpress1.347s (-31.8% 🟢)2.007s (-11.1% 🟢)0.660s451.00x
💻 LocalExpress1.470s (-51.3% 🟢)2.006s (-44.1% 🟢)0.536s451.10x
💻 LocalNitro1.502s (-50.5% 🟢)2.028s (-46.0% 🟢)0.527s451.12x
💻 LocalNext.js (Turbopack)2.051s2.911s0.859s311.53x
🐘 PostgresNext.js (Turbopack)2.699s3.174s0.475s292.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express14.798s (-57.1% 🟢)17.019s (-53.8% 🟢)2.221s61.00x
▲ VercelNext.js (Turbopack)15.167s (-69.5% 🟢)17.136s (-66.9% 🟢)1.969s61.02x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.653s (-35.4% 🟢)3.033s (-34.1% 🟢)0.381s401.00x
🐘 PostgresExpress2.718s (-31.9% 🟢)3.111s (-28.8% 🟢)0.393s391.02x
💻 LocalNitro3.247s (-65.1% 🟢)4.009s (-60.0% 🟢)0.762s301.22x
💻 LocalExpress3.379s (-63.3% 🟢)4.147s (-58.6% 🟢)0.768s291.27x
💻 LocalNext.js (Turbopack)4.242s5.009s0.767s241.60x
🐘 PostgresNext.js (Turbopack)4.269s4.866s0.597s251.61x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express30.021s (-76.9% 🟢)32.852s (-75.1% 🟢)2.831s41.00x
▲ VercelNext.js (Turbopack)30.866s (-71.2% 🟢)32.904s (-69.8% 🟢)2.038s41.03x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.207s (-27.0% 🟢)1.006s (~)0.799s601.00x
🐘 PostgresExpress0.228s (-19.4% 🟢)1.006s (~)0.778s601.10x
🐘 PostgresNext.js (Turbopack)0.423s1.043s0.621s582.05x
💻 LocalNitro0.429s (-29.0% 🟢)1.004s (-1.7%)0.575s602.08x
💻 LocalExpress0.548s (-2.3%)1.115s (+11.1% 🔺)0.568s542.65x
💻 LocalNext.js (Turbopack)0.562s1.022s0.460s592.72x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.382s (+21.9% 🔺)4.078s (+12.1% 🔺)1.696s151.00x
▲ VercelNext.js (Turbopack)46.327s (+2190.6% 🔺)47.594s (+1154.6% 🔺)1.267s719.45x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.326s (-36.1% 🟢)1.006s (~)0.680s901.00x
🐘 PostgresNitro0.346s (-30.3% 🟢)1.007s (~)0.661s901.06x
🐘 PostgresNext.js (Turbopack)0.477s1.022s0.545s891.46x
💻 LocalNitro2.109s (-16.9% 🟢)2.637s (-12.4% 🟢)0.527s356.48x
💻 LocalExpress2.213s (-11.9% 🟢)2.715s (-9.8% 🟢)0.502s346.80x
💻 LocalNext.js (Turbopack)2.281s3.010s0.729s307.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.971s (+63.2% 🔺)6.820s (+41.9% 🔺)1.849s141.00x
▲ VercelNext.js (Turbopack)6.013s (+70.1% 🔺)7.503s (+44.5% 🔺)1.491s131.21x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.611s (-25.3% 🟢)1.006s (-1.1%)0.394s1201.00x
🐘 PostgresNitro0.653s (-17.4% 🟢)1.006s (~)0.353s1201.07x
🐘 PostgresNext.js (Turbopack)0.847s1.238s0.391s971.39x
💻 LocalNitro9.442s (-15.6% 🟢)10.111s (-13.3% 🟢)0.669s1215.44x
💻 LocalExpress9.561s (-14.6% 🟢)10.027s (-16.0% 🟢)0.466s1215.64x
💻 LocalNext.js (Turbopack)10.096s10.861s0.765s1216.51x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express15.112s (+103.7% 🔺)17.658s (+91.0% 🔺)2.546s71.00x
▲ VercelNext.js (Turbopack)19.091s (+84.8% 🔺)20.788s (+69.2% 🔺)1.697s71.26x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.157s (+481.1% 🔺)2.005s (+99.6% 🔺)0.011s (-11.6% 🟢)2.018s (+98.2% 🔺)0.861s101.00x
💻 LocalNitro1.161s (+443.2% 🔺)2.005s (+99.6% 🔺)0.011s (-8.0% 🟢)2.019s (+98.1% 🔺)0.858s101.00x
🐘 PostgresNitro1.162s (+466.8% 🔺)2.001s (+100.2% 🔺)0.001s (-20.0% 🟢)2.010s (+98.8% 🔺)0.848s101.00x
🐘 PostgresExpress1.162s (+466.7% 🔺)2.001s (+100.4% 🔺)0.001s (-25.0% 🟢)2.010s (+98.7% 🔺)0.848s101.00x
💻 LocalNext.js (Turbopack)1.204s2.004s0.012s2.020s0.815s101.04x
🐘 PostgresNext.js (Turbopack)1.246s2.001s0.001s2.013s0.768s101.08x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.238s (-67.3% 🟢)2.943s (-66.0% 🟢)3.523s (+457.5% 🔺)6.871s (-29.8% 🟢)4.633s101.00x
▲ VercelExpress2.298s (-8.3% 🟢)3.341s (-18.3% 🟢)2.155s (+124.3% 🔺)6.004s (+7.4% 🔺)3.706s101.03x
▲ VercelNitro⚠️missing-----

🔍 Observability: Next.js (Turbopack) | Express

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.565s (+106.7% 🔺)2.009s (+95.3% 🔺)0.011s (+16.5% 🔺)2.022s (+94.4% 🔺)0.457s301.00x
🐘 PostgresNitro1.578s (+152.8% 🔺)2.004s (+99.1% 🔺)0.004s (-3.3%)2.025s (+98.1% 🔺)0.447s301.01x
💻 LocalNitro1.592s (+89.8% 🔺)2.009s (+98.6% 🔺)0.010s (+8.9% 🔺)2.021s (+81.1% 🔺)0.430s301.02x
🐘 PostgresExpress1.617s (+156.6% 🔺)2.006s (+99.3% 🔺)0.003s (-11.2% 🟢)2.024s (+97.8% 🔺)0.407s301.03x
💻 LocalNext.js (Turbopack)1.716s2.008s0.011s2.021s0.305s301.10x
🐘 PostgresNext.js (Turbopack)1.959s2.406s0.002s2.424s0.464s251.25x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)5.968s (-64.7% 🟢)7.233s (-60.3% 🟢)0.279s (+31.9% 🔺)7.984s (-57.8% 🟢)2.017s81.00x
▲ VercelExpress6.090s (-6.4% 🟢)7.303s (-8.8% 🟢)0.484s (+18.5% 🔺)8.346s (-5.5% 🟢)2.256s81.02x
▲ VercelNitro⚠️missing-----

🔍 Observability: Next.js (Turbopack) | Express

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.675s (-29.8% 🟢)1.016s (-20.5% 🟢)0.000s (-22.0% 🟢)1.027s (-21.3% 🟢)0.352s591.00x
🐘 PostgresNitro0.720s (-25.7% 🟢)1.033s (-17.2% 🟢)0.000s (-100.0% 🟢)1.048s (-16.7% 🟢)0.328s581.07x
🐘 PostgresNext.js (Turbopack)0.995s1.392s0.000s1.428s0.433s431.47x
💻 LocalNitro1.392s (+13.8% 🔺)2.013s (~)0.001s (+533.3% 🔺)2.016s (~)0.624s302.06x
💻 LocalExpress1.410s (+15.1% 🔺)2.014s (~)0.000s (-70.0% 🟢)2.016s (~)0.606s302.09x
💻 LocalNext.js (Turbopack)1.472s2.013s0.000s2.015s0.543s302.18x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)3.824s (-62.4% 🟢)5.060s (-56.1% 🟢)0.002s (+Infinity% 🔺)5.461s (-54.7% 🟢)1.637s111.00x
▲ VercelExpress3.832s (+2.5%)5.091s (~)0.000s (-100.0% 🟢)5.621s (+1.6%)1.789s111.00x
▲ VercelNitro⚠️missing-----

🔍 Observability: Next.js (Turbopack) | Express

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.392s (-22.3% 🟢)2.097s (-2.1%)0.000s (+93.1% 🔺)2.115s (-2.7%)0.723s291.00x
🐘 PostgresExpress1.431s (-19.2% 🟢)2.069s (-5.0%)0.000s (+Infinity% 🔺)2.085s (-5.2% 🟢)0.653s291.03x
🐘 PostgresNext.js (Turbopack)2.206s2.685s0.000s2.730s0.524s221.58x
💻 LocalNext.js (Turbopack)2.764s3.357s0.001s3.362s0.598s181.99x
💻 LocalNitro2.994s (-11.6% 🟢)3.675s (-8.9% 🟢)0.001s (+65.4% 🔺)3.678s (-8.9% 🟢)0.683s172.15x
💻 LocalExpress3.376s (-2.6%)3.827s (-5.1% 🟢)0.001s (+41.7% 🔺)4.164s (+3.2%)0.789s152.43x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)5.848s (+4.1%)7.019s (+0.5%)0.000s (-100.0% 🟢)7.397s (-1.9%)1.550s91.00x
▲ VercelExpress5.974s (+30.2% 🔺)7.405s (+23.0% 🔺)0.000s (NaN%)7.926s (+22.8% 🔺)1.953s81.02x
▲ VercelNitro⚠️missing-----

🔍 Observability: Next.js (Turbopack) | Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalExpress10/21
🐘 PostgresExpress13/21
▲ VercelExpress12/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres14/21
Next.js (Turbopack)🐘 Postgres13/21
Nitro🐘 Postgres16/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)
  • 🌐 Redis: Community world (local development)
  • 🌐 Redis + BullMQ: Community world (local development)
  • 🌐 Cloudflare: Community world (local development)
  • 🌐 MySQL: Community world (local development)
  • 🌐 Azure: Community world (local development)
  • 🌐 NATS JetStream: Community world (local development)
  • 🌐 Upstash: Community world (local development)

📋 View full workflow run


Some benchmark jobs failed:

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

Check the workflow run for details.

@github-actions

github-actionsBot commented May 29, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production126602191485
✅ 💻 Local Development167102191890
✅ 📦 Local Production167102191890
✅ 🐘 Local Postgres167102191890
✅ 🪟 Windows13500135
✅ 📋 Other7690176945
Total7183010528235

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro109026
✅ example109026
✅ express109026
✅ fastify109026
✅ hono109026
✅ nextjs-turbopack13302
✅ nextjs-webpack13302
✅ nitro109026
✅ nuxt109026
✅ sveltekit12807
✅ vite109026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable110025
✅ express-stable110025
✅ fastify-stable110025
✅ hono-stable110025
✅ nextjs-turbopack-canary116019
✅ nextjs-turbopack-stable-lazy-discovery-disabled13500
✅ nextjs-turbopack-stable-lazy-discovery-enabled13500
✅ nextjs-webpack-canary116019
✅ nextjs-webpack-stable-lazy-discovery-disabled13500
✅ nextjs-webpack-stable-lazy-discovery-enabled13500
✅ nitro-stable110025
✅ nuxt-stable110025
✅ sveltekit-stable12906
✅ vite-stable110025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable110025
✅ express-stable110025
✅ fastify-stable110025
✅ hono-stable110025
✅ nextjs-turbopack-canary116019
✅ nextjs-turbopack-stable-lazy-discovery-disabled13500
✅ nextjs-turbopack-stable-lazy-discovery-enabled13500
✅ nextjs-webpack-canary116019
✅ nextjs-webpack-stable-lazy-discovery-disabled13500
✅ nextjs-webpack-stable-lazy-discovery-enabled13500
✅ nitro-stable110025
✅ nuxt-stable110025
✅ sveltekit-stable12906
✅ vite-stable110025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable110025
✅ express-stable110025
✅ fastify-stable110025
✅ hono-stable110025
✅ nextjs-turbopack-canary116019
✅ nextjs-turbopack-stable-lazy-discovery-disabled13500
✅ nextjs-turbopack-stable-lazy-discovery-enabled13500
✅ nextjs-webpack-canary116019
✅ nextjs-webpack-stable-lazy-discovery-disabled13500
✅ nextjs-webpack-stable-lazy-discovery-enabled13500
✅ nitro-stable110025
✅ nuxt-stable110025
✅ sveltekit-stable12906
✅ vite-stable110025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack13500
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable110025
✅ e2e-local-dev-tanstack-start-110025
✅ e2e-local-postgres-nest-stable110025
✅ e2e-local-postgres-tanstack-start-110025
✅ e2e-local-prod-nest-stable110025
✅ e2e-local-prod-tanstack-start-110025
✅ e2e-vercel-prod-tanstack-start109026

📋 View full workflow run

@VaguelySerious
VaguelySerious marked this pull request as ready for review May 29, 2026 12:24
@VaguelySerious
VaguelySerious requested a review from a team as a code ownerMay 29, 2026 12:24
## Usage

Call [`experimental_setAttributes`](/docs/api-reference/workflow/experimental-set-attributes) from a `"use workflow"` function. Calling it from a `"use step"` function or plain application code is not supported.
Call [`experimental_setAttributes`](/docs/api-reference/workflow/experimental-set-attributes) from a `"use workflow"` function or a `"use step"` function. Plain application code is not supported because there is no active workflow run to attach attributes to.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Suggested change
Call [`experimental_setAttributes`](/docs/api-reference/workflow/experimental-set-attributes) from a `"use workflow"` function or a `"use step"` function. Plain application code is not supported because there is no active workflow run to attach attributes to.
Call [`experimental_setAttributes`](/docs/api-reference/workflow/experimental-set-attributes) from a `"use workflow"` function or a `"use step"` function.

- Setting attributes is currently slower than the final API will be, because each write goes through an internal workflow step. Prefer batching related attributes in one call.
- Workflow-body storage errors are logged after retries, but do not fail the workflow run.
- Step-body storage errors throw from `experimental_setAttributes` like any other step-side network write. Catch the error inside the step if the attribute is best-effort.
- Setting attributes from a workflow body is currently slower than the final API will be, because each write goes through an internal workflow step. Step-body calls post directly to the World. Prefer batching related attributes in one call.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Suggested change
- Setting attributes from a workflow body is currently slower than the final API will be, because each write goes through an internal workflow step. Step-body calls post directly to the World. Prefer batching related attributes in one call.
- Setting attributes from within a workflow function (as opposed to a step function) is currently slower than the final API will be, because each write goes through an internal workflow step. Step-body calls post directly to the World. Prefer batching related attributes in one call.

@VaguelySeriousVaguelySerious changed the title Allow experimental attributes inside steps[core] Allow settings attributes from inside step functionsMay 29, 2026

@TooTallNateTooTallNate 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.

Approve — clean follow-up to the attributes MVP, picks up the scope cut from #2088

This is the step-body support that #2088 deliberately left out, plus a nice refactor that pulls validation into a shared helper. The implementation is straightforward:

  • Workflow-body path unchanged: still dispatches through __builtin_set_attributes step bridge for event-log materialization.
  • Step-body path is new: the host-side set-attributes.ts (which used to throw FatalError) now reads workflowRunId from contextStorage, validates, and posts directly to world.runs.experimentalSetAttributes(). No nested step needed — step bodies already run in host context.
  • Plain host code path: still throws FatalError because there's no active workflow run.

The attribute-changes.ts shared helper extraction is exactly the right move — the workflow-side and host-side both need the same input normalization + validation + AttributeValidationError → FatalError conversion, and DRY'ing them up prevents future drift.

What I verified locally

  • pnpm install --frozen-lockfile
  • pnpm turbo run build --filter @workflow/core
  • pnpm --filter @workflow/core test ✓ (1051/1051)
  • Specifically ran set-attributes.test.ts — 5/5 pass, covering: plain host code throws FatalError, step context posts directly, allowReservedAttributes forwarded, validation rejects before world call, warn-once for unsupported worlds.
  • Confirmed WorldCacheKey is the same symbol (@workflow/world//cache) used by both getWorldLazy() and the existing __builtin_set_attributes builtin — no symbol-mismatch concern.
  • Rebased onto current main cleanly. The "deletions" of trace-viewer files in the GitHub diff are stale-branch artifacts from being forked before #2144 landed; squash merge produces a clean commit.

E2E test design is sharp

The test specifically asserts that NO__builtin_set_attributes step events are created when calling from a step body:

expect(events.some((e)=>(e.eventType==='step_created'||e.eventType==='step_completed')&&stepName.includes('__builtin_set_attributes'))).toBe(false);

That's the right invariant — it documents that the step-body path is genuinely a direct write, not a nested step dispatch.

CI

109 success, 1 failure (Benchmark Vercel (nitro-v3) — recurring infra flake, fails regularly on main too).

One inline comment

The PR body acknowledges a forward-compat asymmetry that the docs don't yet capture — see inline. Step-body writes will be inherently non-deterministic from the workflow's perspective when V1's getAttribute() lands, while workflow-body writes will be cleanly event-sourceable. Worth documenting up-front so users don't write step-body setAttributes calls expecting deterministic reads later.

Non-blocker — the implementation is correct and the asymmetry isn't introduced by this PR (it's inherent to where step bodies execute). Just want the docs to match reality before users adopt the pattern.

Notable trade-off worth flagging (also non-blocker)

FatalError on validation still applies to both workflow-body and step-body calls (because both go through the shared normalizeAttributeChanges helper). Karthik flagged on PR #2088 that this is potentially too heavy-handed for telemetry — "I moved a tag call into a step → my prod run dies". The host-side wrong-call-site path now has a defensible answer (no run exists, so the call is meaningless), but the validation-error case still kills the run for what is fundamentally observability metadata. Not made worse by this PR. Worth a follow-up discussion eventually.

Approving.

- World implementations emit a side-channel observability record per successful write (in `world-vercel`, this hooks into the same observability/analytics pipeline already used for other run lifecycle events)

Calling `experimental_setAttributes` from a step body or plain host code is intentionally not supported in the MVP — the host-side export throws `FatalError` directing callers back to a workflow body. This keeps the implementation a single dispatch path; step-body support can be added later without breaking the workflow-body contract.
Calling `experimental_setAttributes` from a step body was intentionally not supported in the MVP, but step-body calls are now supported as a follow-up. Plain host code remains unsupported because there is no active workflow run to attach attributes to.

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.

Forward-compat caveat in the PR body should make it into the docs.

The PR description says:

This is forward-compatible with event-based attributes if we accept attributes set from a step-level potentially racing with other calls, and accept not supporting deterministic getAttribute calls from within a workflow context. This will need to be documented when we move to the new implementation.

That second part is the bigger semantic asymmetry — and it's the thing a user is most likely to be surprised by when V1 lands with getAttribute():

  • Workflow-body writes go through __builtin_set_attributes (step boundary), and when V1 lands they convert cleanly to attr_set events that the VM replays deterministically. getAttribute() from inside the workflow body will see them.
  • Step-body writes post directly to the world from host context, outside the VM. When V1 lands, these can't be event-sourced (there's no spot in the workflow's event log where they were issued — they're step-side side effects). So getAttribute() from inside the workflow body won't see them deterministically; they're a race with whatever else writes to the same key.

For the MVP this asymmetry is invisible because there's no getAttribute() yet. But the contract is being set now and will be load-bearing when V1 ships. Worth documenting up-front so users don't write step-body setAttributes calls and then expect deterministic reads from the workflow body later.

Suggested addition under the existing "When the full feature ships" paragraph or a new note in attributes-mvp.mdx:

Step-body vs workflow-body forward-compat asymmetry. Workflow-body writes go through a step boundary and will be event-sourced as attr_set events when V1 lands, so future getAttribute() calls from the same workflow body will see them deterministically. Step-body writes post directly to the World outside the VM and are inherently non-deterministic from the workflow's perspective — they won't appear in event replay. Use step-body writes for observability metadata that you don't intend to read back from inside the run; use workflow-body writes for anything you might want to read deterministically later.

Non-blocker (the implementation is correct and the asymmetry is real, not invented by this PR), but worth landing the docs change before users start adopting step-body writes for the wrong reasons.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

We likely won't ship getAttribute, so this is nil

@VaguelySerious
VaguelySerious merged commit 409b103 into mainMay 29, 2026
183 of 192 checks passed
@VaguelySerious
VaguelySerious deleted the peter/step-attributes branch May 29, 2026 17:38
@github-actionsgithub-actionsBot mentioned this pull request May 29, 2026
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for 409b103 (AI decision).

This commit extends the experimental_setAttributes API (originally added in #2134) to support step-body calls. The underlying attributes feature does not exist on stablepackages/core/src/set-attributes.ts, packages/core/src/workflow/set-attributes.ts, and the attribute docs under docs/content/docs/v5/ are all absent on stable. Backporting would require the entire attributes MVP first, so this change is inherently main-only.

To override, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

409b1033d9b7dfab9c26fda9a17494c08e43d0ae

pranaygp added a commit that referenced this pull request Jun 2, 2026
* origin/main:
[world-vercel] Retry transient response-body parse failures in the HTTP client (#2204)
Add virtualization to the trace viewer (#2205)
Trace viewer: scroll-load events past an auto-load cap (#2200)
fix(core): resolve forwarded stream keys across deployments (#2191)
[e2e] Improve error labeling in event-log-race-repro CI job (#2190)
[core] Harden event pagination response parsing (#2180) (#2179)
Add loading skeleton to the new trace viewer (#2164)
Add tooltip components + apply on up/down detail pane (#2163)
fix(swc-plugin): allow wasm host imports during link (#2174)
[test] Forward-port reused-sleep replay divergence test (#2172)
[e2e] Add `event-log-race-repro` label for triggering CI stress-test (#2159)
Version Packages (beta) (#2162)
fix(world-local): skip Nov 2025 ghost versions on npm (#2168)
fix(core,errors): classify SDK encryption failures as RUNTIME_ERROR (#2145)
[web-shared][web] Fix events tab search (#2107)
Version Packages (beta) (#2147)
Allow setting workflow attributes from steps (#2157)
Better search handling on the trace viewer (#2144)
[docs] Document experimental attributes feature (#2141)
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.

2 participants

@VaguelySerious@TooTallNate