Skip to main content
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,
  }),
]);