[v5 only] docs: restore start-in-workflow documentation#1803
Conversation
🦋 Changeset detectedLatest commit: f3d4f42 The changes in this PR will be included in the next version bump. This PR includes changesets to release 0 packagesWhen changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types 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: Next.js (Turbopack) | Express workflow with 1 step💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express | Next.js (Turbopack) workflow with 10 sequential steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express | Next.js (Turbopack) workflow with 25 sequential steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) | Express workflow with 50 sequential steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) | Express Promise.all with 10 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express | Next.js (Turbopack) Promise.all with 25 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express | Next.js (Turbopack) Promise.all with 50 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express | Next.js (Turbopack) Promise.race with 10 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) | Express Promise.race with 25 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) | Express Promise.race with 50 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express | Next.js (Turbopack) workflow with 10 sequential data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express | Next.js (Turbopack) workflow with 25 sequential data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express | Next.js (Turbopack) workflow with 50 sequential data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express | Next.js (Turbopack) workflow with 10 concurrent data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) | Express workflow with 25 concurrent data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express | Next.js (Turbopack) workflow with 50 concurrent data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express | Next.js (Turbopack) Stream Benchmarks (includes TTFB metrics)workflow with stream💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express | Next.js (Turbopack) stream pipeline with 5 transform steps (1MB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express | Next.js (Turbopack) 10 parallel streams (1MB each)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express | Next.js (Turbopack) fan-out fan-in 10 streams (1MB each)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express | Next.js (Turbopack) 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)sveltekit (1 failed):
🐘 Local Postgres (4 failed)hono-stable (2 failed):
nitro-stable (2 failed):
Details by Category❌ ▲ Vercel Production
✅ 💻 Local Development
✅ 📦 Local Production
❌ 🐘 Local Postgres
✅ 🪟 Windows
✅ 📋 Other
❌ Some E2E test jobs failed:
Check the workflow run for details. |
There was a problem hiding this comment.
Pull request overview
Restores and expands documentation around starting workflows, especially using start() from within workflow functions (child workflows, background execution, and self-chaining/recursive patterns), and adds a no-op changeset to satisfy repo policy.
Changes:
- Add docs/examples for calling
start()inside workflow functions and explain determinism/step behavior. - Document recursive/self-chaining patterns and guidance for
deploymentId: "latest". - Update “Common Patterns” background execution example to use
start()directly (no wrapper step).
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| docs/content/docs/foundations/starting-workflows.mdx | Adds a new “Starting Workflows from Workflow Functions” section plus recursion/self-chaining guidance. |
| docs/content/docs/foundations/common-patterns.mdx | Updates background execution pattern to use start() directly inside workflows and adds a clarifying callout. |
| docs/content/docs/api-reference/workflow-api/start.mdx | Updates “Good to Know” and adds an example for using start() inside workflow functions. |
| .changeset/fresh-rules-glow.md | Adds a no-op/metadata-only changeset file per PR policy. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| * The `start()` function can be used in any context: runtime code (API routes, Server Actions), step functions, or directly inside workflow functions. | ||
| * This is different from calling workflow functions directly, which is the typical pattern in Next.js applications. | ||
| * The function returns immediately after enqueuing the workflow - it doesn't wait for the workflow to complete. |
There was a problem hiding this comment.
The updated "Good to Know" bullet says start() can be used directly inside workflow functions, but the page frontmatter summary still states it's for starting runs "from outside a workflow function." Consider updating the frontmatter summary (and/or intro) so it doesn't contradict this expanded usage.
| --- | ||
| --- | ||
|
|
||
| Restore the missing documentation for calling `start()` directly inside workflow functions. |
There was a problem hiding this comment.
PR description says this is an empty changeset to satisfy policy, but this changeset includes descriptive body text. If the intent is to keep it a true no-op changeset (like other empty entries in .changeset/), consider removing the body text or updating the PR description to match what’s being added.
| When `start()` is called inside a workflow function, it automatically executes through an internal step to maintain deterministic replay. The returned `Run` object works just like it does outside workflows — properties like `.runId`, `.status`, `.returnValue`, and methods like `.cancel()` are all available. Each property access or method call executes as a separate step under the hood. | ||
|
|
||
| <Callout type="info"> | ||
| Inside workflow functions, each `Run` property access (e.g., `run.status`, `run.returnValue`) triggers a workflow step. This means each access is recorded in the event log and replayed deterministically. |
There was a problem hiding this comment.
The text here implies that accessing run.runId inside a workflow triggers a step, but runId is a plain string field on Run (not an async getter with a "use step" directive). Consider rewording to clarify that async Run getters/methods (e.g. status, returnValue, cancel(), getReadable()) run as steps, while runId does not.
| When `start()` is called inside a workflow function, it automatically executes through an internal step to maintain deterministic replay. The returned `Run` object works just like it does outside workflows — properties like `.runId`, `.status`, `.returnValue`, and methods like `.cancel()` are all available. Each property access or method call executes as a separate step under the hood. | |
| <Callout type="info"> | |
| Inside workflow functions, each `Run` property access (e.g., `run.status`, `run.returnValue`) triggers a workflow step. This means each access is recorded in the event log and replayed deterministically. | |
| When `start()` is called inside a workflow function, it automatically executes through an internal step to maintain deterministic replay. The returned `Run` object works just like it does outside workflows — plain fields like `.runId` are available directly, while async getters/methods such as `.status`, `.returnValue`, `.cancel()`, and `.getReadable()` can also be used inside the workflow. | |
| <Callout type="info"> | |
| Inside workflow functions, async `Run` getters and methods (for example `run.status`, `run.returnValue`, `run.cancel()`, and `run.getReadable()`) each trigger a workflow step. Plain fields like `run.runId` do not. These step-backed accesses are recorded in the event log and replayed deterministically. |
| </Callout> | ||
|
|
||
| <Callout type="info"> | ||
| Inside workflow functions, `start()` returns a full `Run` object. You can access `.runId`, `.status`, `.returnValue`, `.cancel()`, and other `Run` properties — each access executes as a separate step. |
There was a problem hiding this comment.
This callout lists .runId among Run properties that execute as steps inside workflow functions, but runId is a synchronous field on Run. Suggest updating the wording/examples to distinguish between synchronous fields (e.g. runId) and step-backed async getters/methods (e.g. status, returnValue, cancel()).
| Inside workflow functions, `start()` returns a full `Run` object. You can access `.runId`, `.status`, `.returnValue`, `.cancel()`, and other `Run` properties — each access executes as a separate step. | |
| Inside workflow functions, `start()` returns a full `Run` object. Some members are synchronous fields, such as `.runId`, which you can read immediately. Others, such as `.status`, `.returnValue`, and `.cancel()`, are step-backed async getters/methods that execute as separate steps inside the workflow. |
This reverts commit d744d47.
| If you want the child workflow to run on the latest deployment rather than the current one, you can pass [`deploymentId: "latest"`](/docs/api-reference/workflow-api/start#using-deploymentid-latest) in the `start()` options. This is currently a Vercel-specific feature. Be aware that the child workflow's function name, file path, argument types, and return type must remain compatible across deployments — renaming the function or changing its location will change the workflow ID, and modifying expected inputs or outputs can cause serialization failures. | ||
| </Callout> | ||
|
|
||
| <Callout type="info"> |
There was a problem hiding this comment.
Review
Good restoration. Feature was added in #1133, reverted in #1475, re-added in #1491 (via the simpler 'use step' approach on the existing start() function). The docs accurately describe the re-introduced implementation.
Accuracy checks
All technical claims verified against the current implementation:
- ✓
start()works inside workflows —'use step'directive atpackages/core/src/runtime/start.ts:122makes it a step call inside a workflow context (#1491). - ✓ Returns a full
Runobject —RunhasWORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZEatrun.ts:73-82, so it survives the step boundary. - ✓ Each property access is a step — Verified for
.status,.returnValue,.cancel(),.exists,.workflowName,.createdAt,.startedAt,.completedAt,.wakeUp(),.getReadable()— all have'use step'. - ✓
returnValuepolls every 1 second — Confirmed atrun.ts:295-346(#pollReturnValuewithsetTimeout(resolve, 1_000)). - ✓ Worker held alive during polling — True; since
returnValueis a step, the step runtime holds the worker until the step resolves (or times out against the function'smaxDuration). - ✓ Recursive/self-chaining pattern — Works because
start()is fire-and-forget; parent completes after enqueueing the child. - ✓
deploymentId: "latest"— Supported (start.ts:158-165resolves viaworld.resolveLatestDeploymentId).
Good calls
- PR body's v5-only note + no-backport instruction is correct. The feature doesn't exist on
stable(v4), so these docs would be wrong there.docs/content/IS maintained on stable per AGENTS.md, so the backport action could pull this in if thebackport-stablelabel is applied — but shouldn't be. Worth the explicit call-out in the PR body. - Polling caveat on
returnValueis an important gotcha that users need to know. The suggested workaround (fire-and-forget + hooks for completion notification) is sound and matches the actual implementation of hooks.
Minor inaccuracy
See inline comment: "each property access or method call executes as a separate step" is slightly over-broad. .runId is a plain field (not a getter) and .readable is a non-'use step' getter. Only "most" properties are steps.
Changeset
The empty changeset is defensible here (docs-only change). Worth noting though: docs/content/docs/foundations/*.mdx is bundled into @workflow/core and workflow via prepack scripts, and docs/content/docs/api-reference/workflow-api/*.mdx is bundled into workflow. So the new docs won't reach npm consumers until the next release that bumps those packages for other reasons. If you want the docs to ship specifically with this PR, add workflow and @workflow/core as patch bumps. Otherwise the current empty changeset is fine — the new docs will ride along with the next beta release.
| } | ||
| ``` | ||
|
|
||
| When `start()` is called inside a workflow function, it automatically executes through an internal step to maintain deterministic replay. The returned `Run` object works just like it does outside workflows — properties like `.runId`, `.status`, `.returnValue`, and methods like `.cancel()` are all available. Each property access or method call executes as a separate step under the hood. |
There was a problem hiding this comment.
Minor: "Each property access or method call executes as a separate step under the hood" is slightly overbroad.
Verified against packages/core/src/runtime/run.ts:
| Member | Step? |
|---|---|
runId |
No (plain field, line 87) |
readable |
No (plain getter, line 244) |
status |
Yes (line 184) |
returnValue |
Yes (line 195) |
cancel() |
Yes (line 154) |
wakeUp() |
Yes (line 146) |
exists |
Yes (line 166) |
workflowName |
Yes (line 203) |
createdAt / startedAt / completedAt |
Yes |
getReadable() |
Yes (line 263) |
runId being free is actually nice for users (e.g., logging run.runId inside a loop doesn't cost N steps). Consider rewording to something like: "Most Run properties — .status, .returnValue, .cancel(), etc. — execute as separate steps. Plain fields like .runId are free."
Summary
start()directly inside workflow functionsNote
Testing