Skip to main content
OpenWorkflow is built with TypeScript and provides full type inference for workflow inputs, outputs, and step results. You get autocomplete and compile- time checking throughout.

Schema Validation with Type Inference

For runtime validation with type inference, use Standard Schema compatible libraries like Zod:
import { z } from "zod";

const inputSchema = z.object({
  email: z.string().email(),
  amount: z.number().positive(),
});

export const payment = defineWorkflow(
  {
    name: "payment",
    schema: inputSchema,
  },
  async ({ input, step }) => {
    // input is typed as { email: string; amount: number }
    // and validated at runtime
  },
);

Explicit Type Definitions

To manually specify input and (optionally) output types, use generics with defineWorkflow:
import { defineWorkflow } from "openworkflow";

interface OrderInput {
  orderId: string;
  customerId: string;
}

interface OrderResult {
  success: boolean;
  trackingNumber: string;
}

export const processOrder = defineWorkflow<OrderInput, OrderResult>(
  { name: "process-order" },
  async ({ input, step }) => {
    const validated = await step.run({ name: "validate" }, async () => {
      // input is typed as OrderInput
      return validateOrder(input.orderId);
    });

    const trackingNumber = await step.run({ name: "ship" }, async () => {
      return shipOrder(input.orderId);
    });

    // Return type must match OrderResult
    return {
      success: true,
      trackingNumber,
    };
  },
);
If you only specify the input type, the output type is inferred from the return value:
export const greet = defineWorkflow<{ name: string }>(
  { name: "greet" },
  async ({ input }) => {
    return `Hello, ${input.name}!`; // Output inferred as string
  },
);

Workflow Specs

When using defineWorkflowSpec for separate declaration and implementation, types flow through:
import { defineWorkflowSpec, OpenWorkflow } from "openworkflow";

// Declare the workflow spec with types
export const emailSpec = defineWorkflowSpec<
  { to: string; subject: string },
  { sent: boolean }
>({
  name: "send-email",
});

// Implementation receives typed input
const ow = new OpenWorkflow({ backend });
ow.implementWorkflow(emailSpec, async ({ input, step }) => {
  // input.to and input.subject are typed
  await step.run({ name: "send" }, async () => {
    await sendEmail(input.to, input.subject);
  });
  return { sent: true };
});

Client-Side Type Safety

When you run a workflow, the handle is typed with the output:
const handle = await ow.runWorkflow(processOrder.spec, {
  orderId: "123",
  customerId: "456",
});

// result is typed as OrderResult
const result = await handle.result();
console.log(result.trackingNumber); // TypeScript knows this exists

Step Result Types

Step return types are inferred automatically:
const workflow = defineWorkflow<{ userId: string }>(
  { name: "user-workflow" },
  async ({ input, step }) => {
    // user is inferred as User type from the return
    const user = await step.run({ name: "get-user" }, async () => {
      const response = await fetch(`/api/users/${input.userId}`);
      return response.json() as User;
    });

    // TypeScript knows user has these properties
    console.log(user.email, user.name);
  },
);