Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 10 additions & 8 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,14 +228,16 @@ sleep but a durable pause.
await step.sleep("wait-one-hour", "1h");
```

**`step.invokeWorkflow(name, options)`**: Starts a child workflow and waits for
it durably. When the timeout is reached (default 7d), the parent step fails but
the child workflow continues running independently.

All step APIs (`step.run`, `step.sleep`, and `step.invokeWorkflow`) share the
same collision logic for durable keys. If duplicate base names are encountered
in one execution pass, OpenWorkflow auto-indexes them as `name`, `name:1`,
`name:2`, and so on so each step call maps to a distinct step attempt.
**`step.runWorkflow(spec, input?, options?)`**: Starts a child workflow and
waits for it durably. `options.name` sets the durable step name (defaults to the
target workflow name in `spec`) and `options.timeout` controls the wait timeout
(default 7d). When the timeout is reached, the parent step fails but the child
workflow continues running independently.

All step APIs (`step.run`, `step.sleep`, and `step.runWorkflow`) share the same
collision logic for durable keys. If duplicate base names are encountered in one
execution pass, OpenWorkflow auto-indexes them as `name`, `name:1`, `name:2`,
and so on so each step call maps to a distinct step attempt.

## 4. Error Handling & Retries

Expand Down
6 changes: 2 additions & 4 deletions openworkflow/hello-world-parent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,14 @@ import { helloWorld } from "./hello-world.js";
import { defineWorkflow } from "openworkflow";

/**
* Example workflow that invokes hello-world as a child workflow.
* Example workflow that runs hello-world as a child workflow.
*/
export const helloWorldParent = defineWorkflow(
{ name: "hello-world-parent" },
async ({ step, run }) => {
console.log(`[run ${run.id}]`);

const childResult = await step.invokeWorkflow("hello-world-child", {
workflow: helloWorld,
});
const childResult = await step.runWorkflow(helloWorld.spec);

return { childResult, parentMessage: "Hello from the parent workflow!" };
},
Expand Down
4 changes: 2 additions & 2 deletions packages/dashboard/src/routes/runs/$runId.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export const Route = createFileRoute("/runs/$runId")({
...new Set(
steps
.map((step) =>
step.kind === "invoke" ? step.childWorkflowRunId : null,
step.kind === "workflow" ? step.childWorkflowRunId : null,
)
.filter((childRunId): childRunId is string => childRunId !== null),
),
Expand Down Expand Up @@ -186,7 +186,7 @@ function RunDetailsPage() {
const stepTypeLabel =
step.kind === "function" ? "run" : step.kind;
const childRunId =
step.kind === "invoke" ? step.childWorkflowRunId : null;
step.kind === "workflow" ? step.childWorkflowRunId : null;
const childRun = childRunId
? (childRunsById[childRunId] ?? null)
: null;
Expand Down
87 changes: 37 additions & 50 deletions packages/docs/docs/child-workflows.mdx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
---
title: Child Workflows
description: Invoke workflows from other workflows and wait for their results
description: Run workflows from other workflows and wait for their results
---

A parent workflow can start a child workflow and durably wait for its result
using `step.invokeWorkflow()`. This lets you compose complex processes from
using `step.runWorkflow()`. This lets you compose complex processes from
smaller, reusable workflows — like splitting an order pipeline into separate
payment and shipping workflows.

Expand Down Expand Up @@ -39,9 +39,8 @@ const processOrder = defineWorkflow(
});

// Start the report workflow and wait for it to finish
const report = await step.invokeWorkflow("generate-report", {
workflow: generateReport,
input: { reportId: input.orderId },
const report = await step.runWorkflow(generateReport.spec, {
reportId: input.orderId,
});

await step.run({ name: "send-confirmation" }, async () => {
Expand All @@ -58,29 +57,28 @@ const processOrder = defineWorkflow(
## Timeout

By default, the parent waits up to **7 days** for the child to finish. You can
customize this with the `timeout` option:
customize this with `options.timeout`:

```ts
const result = await step.invokeWorkflow("quick-task", {
workflow: quickTaskWorkflow,
input: { taskId: "abc" },
timeout: "5m", // wait at most 5 minutes
});
const result = await step.runWorkflow(
quickTaskWorkflow.spec,
{ taskId: "abc" },
{ timeout: "5m" }, // wait at most 5 minutes
);
```

`timeout` accepts a [duration string](/docs/sleeping#duration-formats), a
number of milliseconds, or a `Date`:
`timeout` accepts a [duration string](/docs/sleeping#duration-formats), a number
of milliseconds, or a `Date`:

```ts
// Duration string
await step.invokeWorkflow("task", { workflow: w, timeout: "1h" });
await step.runWorkflow(w.spec, undefined, { timeout: "1h" });

