UNPKG

workflow

Version:

Workflow SDK - Build durable, resilient, and observable workflows

204 lines (152 loc) • 13.5 kB
--- title: start description: Start and enqueue a new workflow run. type: reference summary: Use start to programmatically enqueue a new workflow run. prerequisites: - /docs/foundations/starting-workflows related: - /docs/foundations/idempotency - /docs/advanced/dynamic-workflows --- Start/enqueue a new workflow run. ```typescript lineNumbers import { start } from "workflow/api"; import { myWorkflow } from "./workflows/my-workflow"; const run = await start(myWorkflow); // [!code highlight] ``` ## API signature ### Parameters <TSDoc definition={` import { start } from "workflow/api"; export default start;`} showSections={["parameters"]} /> #### StartOptions <TSDoc definition={` import type { StartOptions } from "workflow/api"; export default StartOptions;`} /> ### Returns Returns a `Run` object: <TSDoc definition={` import { Run } from "workflow/api"; export default Run;`} showSections={["returns"]} /> Learn more about [`WorkflowReadableStreamOptions`](/docs/api-reference/workflow-api/get-run#workflowreadablestreamoptions). ## Good to know * Use the `start()` function in runtime contexts to programmatically trigger workflow executions. * In v5, you can also call `start()` directly from a workflow function to spawn a child run or continue work in a new run. See [Workflow Composition](/cookbook/common-patterns/workflow-composition) and [Versioning](/docs/foundations/versioning). * 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. * Each call to `start()` creates a new workflow run. If retried requests must route to one active workflow, have the workflow create a deterministic hook token and use [`getHookByToken()`](/docs/api-reference/workflow-api/get-hook-by-token) to reuse an already-registered active hook. The lookup is not atomic with `start()`, so concurrent callers can still create extra runs before the hook is registered. Handle that race inside the workflow by checking `await hook.getConflict()` before duplicate-sensitive work. On a conflict, it resolves with the run that owns the token, so the duplicate can return the active owner to the caller. If duplicates must be rejected before a workflow body runs, keep a durable request record until native atomic start-and-hook registration exists. See [Idempotency](/docs/foundations/idempotency#run-idempotency). * All arguments must be [serializable](/docs/foundations/serialization). * When you provide `deploymentId`, the argument types and return type become `unknown` because the workflow function's types may differ across deployments. * When `deploymentId` names a deployment other than the caller's, the run is stamped with the spec version the *target* deployment reports on its capability probe (capped at the caller's own), since the target is what executes it. `start()` waits up to 10 seconds for the first probe to a deployment and returns as soon as the target answers; later starts to the same deployment reuse the answer. If the target does not answer in time, the run is stamped with spec version 6, the lowest version a v5 runtime executes; a target on an older major version (such as `stable`) cannot execute such a run, so it logs a warning. In either case, the `attributes` and `experimental_retention` checks below apply to the stamped version, and fail naming the target deployment. * `attributes` seeds plaintext run metadata as part of creation and requires a World implementing spec version 4 or later. Keys that start with `$` are reserved for framework and library code; framework-level callers can pass `allowReservedAttributes: true` to seed reserved keys, with the same semantics as the [`setAttributes`](/docs/api-reference/workflow/set-attributes) option of the same name. * `region` pins the new run to a specific region on Worlds with a regional dimension. The [Vercel World](/worlds/vercel#explicit-region-selection) then serves the run's storage, queue dispatch, and streams from that region. When you omit `region`, the run is pinned to the region where it was created. Worlds without regions ignore the option. * `experimental_retention` asks the World to delete the run's user data as soon as the run completes or fails, instead of keeping it for the World's default window. `0` requests immediate deletion; `'default'` is identical to omitting the option. These are the only two values accepted — the value is a duration and zero is the only one implemented, and its unit is not yet decided. Recorded as the reserved `$retention` attribute, so it needs a World implementing spec version 4 or later. Retention is enforced by the World, not the SDK: the first-party Worlds implement it and a World that does not keeps the data. Note that `await run.returnValue` on a run started with `experimental_retention: 0` usually throws [`RunExpiredError`](/docs/errors/run-expired) rather than resolving, because the deletion races the read. See [Data retention](/docs/observability/retention). <Callout type="info"> If `start()` throws `'start' received an invalid workflow function. Ensure the Workflow SDK is configured correctly and the function includes a 'use workflow' directive.`, the compiler did not transform the passed function as a workflow. The two most common causes are a missing `"use workflow"` directive or missing framework integration. See [start-invalid-workflow-function](/docs/errors/start-invalid-workflow-function). </Callout> ## Examples ### With arguments ```typescript import { start } from "workflow/api"; import { userSignupWorkflow } from "./workflows/user-signup"; const run = await start(userSignupWorkflow, ["user@example.com"]); // [!code highlight] ``` ### With `StartOptions` ```typescript import { start } from "workflow/api"; import { myWorkflow } from "./workflows/my-workflow"; const run = await start(myWorkflow, ["arg1", "arg2"], { // [!code highlight] deploymentId: "custom-deployment-id", // [!code highlight] attributes: { source: "checkout" } // [!code highlight] }); // [!code highlight] ``` ### Using `deploymentId: "latest"` Set `deploymentId` to `"latest"` to automatically resolve the most recent deployment for the current environment. This is useful when you want to ensure a workflow run targets the latest deployed version of your application rather than the deployment that initiated the call. For when to use this and how it fits with default run pinning, see [Versioning](/docs/foundations/versioning). ```typescript import { start } from "workflow/api"; import { myWorkflow } from "./workflows/my-workflow"; const run = await start(myWorkflow, ["arg1", "arg2"], { // [!code highlight] deploymentId: "latest" // [!code highlight] }); // [!code highlight] ``` <Callout type="info"> The `deploymentId` option is currently a Vercel-specific feature. Other Worlds may implement this option differently to match their own deployment runtimes, and the World spec may rename it from `deploymentId` to `version` in a future SDK version. On Vercel, `"latest"` resolves to the most recent deployment matching your current environment: the same production target for production deployments, or the same git branch for preview deployments. In Worlds without atomic, immutable deployments (such as local development or self-hosted Postgres), there is no notion of multiple deployments to resolve between, so `deploymentId: "latest"` has no effect: the SDK logs a warning and the run targets the current deployment. This means a workflow that opts into `"latest"` on Vercel still runs unchanged in local development. </Callout> <Callout type="info"> Resolving `"latest"` is the one `start()` path that calls the Vercel API, so it needs an identity that can see the calling deployment. Inside a Vercel deployment the SDK authenticates with the deployment's own OIDC token, which carries the owning team, and this takes precedence over a `VERCEL_TOKEN` set in the function's environment. A `VERCEL_TOKEN` belongs to a *user* and carries no team, so authenticating with it scopes the lookup to that user's default team and fails with a 404 whenever that is not the team that owns the deployment. Outside a deployment (CLI, CI, the dashboard) `VERCEL_TOKEN` is still used; configure the World's `teamId` so the request is scoped explicitly. </Callout> <Callout type="warn"> When using `deploymentId: "latest"`, the workflow run will execute on a potentially different deployment than the one calling `start()`. Be mindful of forward and backward compatibility: - **Workflow identity**: The function name and file path determine the workflow ID. If the latest deployment has renamed the workflow function or moved it to a different directory, the workflow ID will no longer match and the run will fail to start. - **Input and output compatibility**: The calling deployment serializes the arguments passed to `start()`, and the target deployment deserializes them. Similarly, the target deployment serializes the workflow's return value, and the caller deserializes it. If the workflow's expected arguments or return type have changed (for example, added required fields, removed fields, or changed types), the run may fail or behave unexpectedly. Ensure that input and output schemas remain backward-compatible across deployments. </Callout> ### Inside a workflow function Call `start()` directly from a workflow function to spawn a child run. It is step-backed, so the spawn records a deterministic step boundary in the parent's event log. ```typescript import { start } from "workflow/api"; import { childWorkflow } from "./workflows/child"; export async function parentWorkflow(value: number) { "use workflow"; const childRun = await start(childWorkflow, [value]); // [!code highlight] const result = await childRun.returnValue; // [!code highlight] return { childRunId: childRun.runId, result }; } ``` <Callout type="info"> The returned `Run` object is fully functional inside a workflow. Each property access or method call (`.status`, `.returnValue`, `.cancel()`) executes as a separate step. See [Workflow Composition](/cookbook/common-patterns/workflow-composition) for choosing between spawning a child run and awaiting a workflow function directly. </Callout> <Callout type="warn"> `returnValue` polls the child run every second and holds the polling step's worker slot open for as long as the child takes to finish. For long-running children, spawn without awaiting `returnValue` and have the child resume a [hook](/docs/foundations/hooks) when it completes. See the [`startAndWait()` pattern](/cookbook/advanced/child-workflows). </Callout> ### Dynamic Workflow Source <Callout type="warning"> Experimental and off by default. The API may change without a major version bump. The deployment must set `WORKFLOW_EXPERIMENTAL_DYNAMIC_WORKFLOWS=1`, and dynamic runs can only start on the current deployment. </Callout> `start()` also accepts a string of workflow **source** instead of an imported function, for orchestration whose shape is only known after you deploy. The source is compiled and stored with the run through the same serialization path as other run payloads, including encryption when the World supplies run key material. Dynamic source runs with the full privileges of your deployment's functions: it can read environment variables, use the network and filesystem, and call any step in the deployment. Only pass source you trust as much as your own code. `experimental_dynamic.steps` maps the aliases the source may call to steps that are already registered in your deployment. It keeps ordinary source from calling a step by a name it was not given, but it is not a security boundary. There is no way to define a new step from source. ```typescript import { start } from "workflow/api"; import { fetchUser, sendEmail } from "./steps"; const run = await start( ` async function workflow(input) { "use workflow"; const user = await steps.fetchUser(input.userId); await steps.sendEmail(user.email); return { ok: true }; } `, [{ userId: "user_123" }], { experimental_dynamic: { // [!code highlight] steps: { fetchUser, sendEmail }, // [!code highlight] }, // [!code highlight] } ); ``` | Option | Type | Description | | --- | --- | --- | | `experimental_dynamic.steps` | `Record<string, StepFunction \| { stepId: string }>` | Required. Registered steps the source may call, keyed by the alias it calls them under. | | `experimental_dynamic.exportName` | `string` | Name of the async workflow function in the source. Use letters, digits, and `_`, not starting with a digit. Defaults to `"workflow"`. | The return type is `Run<unknown>`: the source's shape is only known to whatever generated it, so there is nothing to infer. The workflow ID is derived from the source and its step bindings — it cannot be supplied. A dynamic start throws a `WorkflowRuntimeError`, before anything is written, when the deployment has not opted in, when `deploymentId` targets another deployment (including `'latest'` resolving to one), or when the World's backend does not support dynamic-source storage. See [Dynamic Workflows](/docs/advanced/dynamic-workflows) for the opt-in, the source rules and limits, the predefined runtime bindings (`steps`, `sleep`, `createHook`), how the code is stored, and the security model.