OpenWorkflow

Getting Started

Install OpenWorkflow and create your first durable workflow in minutes

Getting Started

This guide will walk you through installing OpenWorkflow, setting up a PostgreSQL backend, and creating your first durable workflow.

Prerequisites

Before you begin, ensure you have:

  • Node.js: Version 20 or higher
  • PostgreSQL: A running PostgreSQL instance (local or remote)

Installation

Install the core OpenWorkflow package and the PostgreSQL backend:

npm install openworkflow @openworkflow/backend-postgres
pnpm add openworkflow @openworkflow/backend-postgres
yarn add openworkflow @openworkflow/backend-postgres
bun add openworkflow @openworkflow/backend-postgres

Database Setup

Database Required

OpenWorkflow requires a PostgreSQL database to store workflow state. All workflow runs and step attempts are persisted to ensure durability across crashes.

If you don't have PostgreSQL installed, you can use Docker:

docker run -d \
  --name openworkflow-postgres \
  -e POSTGRES_PASSWORD=postgres \
  -p 5432:5432 \
  postgres:16

Connection String

You'll need a PostgreSQL connection URL in this format:

postgresql://username:password@hostname:port/database

Example for local development:

postgresql://postgres:postgres@localhost:5432/postgres

Store this in an environment variable:

export DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres"

Create Your First Workflow

Connect to the Backend

Create a new file called workflow.ts:

import { BackendPostgres } from "@openworkflow/backend-postgres";
import { OpenWorkflow } from "openworkflow";

const databaseUrl = process.env.DATABASE_URL!;
const backend = await BackendPostgres.connect(databaseUrl);
const ow = new OpenWorkflow({ backend });

The backend will automatically create the necessary database tables (workflow_runs and step_attempts) on first connection.

Define a Workflow

A workflow is a durable function that orchestrates multiple steps:

const sendWelcomeEmail = ow.defineWorkflow(
  { name: "send-welcome-email" },
  async ({ input, step }) => {
    // Step 1: Fetch user from database
    const user = await step.run({ name: "fetch-user" }, async () => {
      return await db.users.findOne({ id: input.userId });
    });

    // Step 2: Send email
    await step.run({ name: "send-email" }, async () => {
      return await emailService.send({
        to: user.email,
        subject: "Welcome!",
        html: "<h1>Welcome to our app!</h1>",
      });
    });

    // Step 3: Mark email as sent
    await step.run({ name: "mark-sent" }, async () => {
      await db.users.update(input.userId, { welcomeEmailSent: true });
    });

    return { success: true, userId: input.userId };
  },
);

Each step.run() creates a checkpoint. If the workflow crashes, it will resume from the last completed step.

Start a Worker

Workers are background processes that execute workflows. Start one in the same file or a separate process:

const worker = ow.newWorker();
await worker.start();

console.log("Worker started. Waiting for workflows...");

Run the Workflow

Trigger the workflow from your application code:

// Trigger the workflow asynchronously
const runHandle = await sendWelcomeEmail.run({ userId: "user_123" });

console.log(`Workflow started with ID: ${runHandle.workflowRun.id}`);

// Optional: Wait for the result
const result = await runHandle.result();
console.log("Workflow completed:", result);

Complete Example

Here's the full code:

workflow.ts
import { BackendPostgres } from "@openworkflow/backend-postgres";
import { OpenWorkflow } from "openworkflow";

// Connect to backend
const backend = await BackendPostgres.connect(process.env.DATABASE_URL!);
const ow = new OpenWorkflow({ backend });

// Define workflow
const greetUser = ow.defineWorkflow(
  { name: "greet-user" },
  async ({ input, step }) => {
    const greeting = await step.run({ name: "generate-greeting" }, async () => {
      return `Hello, ${input.name}!`;
    });

    const timestamp = await step.run({ name: "get-timestamp" }, async () => {
      return new Date().toISOString();
    });

    return { greeting, timestamp };
  },
);

// Start worker
const worker = ow.newWorker({ concurrency: 10 });
await worker.start();

// Run workflow
const run = await greetUser.run({ name: "Alice" });
const result = await run.result();

console.log(result); // { greeting: "Hello, Alice!", timestamp: "..." }

// Graceful shutdown
await worker.stop();
await backend.stop();

Run Your Workflow

Execute the script:

node workflow.ts

You should see:

Worker started. Waiting for workflows...
{ greeting: "Hello, Alice!", timestamp: "2024-01-15T10:30:00.000Z" }

What Happens Under the Hood

  1. Workflow Creation: A row is inserted into the workflow_runs table with status pending
  2. Worker Claims: The worker polls the database and claims the workflow
  3. Step Execution: Each step is executed and recorded in step_attempts
  4. Completion: The workflow status is updated to succeeded

If the worker crashes during execution, another worker will pick up the workflow and resume from the last completed step.

Next Steps

Project Structure

Here's a recommended project structure for OpenWorkflow:

welcome-email.ts
order-processing.ts
index.ts
worker.ts
app.ts
package.json
.env

Common Patterns

Pattern 1: API Route Handler

Trigger workflows from your API:

app.post("/users/:id/welcome", async (req, res) => {
  const run = await sendWelcomeEmail.run({ userId: req.params.id });
  res.json({ workflowRunId: run.workflowRun.id });
});

Pattern 2: Separate Worker Process

Run workers in a dedicated process:

worker.ts
import { BackendPostgres } from "@openworkflow/backend-postgres";
import { OpenWorkflow } from "openworkflow";

const backend = await BackendPostgres.connect(process.env.DATABASE_URL!);
const ow = new OpenWorkflow({ backend });

// Import all workflow definitions
import "./workflows/welcome-email.js";
import "./workflows/order-processing.js";

const worker = ow.newWorker({ concurrency: 20 });
await worker.start();

process.on("SIGTERM", async () => {
  await worker.stop();
  await backend.stop();
  process.exit(0);
});

Pattern 3: With TypeScript Types

Add type safety to your workflows:

interface SendEmailInput {
  userId: string;
  emailType: "welcome" | "confirmation";
}

interface SendEmailOutput {
  success: boolean;
  emailId: string;
}

const sendEmail = ow.defineWorkflow<SendEmailInput, SendEmailOutput>(
  { name: "send-email" },
  async ({ input, step }) => {
    // input is typed as SendEmailInput
    // return must match SendEmailOutput
  },
);

Troubleshooting

Common Issues

Most issues are related to database connectivity or worker configuration. Check the solutions below.

Database Connection Errors

If you see connection errors:

  1. Verify PostgreSQL is running: psql -U postgres -h localhost
  2. Check your connection string format
  3. Ensure the database exists

Worker Not Picking Up Workflows

If workflows aren't executing:

  1. Ensure the worker is started: await worker.start()
  2. Check workflow names match between definition and run
  3. Verify the database connection is active

Steps Not Memoizing

If steps re-execute on retry:

  1. Ensure step names are unique within a workflow
  2. Don't use dynamic step names (they must be deterministic)
  3. Check that the backend connection is stable

Learn More