feat: allow start() to be called directly inside workflow functions#1491
feat: allow start() to be called directly inside workflow functions#1491
Conversation
🦋 Changeset detectedLatest commit: e75a208 The changes in this PR will be included in the next version bump. This PR includes changesets to release 16 packages
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 |
📊 Benchmark Results
workflow with no steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express | Nitro | Next.js (Turbopack) workflow with 1 step💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Next.js (Turbopack) | Express workflow with 10 sequential steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Next.js (Turbopack) | Express workflow with 25 sequential steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Express | Next.js (Turbopack) workflow with 50 sequential steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Next.js (Turbopack) | Express Promise.all with 10 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Next.js (Turbopack) | Express Promise.all with 25 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express | Nitro | Next.js (Turbopack) Promise.all with 50 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express | Next.js (Turbopack) | Nitro Promise.race with 10 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Express | Next.js (Turbopack) Promise.race with 25 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express | Next.js (Turbopack) | Nitro Promise.race with 50 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express | Nitro | Next.js (Turbopack) workflow with 10 sequential data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) | Nitro | Express workflow with 25 sequential data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Next.js (Turbopack) | Express workflow with 50 sequential data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Next.js (Turbopack) | Express workflow with 10 concurrent data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Express | Next.js (Turbopack) workflow with 25 concurrent data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Express | Next.js (Turbopack) workflow with 50 concurrent data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Next.js (Turbopack) | Express Stream Benchmarks (includes TTFB metrics)workflow with stream💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express | Nitro | Next.js (Turbopack) stream pipeline with 5 transform steps (1MB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express | Next.js (Turbopack) | Nitro 10 parallel streams (1MB each)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Express | Next.js (Turbopack) fan-out fan-in 10 streams (1MB each)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express | Next.js (Turbopack) | Nitro SummaryFastest Framework by WorldWinner determined by most benchmark wins
Fastest World by FrameworkWinner determined by most benchmark wins
Column Definitions
Worlds:
|
🧪 E2E Test Results❌ Some tests failed Summary
❌ Failed Tests▲ Vercel Production (1 failed)nextjs-turbopack (1 failed):
📦 Local Production (1 failed)nextjs-turbopack-stable (1 failed):
🐘 Local Postgres (3 failed)hono-stable (2 failed):
nextjs-turbopack-stable (1 failed):
🌍 Community Worlds (76 failed)mongodb (7 failed):
redis (7 failed):
turso (62 failed):
Details by Category❌ ▲ Vercel Production
✅ 💻 Local Development
❌ 📦 Local Production
❌ 🐘 Local Postgres
✅ 🪟 Windows
❌ 🌍 Community Worlds
✅ 📋 Other
❌ Some E2E test jobs failed:
Check the workflow run for details. |
Revert "Add support for calling start() inside workflow functions"
Revert "Add support for calling start() inside workflow functions"There was a problem hiding this comment.
Pull request overview
Re-enables calling start() from workflow/api directly inside "use workflow" functions by routing the call through internal built-in steps, with Run objects working in workflow VM context (step-backed getters/methods) and improved observability UI hydration/rendering for run references.
Changes:
- Add workflow-context
start()delegation viaWORKFLOW_START, backed by a built-instartstep and a workflow-VMWorkflowRunproxy class. - Extend serialization/hydration to support
Run/WorkflowRunacross boundaries and render Run references as clickable UI elements. - Update builders, plugin behavior/docs, and add unit + e2e coverage for
startFromWorkflowand recursivefibonacciWorkflow.
Reviewed changes
Copilot reviewed 38 out of 38 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| workbench/nextjs-turbopack/app/workflows/definitions.ts | Adds default args for new e2e workflows. |
| workbench/example/workflows/99_e2e.ts | Adds startFromWorkflow and recursive fibonacciWorkflow workflows for e2e. |
| skills/workflow/SKILL.md | Updates skill guidance to reflect start() support inside workflows. |
| packages/workflow/src/runtime.ts | Re-exports getRun and start from core runtime for rewritten step imports. |
| packages/workflow/src/internal/builtins.ts | Adds built-in start step and Run.* step-backed static methods. |
| packages/workflow/src/api-workflow.ts | Routes start() in workflow bundle via WORKFLOW_START injection. |
| packages/web/app/components/run-detail-view.tsx | Adds run-ref click navigation wiring. |
| packages/web-shared/src/lib/hydration.ts | Exposes RunRef helpers for UI hydration. |
| packages/web-shared/src/components/workflow-traces/trace-span-construction.ts | Improves span naming fallback behavior. |
| packages/web-shared/src/components/workflow-trace-view.tsx | Plumbs onRunClick and resets selection on run change. |
| packages/web-shared/src/components/ui/data-inspector.tsx | Adds RunRef rendering + “opaque ref” collapsing + click contexts. |
| packages/web-shared/src/components/sidebar/entity-detail-panel.tsx | Passes onRunClick into detail panel rendering. |
| packages/web-shared/src/components/sidebar/attribute-panel.tsx | Adds Run click context and improves stepName display fallback. |
| packages/web-shared/src/components/run-trace-view.tsx | Threads onRunClick through trace view components. |
| packages/swc-plugin-workflow/transform/src/lib.rs | Removes __builtin* step-id special casing to use normal IDs. |
| packages/swc-plugin-workflow/spec.md | Updates spec examples to include builtins module-specifier IDs. |
| packages/next/src/builder-deferred.ts | Rewrites bare specifiers (incl. dynamic import) and ensures builtins are included in step files. |
| packages/core/src/workflow/start.ts | Implements workflow-VM createStart() using internal start built-in step. |
| packages/core/src/workflow/start.test.ts | Adds unit tests for workflow-context start() behavior and option validation. |
| packages/core/src/workflow/run.ts | Adds workflow-VM WorkflowRun proxy delegating to built-in Run steps. |
| packages/core/src/workflow/run.test.ts | Adds unit tests asserting step delegation naming/arguments. |
| packages/core/src/workflow/builtin-step-id.ts | Adds helper to construct built-in step IDs. |
| packages/core/src/workflow.ts | Injects WORKFLOW_START, registers WorkflowRun, and updates response builtins to qualified IDs. |
| packages/core/src/symbols.ts | Adds WORKFLOW_START symbol. |
| packages/core/src/serialization.ts | Adds Run reducer/reviver using __serializable marker + class registry. |
| packages/core/src/serialization-format.ts | Adds RunRef marker/type + reviver mapping Run → RunRef for o11y. |
| packages/core/src/runtime/step-handler.ts | Registers runtime Run class in host class registry for Run reviver. |
| packages/core/src/runtime/start.ts | Delegates start() to injected workflow implementation when in workflow VM. |
| packages/core/src/runtime/run.ts | Adds Run.__serializable marker for serialization. |
| packages/core/src/private.ts | Extends builtin step aliasing to accept fully-qualified builtins step IDs. |
| packages/core/e2e/e2e.test.ts | Adds e2e tests validating workflow-context start() and recursion. |
| docs/proxy.ts | Formatting-only string quote normalization. |
| docs/lib/ai-agent-detection.ts | Formatting-only string quote normalization. |
| docs/content/docs/foundations/starting-workflows.mdx | Documents calling start() inside workflow functions and recursive patterns. |
| docs/content/docs/foundations/common-patterns.mdx | Updates patterns docs to use direct start() in workflows. |
| docs/content/docs/api-reference/workflow-api/start.mdx | Updates API reference to include workflow-context usage. |
| AGENTS.md | Updates local e2e instructions to include WORKFLOW_PUBLIC_MANIFEST=1. |
| .changeset/start-in-workflow.md | Declares patch release for workflow-context start() support. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Add 'use step' to start() so it can be called directly from workflow
code. The SWC compiler strips the function body in workflow mode and
replaces it with a step proxy. When called from a workflow:
1. The workflow function reference is serialized via WorkflowFunction
reducer (serializes { workflowId })
2. start() executes in the step context with full Node.js access
3. The returned Run is serialized via WORKFLOW_SERIALIZE and deserialized
back in the workflow VM
4. Run getters (.status, .returnValue, etc.) are 'use step' getters
that each execute as separate steps
Also re-exports start from @workflow/core/runtime/start in api-workflow.ts
instead of using a throwing stub, adds e2e tests for startFromWorkflow
(with hook communication) and fibonacciWorkflow (recursive composition).
… duplicate classes Files belonging to packages (detected by walking up to find a package.json with a name field) are imported via relative path instead of being copied to __workflow_step_files__/. Copying creates a second module instance which breaks JS native private field (#) brand checks when the runtime creates instances from one copy and the step handler accesses fields from the other.
…t all package step files Regular package step files (like fetch) must still be copied to ensure the SWC loader registers them. Only serde class files from packages are excluded from copying since those define classes with JS native private fields (#) that break when duplicated.
…d of full copies For package files that define serde classes (like Run), generate a thin wrapper that imports the original class and registers steps/classes from the manifest. This avoids duplicating the class definition (which breaks JS native private field brand checks) while still registering all step functions and the class in the serialization registry. Regular package step files (like fetch) are still copied as before.
… step mode Instead of copying package serde+step files (which creates duplicate classes with #private brand check issues) or generating fragile wrappers, add the original file paths to a shared forceStepModeFiles set. The loader checks this set and transforms those files in step mode directly, so the SWC plugin generates proper step registrations on the original class — no duplication, no reimplemented registration logic.
…just copies The loader now selects step mode for any file that has 'use step' directives or serde patterns, regardless of whether it's a deferred step copy. Step mode is a superset of client mode — the only addition is step registry IIFEs, which are harmless for non-step consumers. This means package serde+step files (like Run) no longer need to be copied to get step registrations. They're imported directly in the step route and the loader transforms the original file in step mode. One class instance, no duplication, no wrapper generation.
Summary
Add
"use step"tostart()so it can be called directly from"use workflow"functions, without needing a wrapper step function.This is a dramatically simplified replacement for the original PR approach. Instead of complex VM-side proxy classes and builtin step delegation, the entire implementation is:
"use step"tostart()inpackages/core/src/runtime/start.tsstartfrom@workflow/core/runtime/startinapi-workflow.ts(instead of a throwing stub)This works because:
start()is serialized as{ workflowId }and deserialized as a function with.workflowIdRunreturned bystart()is serialized viaWORKFLOW_SERIALIZEand deserialized in the workflow VMrun.status,run.returnValue,run.cancel(), etc. are"use step"getters/methods that each execute as separate stepsChanges
packages/core/src/runtime/start.ts: Add"use step"directivepackages/workflow/src/api-workflow.ts: Re-exportstartfrom core instead of stubworkbench/example/workflows/99_e2e.ts: AddstartFromWorkflowandfibonacciWorkflowe2e workflowspackages/core/e2e/e2e.test.ts: Add e2e testsworkbench/nextjs-turbopack/app/workflows/definitions.ts: Add default argsE2E Tests
start(), child signals parent via hook, verifies bidirectional communicationfib(6) = 8via tree of child workflows usingPromise.all([start(...), start(...)])