A parent workflow can start a child workflow and durably wait for its result
using step.runWorkflow(). This lets you compose complex processes from
smaller, reusable workflows β like splitting an order pipeline into separate
payment and shipping workflows.
The parent sleeps while the child runs, freeing its worker slot. When the child
finishes, the parent automatically resumes and receives the childβs output.
Basic Usage
import { defineWorkflow } from "openworkflow";
const generateReport = defineWorkflow(
{ name: "generate-report" },
async ({ input, step }) => {
const data = await step.run({ name: "fetch-data" }, async () => {
return await db.reports.getData(input.reportId);
});
await step.run({ name: "upload-report" }, async () => {
await storage.upload(`reports/${input.reportId}.pdf`, data);
});
return { reportUrl: `https://example.com/reports/${input.reportId}.pdf` };
},
);
const processOrder = defineWorkflow(
{ name: "process-order" },
async ({ input, step }) => {
await step.run({ name: "charge-card" }, async () => {
await payments.charge(input.orderId);
});
// Start the report workflow and wait for it to finish
const report = await step.runWorkflow(generateReport.spec, {
reportId: input.orderId,
});
await step.run({ name: "send-confirmation" }, async () => {
await email.send({
to: input.email,
subject: "Order complete",
body: `Your report is ready: ${report.reportUrl}`,
});
});
},
);
Timeout
By default, the parent waits up to 1 year for the child to finish. You can
customize this with options.timeout:
const result = await step.runWorkflow(
quickTaskWorkflow.spec,
{ taskId: "abc" },
{ timeout: "5m" }, // wait at most 5 minutes
);
timeout accepts a duration string, a number
of milliseconds, or a Date:
// Duration string
await step.runWorkflow(w.spec, undefined, { timeout: "1h" });
// Milliseconds
await step.runWorkflow(w.spec, undefined, { timeout: 60_000 });
// Absolute deadline
await step.runWorkflow(w.spec, undefined, {
timeout: new Date("2026-03-01"),
});
When the timeout is reached, the parent step fails but the child
workflow keeps running independently. The child is not automatically
canceled.
Workflow Spec
The first argument accepts a workflow spec:
// From a defined workflow
await step.runWorkflow(myWorkflow.spec, { key: "value" });
// Or any WorkflowSpec-compatible object
await step.runWorkflow({ name: "my-workflow" }, { key: "value" });
Step Name
Set options.name to control the durable step name. If omitted, OpenWorkflow
uses the target workflow name.
Error Handling
If the child workflow fails, the parent workflow step also fails:
const orderPipeline = defineWorkflow(
{ name: "order-pipeline" },
async ({ input, step }) => {
try {
const result = await step.runWorkflow(chargeWorkflow.spec, {
orderId: input.orderId,
});
return result;
} catch (error) {
// Handle child failure in the parent
await step.run({ name: "notify-failure" }, async () => {
await alerts.send(`Payment failed for order ${input.orderId}`);
});
}
},
);
If the child workflow is canceled, the parent workflow step fails with an
error indicating the child was canceled.
Workflow steps do not retry automatically. The child workflow is responsible
for its own retries. If the child fails permanently, the error propagates to
the parent.
Parallel Child Workflows
Start multiple child workflows concurrently with Promise.all:
const [payment, shipping] = await Promise.all([
step.runWorkflow(paymentWorkflow.spec, {
orderId: input.orderId,
}),
step.runWorkflow(shippingWorkflow.spec, {
orderId: input.orderId,
}),
]);