Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Add support for calling start() inside workflow functions#1133

Draft
pranaygp wants to merge 2 commits intomainfrom
pgp/start-in-workflow
Draft

Add support for calling start() inside workflow functions#1133
pranaygp wants to merge 2 commits intomainfrom
pgp/start-in-workflow

Conversation

@pranaygp
Copy link
Collaborator

Summary

  • Enable start() from workflow/api to work directly inside "use workflow" functions
  • Routes through an internal step (__workflow_start) reusing existing step infrastructure — no new event types, no server/world changes
  • Returns { runId } in workflow context (not full Run object; use getRun(runId) in a step for full access)

Implementation

  • Added WORKFLOW_START symbol and createStart(ctx) factory that creates a step proxy
  • Registered __workflow_start built-in step in the step handler that calls real start()
  • Public start() detects workflow context via symbol check and delegates accordingly
  • Added e2e test with parent→child workflow communication via hooks
  • Updated docs (starting-workflows, start API reference, common-patterns) and skill file

Test plan

  • All 354 unit tests pass (pnpm test)
  • Full build succeeds (26/26 turbo tasks)
  • E2e test: DEPLOYMENT_URL="http://localhost:3000" APP_NAME="nextjs-turbopack" pnpm vitest run packages/core/e2e/e2e.test.ts -t "startFromWorkflow"
  • Existing spawn test still passes: -t "spawnWorkflowFromStepWorkflow"

🤖 Generated with Claude Code

Enable `start()` to work in workflow context by routing through an
internal step (`__workflow_start`), reusing existing step infrastructure
with no new event types or server changes needed.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Copilot AI review requested due to automatic review settings February 20, 2026 01:57
@changeset-bot
Copy link

changeset-bot bot commented Feb 20, 2026

🦋 Changeset detected

Latest commit: d832df7

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

This PR includes changesets to release 14 packages
Name Type
@workflow/core Patch
workflow Patch
@workflow/builders Patch
@workflow/cli Patch
@workflow/next Patch
@workflow/nitro Patch
@workflow/web-shared Patch
@workflow/world-testing Patch
@workflow/astro Patch
@workflow/nest Patch
@workflow/rollup Patch
@workflow/sveltekit Patch
@workflow/vite Patch
@workflow/nuxt Patch

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

@vercel
Copy link
Contributor

vercel bot commented Feb 20, 2026

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Enable start() (from workflow/api) to be invoked directly inside "use workflow" functions by routing the call through an internal built-in step, keeping workflow execution deterministic.

Changes:

  • Injected a workflow-context start implementation into the workflow VM via a new WORKFLOW_START symbol and createStart(ctx) factory.
  • Registered a built-in __workflow_start step in the step handler that calls the real runtime start() and returns the spawned runId.
  • Added an e2e scenario and updated docs/skill content to document workflow-context semantics ({ runId } only).

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
workbench/example/workflows/99_e2e.ts Adds example parent→child workflow start from workflow context with hook signaling.
skills/workflow/SKILL.md Documents that start() works in workflow functions and returns { runId } there.
packages/core/src/workflow/start.ts Implements createStart(ctx) that proxies start() through __workflow_start.
packages/core/src/workflow.ts Injects WORKFLOW_START into the workflow VM global to enable delegation.
packages/core/src/symbols.ts Adds the WORKFLOW_START symbol.
packages/core/src/runtime/step-handler.ts Registers the built-in __workflow_start step for workflow-context start().
packages/core/src/runtime/start.ts Delegates to injected workflow start when running inside the workflow VM.
packages/core/e2e/e2e.test.ts Adds e2e coverage for starting a child workflow from a workflow function.
docs/content/docs/foundations/starting-workflows.mdx Adds docs section describing start() usage inside workflow functions.
docs/content/docs/foundations/common-patterns.mdx Updates “background execution” guidance to use start() directly in workflows.
docs/content/docs/api-reference/workflow-api/start.mdx Documents workflow-context start() behavior and return shape.
.changeset/start-in-workflow.md Declares a patch release for the new capability.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