// Milliseconds
await step.invokeWorkflow("task", { workflow: w, timeout: 60_000 });
await step.runWorkflow(w.spec, undefined, { timeout: 60_000 });

// Absolute deadline
await step.invokeWorkflow("task", {
workflow: w,
await step.runWorkflow(w.spec, undefined, {
timeout: new Date("2026-03-01"),
});
```
Expand All @@ -91,43 +89,34 @@ await step.invokeWorkflow("task", {
canceled.
</Warning>

## Workflow Target
## Workflow Spec

The `workflow` option accepts a workflow definition, a workflow spec, or a plain
string name:
The first argument accepts a workflow spec:

```ts
// Workflow definition (recommended — type-safe input/output)
await step.invokeWorkflow("run-child", {
workflow: myWorkflow,
input: { key: "value" },
});

// Workflow spec
await step.invokeWorkflow("run-child", {
workflow: myWorkflow.spec,
input: { key: "value" },
});
// From a defined workflow
await step.runWorkflow(myWorkflow.spec, { key: "value" });

// String name (useful when the child is defined in another package)
await step.invokeWorkflow("run-child", {
workflow: "my-workflow",
input: { 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 invoke step also fails:
If the child workflow **fails**, the parent workflow step also fails:

```ts
const orderPipeline = defineWorkflow(
{ name: "order-pipeline" },
async ({ input, step }) => {
try {
const result = await step.invokeWorkflow("charge", {
workflow: chargeWorkflow,
input: { orderId: input.orderId },
const result = await step.runWorkflow(chargeWorkflow.spec, {
orderId: input.orderId,
});
return result;
} catch (error) {
Expand All @@ -140,13 +129,13 @@ const orderPipeline = defineWorkflow(
);
```

If the child workflow is **canceled**, the parent invoke step fails with an
If the child workflow is **canceled**, the parent workflow step fails with an
error indicating the child was canceled.

<Note>
Invoke 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.
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.
</Note>

## Parallel Child Workflows
Expand All @@ -155,13 +144,11 @@ Start multiple child workflows concurrently with `Promise.all`:

```ts
const [payment, shipping] = await Promise.all([
step.invokeWorkflow("process-payment", {
workflow: paymentWorkflow,
input: { orderId: input.orderId },
step.runWorkflow(paymentWorkflow.spec, {
orderId: input.orderId,
}),
step.invokeWorkflow("prepare-shipping", {
workflow: shippingWorkflow,
input: { orderId: input.orderId },
step.runWorkflow(shippingWorkflow.spec, {
orderId: input.orderId,
}),
]);
```
2 changes: 1 addition & 1 deletion packages/docs/docs/roadmap.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ description: What's coming next for OpenWorkflow
- ✅ Configurable retry policies
- ✅ Idempotency keys
- ✅ Prometheus `/metrics` endpoint
- ✅ Child workflows (`step.invokeWorkflow`)
- ✅ Child workflows (`step.runWorkflow`)

## Coming Soon

Expand Down
12 changes: 6 additions & 6 deletions packages/docs/docs/steps.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -136,16 +136,16 @@ Pauses the workflow until a specified duration has elapsed. See
await step.sleep("wait-one-hour", "1h");
```

### `step.invokeWorkflow()`
### `step.runWorkflow()`

Starts a child workflow and waits for its result durably:

```ts
const childOutput = await step.invokeWorkflow("generate-report", {
workflow: generateReportWorkflow,
input: { reportId: input.reportId },
timeout: "5m", // optional, defaults to 7 days
});
const childOutput = await step.runWorkflow(
generateReportWorkflow.spec,
{ reportId: input.reportId },
{ timeout: "5m" }, // optional, defaults to 7 days
);
```

## Retry Policy (Optional)
Expand Down
14 changes: 7 additions & 7 deletions packages/docs/docs/workflows.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -213,12 +213,12 @@ create a separate run.

The workflow function receives an object with four properties:

| Parameter | Type | Description |
| --------- | --------------------- | ------------------------------------------------------------------------ |
| `input` | Generic | The input data passed when starting the workflow |
| `step` | `StepApi` | API for defining steps (`step.run`, `step.sleep`, `step.invokeWorkflow`) |
| `version` | `string \| null` | The workflow version, if specified |
| `run` | `WorkflowRunMetadata` | Read-only run metadata snapshot (`run.id`, etc.) |
| Parameter | Type | Description |
| --------- | --------------------- | --------------------------------------------------------------------- |
| `input` | Generic | The input data passed when starting the workflow |
| `step` | `StepApi` | API for defining steps (`step.run`, `step.sleep`, `step.runWorkflow`) |
| `version` | `string \| null` | The workflow version, if specified |
| `run` | `WorkflowRunMetadata` | Read-only run metadata snapshot (`run.id`, etc.) |

```ts
defineWorkflow({ name: "example" }, async ({ input, step, version, run }) => {
Expand All @@ -240,7 +240,7 @@ A workflow run progresses through these states:
| ----------- | -------------------------------------------------------------- |
| `pending` | Created and waiting for a worker to claim it |
| `running` | Actively being executed by a worker |
| `sleeping` | Paused while waiting for `step.sleep` or `step.invokeWorkflow` |
| `sleeping` | Paused while waiting for `step.sleep` or `step.runWorkflow` |
| `completed` | Finished successfully |
| `failed` | Failed after exhausting retries, hitting deadline, or step cap |
| `canceled` | Explicitly canceled and will not continue |
Expand Down
2 changes: 1 addition & 1 deletion packages/openworkflow/client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export class OpenWorkflow {
* @returns Handle for awaiting the result
* @example
* ```ts
* const handle = await ow.runWorkflow(emailWorkflow, { to: 'user@example.com' });
* const handle = await ow.runWorkflow(emailWorkflow.spec, { to: 'user@example.com' });
* const result = await handle.result();
* ```
*/
Expand Down
16 changes: 8 additions & 8 deletions packages/openworkflow/core/step-attempt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
normalizeStepOutput,
calculateDateFromDuration,
createSleepContext,
createInvokeContext,
createWorkflowContext,
} from "./step-attempt.js";
import type { StepAttempt, StepAttemptCache } from "./step-attempt.js";
import { describe, expect, test } from "vitest";
Expand Down Expand Up @@ -318,22 +318,22 @@ describe("createSleepContext", () => {
});
});

describe("createInvokeContext", () => {
test("creates invoke context with timeout", () => {
describe("createWorkflowContext", () => {
test("creates workflow context with timeout", () => {
const timeoutAt = new Date("2025-06-15T10:30:00.000Z");
const context = createInvokeContext(timeoutAt);
const context = createWorkflowContext(timeoutAt);

expect(context).toEqual({
kind: "invoke",
kind: "workflow",
timeoutAt: "2025-06-15T10:30:00.000Z",
});
});

test("creates invoke context with null timeout", () => {
const context = createInvokeContext(null);
test("creates workflow context with null timeout", () => {
const context = createWorkflowContext(null);

expect(context).toEqual({
kind: "invoke",
kind: "workflow",
timeoutAt: null,
});
});
Expand Down
20 changes: 10 additions & 10 deletions packages/openworkflow/core/step-attempt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { err, ok } from "./result.js";
/**
* The kind of step in a workflow.
*/
export type StepKind = "function" | "sleep" | "invoke";
export type StepKind = "function" | "sleep" | "workflow";
Comment thread
jamescmartinez marked this conversation as resolved.

/**
* Status of a step attempt through its lifecycle.
Expand All @@ -27,10 +27,10 @@ export interface SleepStepAttemptContext {
}

/**
* Context for an invoke step attempt.
* Context for a workflow step attempt.
*/
export interface InvokeStepAttemptContext {
kind: "invoke";
export interface WorkflowStepAttemptContext {
kind: "workflow";
timeoutAt: string | null;
}

Expand All @@ -39,7 +39,7 @@ export interface InvokeStepAttemptContext {
*/
export type StepAttemptContext =
| SleepStepAttemptContext
| InvokeStepAttemptContext;
| WorkflowStepAttemptContext;

/**
* StepAttempt represents a single attempt of a step within a workflow.
Expand Down Expand Up @@ -159,15 +159,15 @@ export function createSleepContext(
}

/**
* Create the context object for an invoke step attempt.
* Create the context object for a workflow step attempt.
* @param timeoutAt - Parent wait timeout deadline, or null for no timeout
* @returns The context object for an invoke step
* @returns The context object for a workflow step
*/
export function createInvokeContext(
export function createWorkflowContext(
timeoutAt: Readonly<Date> | null,
): InvokeStepAttemptContext {
): WorkflowStepAttemptContext {
return {
kind: "invoke" as const,
kind: "workflow" as const,
timeoutAt: timeoutAt?.toISOString() ?? null,
};
}
Loading
Loading