Starting Workflows
Trigger workflow execution with the start() function and track progress with Run objects.
Once you've defined your workflow functions, you need to trigger them to begin execution. This is done using the start() function from workflow/api, which enqueues a new workflow run and returns a Run object that you can use to track its progress.
The start() Function
The start() function is used to programmatically trigger workflow executions from runtime contexts like API routes, Server Actions, or any server-side code.
import { start } from "workflow/api";
import { handleUserSignup } from "./workflows/user-signup";
export async function POST(request: Request) {
const { email } = await request.json();
// Start the workflow
const run = await start(handleUserSignup, [email]);
return Response.json({
message: "Workflow started",
runId: run.runId
});
}Key Points:
start()returns immediately after enqueuing the workflow - it doesn't wait for completion- The first argument is your workflow function
- The second argument is an array of arguments to pass to the workflow (optional if the workflow takes no arguments)
- All arguments must be serializable
Learn more: start() API Reference
The Run Object
When you call start(), it returns a Run object that provides access to the workflow's status and results.
import { start } from "workflow/api";
import { processOrder } from "./workflows/process-order";
const run = await start(processOrder, [/* orderId */]);
// The run object has properties you can await
console.log("Run ID:", run.runId);
// Check the workflow status
const status = await run.status; // "running" | "completed" | "failed"
// Get the workflow's return value (blocks until completion)
const result = await run.returnValue;Key Properties:
runId- Unique identifier for this workflow runstatus- Current status of the workflow (async)returnValue- The value returned by the workflow function (async, blocks until completion)readable- ReadableStream for streaming updates from the workflow
Most Run properties are async getters that return promises. You need to await them to get their values. For a complete list of properties and methods, see the API reference below.
Learn more: Run API Reference
Common Patterns
Starting Workflows from Workflow Functions
You can also call start() directly inside workflow functions to spawn child workflows:
import { start } from "workflow/api";
import { childWorkflow } from "./workflows/child";
export async function parentWorkflow(inputValue: number) {
"use workflow";
const childRun = await start(childWorkflow, [inputValue]);
// childRun is a full Run object — use it like normal
const childResult = await childRun.returnValue;
return { childRunId: childRun.runId, childResult };
}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.
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.
returnValue currently polls the child workflow every 1 second and holds a serverless worker alive for the entire duration of the child workflow. This is a temporary implementation — a future release will use internal hooks so the parent workflow can suspend and be resumed by the child, avoiding the polling overhead. For now, if the child workflow is long-running, prefer a fire-and-forget pattern combined with hooks to receive a notification when the child completes.
Fire and Forget
The most common pattern is to start a workflow and immediately return, letting it execute in the background:
import { start } from "workflow/api";
import { sendNotifications } from "./workflows/notifications";
export async function POST(request: Request) {
// Start workflow and don't wait for it
const run = await start(sendNotifications, [userId]);
// Return immediately
return Response.json({
message: "Notifications queued",
runId: run.runId
});
}Wait for Completion
If you need to wait for the workflow to complete before responding:
import { start } from "workflow/api";
import { generateReport } from "./workflows/reports";
export async function POST(request: Request) {
const run = await start(generateReport, [reportId]);
// Wait for the workflow to complete
const report = await run.returnValue;
return Response.json({ report });
}Be cautious when waiting for returnValue - if your workflow takes a long time, your API route may timeout.
Stream Updates to Client
Stream real-time updates from your workflow as it executes, without waiting for completion:
import { start } from "workflow/api";
import { generateAIContent } from "./workflows/ai-generation";
export async function POST(request: Request) {
const { prompt } = await request.json();
// Start the workflow
const run = await start(generateAIContent, [prompt]);
// Get the readable stream (can also use run.readable as shorthand)
const stream = run.getReadable();
// Return the stream immediately
return new Response(stream, {
headers: {
"Content-Type": "application/octet-stream",
},
});
}Your workflow can write to the stream using getWritable():
import { getWritable } from "workflow";
export async function generateAIContent(prompt: string) {
"use workflow";
const writable = getWritable();
await streamContentToClient(writable, prompt);
return { status: "complete" };
}
async function streamContentToClient(
writable: WritableStream,
prompt: string
) {
"use step";
const writer = writable.getWriter();
// Stream updates as they become available
for (let i = 0; i < 10; i++) {
const chunk = new TextEncoder().encode(`Update ${i}\n`);
await writer.write(chunk);
}
writer.releaseLock();
}Streams are particularly useful for AI workflows where you want to show progress to users in real-time, or for long-running processes that produce intermediate results.
Learn more: Streaming in Workflows
Check Status Later
You can retrieve a workflow run later using its runId with getRun():
import { getRun } from "workflow/api";
export async function GET(request: Request) {
const url = new URL(request.url);
const runId = url.searchParams.get("runId");
// Retrieve the existing run
const run = getRun(runId);
// Check its status
const status = await run.status;
if (status === "completed") {
const result = await run.returnValue;
return Response.json({ result });
}
return Response.json({ status });
}Recursive and Repeating Workflows
A workflow can start a new instance of itself. This is useful when a single long-running workflow would accumulate too many events — large event logs become slower to replay, more expensive to store, and harder to inspect in the UI. By breaking work into smaller runs that chain together, each run stays lean.
import { sleep } from "workflow";
import { start } from "workflow/api";
declare function fetchBatch(cursor?: string): Promise<{ items: string[]; nextCursor?: string }>; // @setup
declare function processBatch(items: string[]): Promise<void>; // @setup
export async function processQueue(cursor?: string) {
"use workflow";
const { items, nextCursor } = await fetchBatch(cursor);
await processBatch(items);
if (nextCursor) {
// Continue processing in a new workflow run
await start(processQueue, [nextCursor]);
}
}This pattern also enables repeating cron-like workflows. A workflow can complete its work, sleep, and then schedule a new instance of itself — creating an indefinite chain without any single run growing too large:
import { sleep } from "workflow";
import { start } from "workflow/api";
declare function refreshMetrics(): Promise<void>; // @setup
export async function syncDashboard() {
"use workflow";
await refreshMetrics();
await sleep("1h");
// Schedule the next run
await start(syncDashboard);
}Starting against the latest deployment
For self-chaining workflows that run over long periods, you may want the next run to use the latest deployment so it picks up new code. Pass deploymentId: "latest" to start():
import { sleep } from "workflow";
import { start } from "workflow/api";
declare function doWork(): Promise<void>; // @setup
export async function repeatingJob() {
"use workflow";
await doWork();
await sleep("6h");
// Next run uses the latest deployment's code
await start(repeatingJob, [], { deploymentId: "latest" });
}See the start() API reference for details on deploymentId: "latest" behavior, including compatibility considerations across deployments.
Next Steps
Now that you understand how to start workflows and track their execution:
- Learn about Common Patterns for organizing complex workflows
- Explore Errors & Retrying to handle failures gracefully
- Check the
start()API Reference for complete details