// When called inside a workflow function, delegate to the injected start implementation
// which routes through an internal step for deterministic replay.
const workflowStartFn = (globalThis as any)[WORKFLOW_START];
if (workflowStartFn) {
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

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

workflowStartFn is checked for truthiness but not for being callable. If anything sets globalThis[WORKFLOW_START] to a non-function value, this will throw a confusing "is not a function" error and bypass the normal runtime implementation. Suggest guarding with typeof workflowStartFn === 'function' before delegating (and otherwise falling back to the normal runtime path).

Suggested change
if (workflowStartFn) {
if (typeof workflowStartFn === 'function') {

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good catch — addressed in d832df7. Using typeof workflowStartFn === 'function' now.

Comment on lines +26 to +34
// Parse overloaded args/options (same pattern as real start)
let args: Serializable[] = [];
let opts: Serializable = (options ?? {}) as Serializable;
if (Array.isArray(argsOrOptions)) {
args = argsOrOptions as Serializable[];
} else if (typeof argsOrOptions === 'object' && argsOrOptions !== null) {
opts = argsOrOptions as Serializable;
}

Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

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

createStart() forwards options through step argument serialization (opts: Serializable). StartOptions includes world?: World, which won’t be serializable and (worse) could deserialize to an invalid shape and then break start() inside the step handler. Consider explicitly validating/sanitizing the allowed options in workflow context (e.g., disallow world, and only permit serializable fields like deploymentId / specVersion) and throw a clear error if unsupported options are provided.

Suggested change
// Parse overloaded args/options (same pattern as real start)
let args: Serializable[] = [];
let opts: Serializable = (options ?? {}) as Serializable;
if (Array.isArray(argsOrOptions)) {
args = argsOrOptions as Serializable[];
} else if (typeof argsOrOptions === 'object' && argsOrOptions !== null) {
opts = argsOrOptions as Serializable;
}
// Parse overloaded args/options (same pattern as real start),
// but explicitly validate/sanitize options to ensure they are serializable
// and do not include unsupported fields like `world`.
let args: Serializable[] = [];
let rawOpts: Record<string, unknown> =
(options as Record<string, unknown>) ?? {};
if (Array.isArray(argsOrOptions)) {
args = argsOrOptions as Serializable[];
} else if (typeof argsOrOptions === 'object' && argsOrOptions !== null) {
rawOpts = argsOrOptions as Record<string, unknown>;
}
// Only allow a limited set of workflow options in this context.
const allowedOptionKeys = new Set(['deploymentId', 'specVersion']);
const sanitizedOpts: Record<string, Serializable> = {};
for (const [key, value] of Object.entries(rawOpts)) {
if (!allowedOptionKeys.has(key)) {
throw new Error(
`Unsupported option '${key}' passed to 'start' in workflow context. ` +
`Only 'deploymentId' and 'specVersion' are allowed.`
);
}
sanitizedOpts[key] = value as Serializable;
}
const opts: Serializable = sanitizedOpts as Serializable;

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Addressed in d832df7. Options are now validated against an allowlist (deploymentId, specVersion). Unsupported options like world throw a clear error.

Comment on lines +5 to +9
export function createStart(ctx: WorkflowOrchestratorContext) {
const internalStartStep = createUseStep(ctx)<
[string, Serializable[], Serializable],
string
>('__workflow_start');
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

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

There are dedicated unit tests for other workflow-context factories like createSleep and createCreateHook, but this new createStart implementation has no unit test coverage. Adding a focused unit test that asserts start() inside workflow context enqueues a __workflow_start step invocation (and returns { runId } on step_completed) would help prevent regressions without relying solely on e2e.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Addressed in d832df7. Added 6 unit tests in src/workflow/start.test.ts covering: step completion → { runId }, suspension when no events, invalid workflow rejection, unsupported options rejection, valid options passthrough, and options parsing from second argument.

options
);
return run.runId;
}
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

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

__workflow_start executes start() inside a regular step, which is subject to step retries (DEFAULT_STEP_MAX_RETRIES). Because start() generates a new runId on each attempt, a retry after a partial failure can spawn multiple child workflow runs (orphaning earlier ones). Consider making this built-in step non-retriable (e.g., set maxRetries = 0/1) and/or adding an idempotency mechanism so the same child runId is reused across retries.

Suggested change
}
},
{ maxRetries: 0 }

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good point — addressed in d832df7. Set maxRetries = 0 on the __workflow_start step since each retry would generate a new runId and orphan earlier child runs.

Comment on lines 66 to +87
export function start<TArgs extends unknown[], TResult>(
workflow: WorkflowFunction<TArgs, TResult> | WorkflowMetadata,
args: TArgs,
options?: StartOptions
): Promise<Run<TResult>>;

export function start<TResult>(
workflow: WorkflowFunction<[], TResult> | WorkflowMetadata,
options?: StartOptions
): Promise<Run<TResult>>;

export async function start<TArgs extends unknown[], TResult>(
workflow: WorkflowFunction<TArgs, TResult> | WorkflowMetadata,
argsOrOptions?: TArgs | StartOptions,
options?: StartOptions
) {
// When called inside a workflow function, delegate to the injected start implementation
// which routes through an internal step for deterministic replay.
const workflowStartFn = (globalThis as any)[WORKFLOW_START];
if (workflowStartFn) {
return workflowStartFn(workflow, argsOrOptions, options);
}
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

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

The function overloads declare start() always returns Promise<Run<TResult>>, but when WORKFLOW_START is present you return the injected workflow implementation (which returns { runId } per the PR). This makes the public API type-unsafe in workflow context (code will typecheck against Run methods that are missing at runtime). Please update the typings to reflect the workflow-context return value (e.g., a union return type or a separate workflow-only overload/type) so TS users can’t accidentally call Run APIs.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Valid concern. The runId property does exist on Run so the most common usage pattern works at runtime. We're planning a follow-up to make Run fully serializable with instance methods that work as steps in workflow context, which will resolve this type mismatch properly.

@pranaygp pranaygp requested a review from TooTallNate February 20, 2026 02:42
With background execution, the parent workflow continues immediately after starting the child. The child workflow runs independently with its own event log and can be monitored separately using the returned `runId`.

<Callout type="info">
Inside workflow functions, `start()` returns an object with a `runId` property. For full `Run` capabilities (like `.returnValue` or `.cancel()`), use `getRun(runId)` inside a step function.
Copy link
Collaborator Author

@pranaygp pranaygp Feb 20, 2026

Choose a reason for hiding this comment

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

actually we should add Run to the serialization layer so that we can serialize and deserialize it correctly. then update the docs and examples everywhere so that you can just use run like normal in workflow.

any instance methods of the Run class that can't run inside the workflow vm should also have "use step" in the function definitions so that they work inside workflow and defer to step when run there (and then noop inside steps)

- Use typeof check instead of truthiness for WORKFLOW_START symbol
- Validate start() options in workflow context (reject unsupported options like world)
- Set maxRetries=0 on __workflow_start step to prevent orphaned child runs
- Add unit tests for createStart factory (6 tests)

Co-Authored-By: Claude Opus 4.6 <[email protected]>
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.

2 participants