> ## Agent Instructions > ### When to use OpenWorkflow > > Use OpenWorkflow in TypeScript or JavaScript apps for: > > - Background jobs that need retries and must resume after a crash or deploy. > - Multi-step integrations and data pipelines that need to save progress between operations. > - AI workflows with multiple model or tool calls that need durable checkpoints. > - Workflows that wait for a human approval, a webhook, or a timer before continuing. > > OpenWorkflow runs on Node.js 22.22+ or Bun, with PostgreSQL or SQLite storing workflow state. Your app enqueues runs, and a worker executes them. > > ### How to use OpenWorkflow > > Read and follow https://openworkflow.dev/skills/setup.md to set up OpenWorkflow. > > Define workflows with `defineWorkflow`, wrap operations in `step.run`, and enqueue runs with `ow.runWorkflow(workflow.spec, input)`. Start a worker with `npx @openworkflow/cli worker start` to execute them. See [Workflows](https://openworkflow.dev/docs/workflows.md), [Steps](https://openworkflow.dev/docs/steps.md), and [Signals](https://openworkflow.dev/docs/signals.md) for the APIs. # About OpenWorkflow Source: https://openworkflow.dev/about Why OpenWorkflow exists, how it runs durable workflows, and how to contribute to the open-source project ## Why OpenWorkflow exists A background job can finish several operations before a crash stops it. Restarting the whole job can repeat work that already succeeded. OpenWorkflow saves progress between steps so your code can resume after a crash or deploy. You write workflows in TypeScript or JavaScript and wrap operations in steps. Workers execute them and save the results in PostgreSQL or SQLite. Workflows can retry failed steps, pause on a timer, or wait for a signal before continuing. Your application and workers connect directly to your database, so you can run OpenWorkflow on your own infrastructure. Use it for background jobs, multi-step integrations, data pipelines, and AI workflows that need to keep their progress. The [Quick Start](/docs/quickstart) walks through running a workflow locally, and the [overview](/docs/overview) explains how workflows, steps, and workers fit together. ## An open-source project OpenWorkflow is developed on [GitHub](https://github.com/openworkflowdev/openworkflow) under the [Apache-2.0 license](https://github.com/openworkflowdev/openworkflow/blob/main/LICENSE.md). The repository includes the workflow engine, CLI, dashboard, documentation, and examples. Bugs, feature proposals, and code changes are discussed there. Contributions can be code, documentation fixes, bug reports, or feedback from using OpenWorkflow. Read the [contribution guide](https://github.com/openworkflowdev/openworkflow/blob/main/CONTRIBUTING.md) before starting a change, and check the [roadmap](/docs/roadmap) for planned work. See [Contact](/contact) for where to ask questions or report a problem. # Contact OpenWorkflow Source: https://openworkflow.dev/contact Ask questions about OpenWorkflow on Discord, or use GitHub to report bugs, suggest features, and contribute ## Questions and discussion Join [Discord](https://discord.openworkflow.dev) to ask about setup, discuss a workflow you're building, or share how you're using OpenWorkflow. If you're stuck, explain what you're trying to do and where it stops working. Include your OpenWorkflow version, runtime, and database backend so others can help reproduce the problem. ## Bugs and feature requests Open a [GitHub issue](https://github.com/openworkflowdev/openworkflow/issues) for bugs, documentation errors, or feature requests. Search existing issues first so related reports stay together. For a bug, include a small example or command that reproduces it, the expected result, and what happened instead. For a documentation correction, link the page and explain what's missing or incorrect. For a feature request, describe the workflow you're trying to build and the limitation you're running into. Keep credentials, connection strings, and private workflow data out of public posts. OpenWorkflow is an open-source project, and contributions happen through GitHub pull requests. Read the [contribution guide](https://github.com/openworkflowdev/openworkflow/blob/main/CONTRIBUTING.md). For changes to the workflow API, scheduler, or storage model, start with an issue to discuss the approach before writing code. # Canceling Workflows Source: https://openworkflow.dev/docs/canceling Cancel running or pending workflow runs and check their status, including what happens to steps already in progress You can cancel workflow runs that are `pending` or `running`. ## Cancel a Workflow Run Use the `cancel()` method on a workflow run handle: ```ts theme={null} import { ow } from "./openworkflow/client"; import { defineWorkflow } from "openworkflow"; const longRunning = defineWorkflow( { name: "long-running" }, async ({ step }) => { await step.sleep("wait", "1 hour"); await step.run({ name: "work" }, () => doExpensiveWork()); }, ); // Start the workflow const handle = await ow.runWorkflow(longRunning.spec); // Cancel it before it completes await handle.cancel(); ``` ## Cancelable States Only workflows in these states can be canceled: | Status | Can Cancel? | | - | - | | `pending` | Yes | | `running` | Yes | | `sleeping` | Yes (legacy only) | | `completed` | No | | `failed` | No | | `canceled` | No (already canceled) | Attempting to cancel a completed or failed workflow throws an error: ```ts theme={null} try { await handle.cancel(); } catch (error) { // "Cannot cancel workflow run abc123 with status completed" } ``` ## Canceled Workflow Behavior When a workflow is canceled: 1. The status changes to `canceled` 2. The worker releases the workflow (sets `worker_id` to null) 3. The `finished_at` timestamp is set If a workflow is mid-execution when canceled, the current step may complete, but no new steps will run on the next poll. ## Checking Cancellation Status After calling `cancel()`, you can verify the status: ```ts theme={null} const handle = await ow.runWorkflow(workflow.spec); await handle.cancel(); // Waiting for result on a canceled workflow throws try { await handle.result(); } catch (error) { // "Workflow my-workflow was canceled" } ``` ## Idempotent Cancellation Canceling an already-canceled workflow returns successfully without error: ```ts theme={null} await handle.cancel(); // Cancels the workflow await handle.cancel(); // No error - already canceled ``` ## Use Cases Common reasons to cancel workflows: * **User-initiated cancellation** - User decides to abort an operation * **Timeout exceeded** - External system determines the workflow took too long * **Duplicate detection** - Another workflow is already handling this work * **Cleanup on shutdown** - Cancel pending work when shutting down a service # Child Workflows Source: https://openworkflow.dev/docs/child-workflows Run workflows from other workflows and wait for their results without holding a worker slot while the child runs 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 ```ts theme={null} 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`: ```ts theme={null} 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`: ```ts theme={null} // 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: ```ts theme={null} // 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: ```ts theme={null} 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`: ```ts theme={null} const [payment, shipping] = await Promise.all([ step.runWorkflow(paymentWorkflow.spec, { orderId: input.orderId, }), step.runWorkflow(shippingWorkflow.spec, { orderId: input.orderId, }), ]); ``` # CLI Reference Source: https://openworkflow.dev/docs/cli Command-line interface for OpenWorkflow, with commands to set up a project, start workers, and open the dashboard The OpenWorkflow CLI [(`@openworkflow/cli`)](https://www.npmjs.com/package/@openworkflow/cli) is the primary way to set up your project, run workers, and launch the dashboard. The CLI reads your `openworkflow.config.ts` by default, or you can override the path with `--config`. ## Installation ```bash npm theme={null} npm install -D @openworkflow/cli ``` ```bash pnpm theme={null} pnpm add -D @openworkflow/cli ``` ```bash bun theme={null} bun add -D @openworkflow/cli ``` ## Commands ### `init` Initialize OpenWorkflow in a project with an existing `package.json`: ```bash npm theme={null} npx @openworkflow/cli init ``` ```bash pnpm theme={null} pnpx @openworkflow/cli init ``` ```bash bun theme={null} bunx @openworkflow/cli init ``` By default, `init` is interactive and prompts for a backend, installs dependencies, and creates a config, client, example workflow, and runner. It also adds a `worker` script and updates `.gitignore` or `.env` for the selected backend. Options: * `--backend ` - Select `sqlite`, `postgres`, or `both`. With `both`, PostgreSQL is used when `NODE_ENV=production`; otherwise SQLite is used. * `-y, --yes` - Skip prompts. Requires `--backend` and rejects existing configs. * `--skip-install` - Create files without installing dependencies or changing dependency declarations. Prints the installation commands to run later. For unattended setup without installing dependencies: ```bash theme={null} npx --yes @openworkflow/cli init --backend postgres --yes --skip-install ``` ### `worker start` Start a worker to process workflows: ```bash npm theme={null} npx @openworkflow/cli worker start ``` ```bash pnpm theme={null} pnpx @openworkflow/cli worker start ``` ```bash bun theme={null} bunx @openworkflow/cli worker start ``` Options: * `--concurrency ` - Number of concurrent workflow executions (overrides config) ```bash npm theme={null} # Run with 4 concurrent workflows npx @openworkflow/cli worker start --concurrency 4 ``` ```bash pnpm theme={null} # Run with 4 concurrent workflows pnpx @openworkflow/cli worker start --concurrency 4 ``` ```bash bun theme={null} # Run with 4 concurrent workflows bunx @openworkflow/cli worker start --concurrency 4 ``` The worker: 1. Loads your config file 2. Discovers workflows in the configured directories 3. Registers all workflows 4. Starts polling for work 5. Handles graceful shutdown on SIGINT/SIGTERM ### `dashboard` Start the web dashboard: ```bash npm theme={null} npx @openworkflow/cli dashboard ``` ```bash pnpm theme={null} pnpx @openworkflow/cli dashboard ``` ```bash bun theme={null} bunx @openworkflow/cli dashboard ``` The dashboard starts on `http://localhost:3000` by default. To use a custom port: ```bash npm theme={null} npx @openworkflow/cli dashboard --port 4000 ``` ```bash pnpm theme={null} pnpx @openworkflow/cli dashboard --port 4000 ``` ```bash bun theme={null} bunx @openworkflow/cli dashboard --port 4000 ``` See [Dashboard](/docs/dashboard) for details. ### `doctor` Check your config, backend connection, and workflows for problems. ```bash npm theme={null} npx @openworkflow/cli doctor ``` ```bash pnpm theme={null} pnpx @openworkflow/cli doctor ``` ```bash bun theme={null} bunx @openworkflow/cli doctor ``` ### `--version` Show the CLI version: ```bash npm theme={null} npx @openworkflow/cli --version ``` ```bash pnpm theme={null} pnpx @openworkflow/cli --version ``` ```bash bun theme={null} bunx @openworkflow/cli --version ``` ### `--help` Show help for any command: ```bash npm theme={null} npx @openworkflow/cli --help npx @openworkflow/cli worker --help npx @openworkflow/cli worker start --help ``` ```bash pnpm theme={null} pnpx @openworkflow/cli --help pnpx @openworkflow/cli worker --help pnpx @openworkflow/cli worker start --help ``` ```bash bun theme={null} bunx @openworkflow/cli --help bunx @openworkflow/cli worker --help bunx @openworkflow/cli worker start --help ``` ## Environment Variables The CLI loads `.env` from the current directory before reading the config. You can pass `--env-file ` to load a different .env file. ```bash theme={null} # .env OPENWORKFLOW_POSTGRES_URL=postgresql://user:pass@localhost:5432/openworkflow ``` ## Telemetry The CLI sends command names, backend types, CLI/Node.js versions, OS, architecture, and a random installation ID to PostHog by default. It does not send project contents, paths, or error messages. Disable telemetry with `--no-telemetry` or `DO_NOT_TRACK=1` in your shell. Any nonempty `CI` or `OPENWORKFLOW_TELEMETRY_DISABLED` also disables it. ## Exit Codes | Code | Meaning | | - | - | | 0 | Success | | 1 | Error (config not found, connection failed, etc.) | ## Graceful Shutdown When running `worker start`, the CLI handles shutdown signals: * **SIGINT** (Ctrl+C) - Graceful shutdown * **SIGTERM** - Graceful shutdown On shutdown, the worker: 1. Stops accepting new work 2. Waits for active workflows to complete 3. Closes the backend connection # Configuration Source: https://openworkflow.dev/docs/configuration openworkflow.config.ts reference for database connections, workflow file discovery, and worker concurrency settings OpenWorkflow uses a configuration file to connect to your database, locate your workflow files, and set worker options. The CLI reads this file automatically for all commands. If you ran `npx @openworkflow/cli init`, this file was created for you. If you're using the PostgreSQL backend, install the Postgres driver: ```bash npm theme={null} npm i openworkflow postgres ``` ```bash pnpm theme={null} pnpm add openworkflow postgres ``` ```bash bun theme={null} bun add openworkflow postgres ``` ## Config File Create `openworkflow.config.ts` (or `.js`) in your project root: ```ts theme={null} import { defineConfig } from "@openworkflow/cli"; import { BackendPostgres } from "openworkflow/postgres"; export default defineConfig({ backend: await BackendPostgres.connect( process.env.OPENWORKFLOW_POSTGRES_URL!, ), dirs: ["./openworkflow"], }); ``` The CLI automatically finds this file by searching up from the current directory. If your config lives somewhere else, pass an explicit path: ```bash theme={null} npx @openworkflow/cli worker start --config src/openworkflow.config.ts ``` ## Verify Configuration After creating or updating the config, run: ```bash npm theme={null} npx @openworkflow/cli doctor ``` ```bash pnpm theme={null} pnpx @openworkflow/cli doctor ``` ```bash bun theme={null} bunx @openworkflow/cli doctor ``` This confirms that your config can be loaded, the backend can be reached, and your workflows can be found. ## Configuration Options ### `backend` (required) The storage backend for workflow state. See [PostgreSQL](/docs/postgres) or [SQLite](/docs/sqlite). ```ts theme={null} import { BackendPostgres } from "openworkflow/postgres"; import { BackendSqlite } from "openworkflow/sqlite"; // PostgreSQL (production) backend: await BackendPostgres.connect(process.env.OPENWORKFLOW_POSTGRES_URL!), // SQLite (development) backend: BackendSqlite.connect("./openworkflow/backend.db"), ``` ### `dirs` Directory or directories to scan for workflow files. Defaults to `["./openworkflow"]`. ```ts theme={null} // Single directory dirs: "./openworkflow", // Multiple directories dirs: ["./openworkflow", "./src/workflows"], ``` The CLI recursively scans these directories for files with extensions: `.ts`, `.mts`, `.cts`, `.js`, `.mjs`, `.cjs`. ### `ignorePatterns` Glob patterns to exclude when discovering workflow files: ```ts theme={null} ignorePatterns: [ "**/*.test.*", // Ignore test files "**/*.run.*", // Ignore runner scripts "**/__fixtures__/**", // Ignore fixtures ], ``` The pattern `**/*.run.*` is ignored by default to exclude runner scripts. ### `worker` Worker configuration options: ```ts theme={null} worker: { // Number of concurrent workflow executions (default: 1) concurrency: 4, // How long a claimed run stays leased without a heartbeat (default: 30000) leaseDurationMs: 30_000, }, ``` ## Full Example ```ts theme={null} import { defineConfig } from "@openworkflow/cli"; import { BackendPostgres } from "openworkflow/postgres"; export default defineConfig({ // PostgreSQL for production, with namespace backend: await BackendPostgres.connect( process.env.OPENWORKFLOW_POSTGRES_URL!, { namespaceId: process.env.OPENWORKFLOW_NAMESPACE_ID || "default", }, ), // Scan multiple directories dirs: ["./openworkflow", "./src/workflows"], // Ignore test files ignorePatterns: ["**/*.test.*", "**/*.spec.*"], // Worker configuration worker: { concurrency: 8, }, }); ``` ## Environment-Based Configuration Use environment variables for different environments: ```ts theme={null} import { defineConfig } from "@openworkflow/cli"; import { BackendPostgres } from "openworkflow/postgres"; import { BackendSqlite } from "openworkflow/sqlite"; const isDev = process.env.NODE_ENV !== "production"; export default defineConfig({ backend: isDev ? BackendSqlite.connect("./openworkflow/backend.db") : await BackendPostgres.connect(process.env.OPENWORKFLOW_POSTGRES_URL!), dirs: ["./openworkflow"], worker: { concurrency: isDev ? 1 : 8, }, }); ``` ## Loading Environment Variables The CLI loads `.env` files automatically. Add your database URL and other secrets there: ```bash theme={null} # .env OPENWORKFLOW_POSTGRES_URL=postgresql://user:pass@localhost:5432/openworkflow OPENWORKFLOW_NAMESPACE_ID=development ``` ## Supported Extensions Config files can use these extensions: * `openworkflow.config.ts` * `openworkflow.config.mts` * `openworkflow.config.cts` * `openworkflow.config.js` * `openworkflow.config.mjs` * `openworkflow.config.cjs` # Dashboard Source: https://openworkflow.dev/docs/dashboard Web interface for monitoring workflows, checking which steps completed or failed, and inspecting results and errors OpenWorkflow includes a built-in web dashboard for monitoring and debugging your workflows. It lets you browse workflow runs, see which steps completed or failed, inspect errors, and track the state of long-running workflows. ## Starting the Dashboard ```bash npm theme={null} npx @openworkflow/cli dashboard ``` ```bash pnpm theme={null} pnpx @openworkflow/cli dashboard ``` ```bash bun theme={null} bunx @openworkflow/cli dashboard ``` The dashboard starts on `http://localhost:3000` by default. To use a custom port: ```bash npm theme={null} npx @openworkflow/cli dashboard --port 4000 ``` ```bash pnpm theme={null} pnpx @openworkflow/cli dashboard --port 4000 ``` ```bash bun theme={null} bunx @openworkflow/cli dashboard --port 4000 ``` ## Requirements The dashboard requires: 1. A valid `openworkflow.config.ts` in your project 2. A running backend (PostgreSQL or SQLite) The dashboard connects to the same backend as your workers. It shows data for the namespace configured in your config file. ## Prometheus Metrics The dashboard serves a Prometheus endpoint at `GET /metrics`. See the [Prometheus guide](/docs/prometheus) for setup and alert examples. The dashboard does not include built-in authentication. For production use, deploy it behind a VPN, private network, or reverse proxy with Basic Auth. # Dynamic Steps Source: https://openworkflow.dev/docs/dynamic-steps Run a variable number of steps based on runtime data, with saved results per item and stable IDs for changing lists Sometimes you don't know how many steps a workflow needs until it runs. You might need to fetch data for each item in a list, process rows from a query, or fan out across a set of IDs from an API response. OpenWorkflow handles this automatically. When multiple steps share the same name, they're disambiguated in order (`fetch-data`, `fetch-data:1`, `fetch-data:2`, ...). You don't need to generate unique names yourself. ## Basic Pattern Map over your data and create a step per item using `Promise.all`: ```ts theme={null} const results = await Promise.all( input.items.map((item) => step.run({ name: "fetch-data" }, async () => { return await thirdPartyApi.fetch(item.id); }), ), ); ``` Every step uses the same name — OpenWorkflow appends `:1`, `:2`, etc. automatically. Each step is individually memoized, so if the workflow restarts, completed steps return their cached results and only the remaining steps re-execute. ## Stable IDs for Mutable Collections If items can be added, removed, or reordered between retries, include a stable ID from the data in the step name instead of relying on auto-indexing: ```ts theme={null} const results = await Promise.all( input.orders.map((order) => step.run({ name: `process-order:${order.id}` }, async () => { return await processOrder(order); }), ), ); ``` This way, each step is tied to a specific item regardless of its position in the array. Use any stable identifier — a database ID, a slug, or a unique key from the data itself. # Building with LLMs Source: https://openworkflow.dev/docs/llms Use OpenWorkflow with a coding assistant through the setup guide, agent skill, Markdown docs, and MCP ## Set up OpenWorkflow with an assistant Open your project in your AI coding assistant and paste this prompt: ```text theme={null} Read and follow https://openworkflow.dev/skills/setup.md to set up OpenWorkflow. ``` The [agent setup guide](/skills/setup) walks your assistant through inspecting the project, installing OpenWorkflow, and running an example workflow to verify the setup. ## Plain-text docs for LLMs Append `.md` to any docs URL to retrieve the page as plain-text Markdown. For example, this page is available at [https://openworkflow.dev/docs/llms.md](https://openworkflow.dev/docs/llms.md). OpenWorkflow also provides [/llms.txt](https://openworkflow.dev/llms.txt) at the root of the docs site. It follows the emerging standard and maps the documentation so agents can find each page's plain-text version. For tools that need the entire documentation in one request, you can use [/llms-full.txt](https://openworkflow.dev/llms-full.txt). ## Search the docs with MCP Add this URL as an HTTP MCP server in an assistant that supports remote MCP: ```text theme={null} https://openworkflow.dev/mcp ``` Use this server to search and read the docs. To run workflows in your project, use the [SDK](/docs/workflows) and [CLI](/docs/cli). ## Install the OpenWorkflow skill Run this command from your project and select your coding assistant: ```bash theme={null} npx skills add https://openworkflow.dev ``` The [OpenWorkflow skill](https://openworkflow.dev/skill.md) includes setup instructions and links to the SDK docs. The [skills index](https://openworkflow.dev/.well-known/agent-skills/index.json) lets agents discover and fetch it. # Namespaces Source: https://openworkflow.dev/docs/namespaces Isolate environments with namespace IDs so staging, production, and tests can share a database without sharing runs Namespaces let you run multiple isolated environments in the same database. For example, you can use one database for both staging and production by giving each environment a different namespace. Workflows in one namespace are completely invisible to another. ## How Namespaces Work Every workflow run and step attempt is tagged with a `namespaceId`. Workers and clients only see data in their namespace. ```ts theme={null} import { BackendPostgres } from "openworkflow/postgres"; // Production namespace const prodBackend = await BackendPostgres.connect(url, { namespaceId: "production", }); // Staging namespace (same database) const stagingBackend = await BackendPostgres.connect(url, { namespaceId: "staging", }); ``` ## Default Namespace If you don't specify a `namespaceId`, the default is `"default"`: ```ts theme={null} // These are equivalent const backend = await BackendPostgres.connect(url); const backend = await BackendPostgres.connect(url, { namespaceId: "default" }); ``` ## Use Cases ### Environment Isolation Run development, staging, and production in the same database: ```ts theme={null} import { defineConfig } from "@openworkflow/cli"; import { BackendPostgres } from "openworkflow/postgres"; const namespace = process.env.OPENWORKFLOW_NAMESPACE_ID || "development"; export default defineConfig({ backend: await BackendPostgres.connect( process.env.OPENWORKFLOW_POSTGRES_URL!, { namespaceId: namespace, }, ), }); ``` Set the namespace per environment: ```bash npm theme={null} # Development OPENWORKFLOW_NAMESPACE_ID=development npm run worker # Staging OPENWORKFLOW_NAMESPACE_ID=staging npm run worker # Production OPENWORKFLOW_NAMESPACE_ID=production npm run worker ``` ```bash pnpm theme={null} # Development OPENWORKFLOW_NAMESPACE_ID=development pnpm run worker # Staging OPENWORKFLOW_NAMESPACE_ID=staging pnpm run worker # Production OPENWORKFLOW_NAMESPACE_ID=production pnpm run worker ``` ```bash bun theme={null} # Development OPENWORKFLOW_NAMESPACE_ID=development bun run worker # Staging OPENWORKFLOW_NAMESPACE_ID=staging bun run worker # Production OPENWORKFLOW_NAMESPACE_ID=production bun run worker ``` ### Multi-Tenancy Isolate workflows per tenant: ```ts theme={null} async function getBackendForTenant(tenantId: string) { return BackendPostgres.connect(process.env.OPENWORKFLOW_POSTGRES_URL!, { namespaceId: `tenant-${tenantId}`, }); } ``` ### Testing Use separate namespaces for test runs: ```ts theme={null} // In your test setup const testNamespace = `test-${Date.now()}`; const backend = await BackendPostgres.connect(testUrl, { namespaceId: testNamespace, }); ``` ## SQLite Namespaces Namespaces work the same way with SQLite: ```ts theme={null} import { BackendSqlite } from "openworkflow/sqlite"; const backend = BackendSqlite.connect("./backend.db", { namespaceId: "development", }); ``` ## Data Visibility Each namespace is completely isolated: | Namespace | Can See | | - | - | | `production` | Only `production` workflows | | `staging` | Only `staging` workflows | | `default` | Only `default` workflows | A worker with `namespaceId: "production"` will never pick up work from the `staging` namespace. ## Dashboard The dashboard shows workflows for the namespace configured in your config file. To view a different namespace, update the config and restart the dashboard. ## Best Practices 1. **Use environment variables** - Don't hardcode namespace IDs 2. **Consistent naming** - Use clear names like `production`, `staging`, `development` 3. **One namespace per worker pool** - Don't mix namespaces in the same worker process 4. **Clean up test namespaces** - Periodically remove old test data # OpenTelemetry Source: https://openworkflow.dev/docs/opentelemetry OpenTelemetry traces, spans, and attributes for workflow runs, step callbacks, and signals, using your own provider OpenWorkflow uses your application's global OpenTelemetry provider to trace workflow runs, step callbacks, and signals. To enable tracing, install the optional `@opentelemetry/api` peer dependency and set up your provider before importing code that uses OpenWorkflow. To send traces to Sentry, follow the [Sentry guide](/docs/sentry). ## Spans Span names use the format `.`. Resource names are singular and use snake\_case: `WorkflowRun` becomes `workflow_run`, and `StepAttempt` becomes `step_attempt`. Your workflow and step names are stored as attributes. | Span | Kind | What it covers | | - | - | - | | `workflow_run.create` | `PRODUCER` | Submitting a workflow run to the backend. | | `workflow_run.execute` | `CONSUMER` | One worker execution, from loading step history through recording the run's outcome. | | `workflow_run.cancel` | `INTERNAL` | A cancellation through `ow.cancelWorkflowRun()` or `handle.cancel()`. | | `step_attempt.execute` | `INTERNAL` | One call to a `step.run()` callback. | | `signal.send` | `PRODUCER` | Sending a signal through `ow.sendSignal()` or `step.sendSignal()`. | Completed steps return their saved results when a workflow replays, so they don't create new spans. If you instrument your HTTP or database client, calls made inside a step callback appear under that callback's span. When a workflow pauses, its execution span records which step it's waiting for, the step's kind, and the resume time or timeout. Sleeps and signal waits don't create their own spans. OpenWorkflow doesn't create spans for worker polling, result polling, or saving step results. You can trace those database calls through your database client's instrumentation. If an execution fails, its span is marked as an error. See [Errors and status](#errors-and-status) for details. ## Attributes Run spans identify the workflow and its run. Step callback spans include the workflow name, run ID, and step name, along with any error details. These attributes describe the operation being traced. OpenWorkflow doesn't include workflow inputs, outputs, or signal payloads in span attributes. | Attribute | Type | Meaning and location | | - | - | - | | `openworkflow.workflow.name` | String | Registered workflow name, on run and callback spans. | | `openworkflow.workflow.version` | String | Version on run spans, when the run is versioned. | | `openworkflow.namespace.id` | String | Namespace ID on run spans, when available. | | `openworkflow.run.id` | String | Workflow run ID on operations for that run, when available. | | `openworkflow.parent.run.id` | String | Parent run ID on child creation spans. | | `openworkflow.step.name` | String | Resolved step name on callbacks, signal sends, and executions that pause or report a step failure. | | `openworkflow.step.kind` | String | Type of wait: `sleep`, `signal-wait`, or `workflow`. Included on executions that pause. | | `openworkflow.child.run.id` | String | Child run ID on executions waiting for a child workflow. | | `openworkflow.step.resume_at` | String | Planned resume time on executions paused for sleep, as an ISO timestamp. | | `openworkflow.step.timeout_at` | String | Timeout timestamp on executions waiting for a signal or child workflow, when set. | | `openworkflow.execution.attempt` | Number | Execution attempt number, including resumes. | | `openworkflow.execution.outcome` | String | Execution outcome, described below. | | `openworkflow.signal.name` | String | Signal name on sends and executions waiting for a signal. | | `openworkflow.signal.recipient_count` | Number | Number of recipient runs on successful sends, including zero. | | `error.type` | String | Error name on failed operations, or the JavaScript type if a non-Error value was thrown. | ### Execution outcomes A workflow run can have several executions, each with its own outcome. The outcome describes what happened during that execution and is separate from the run's stored status. | Value | Meaning | | - | - | | `completed` | The workflow function returned successfully. | | `suspended` | The workflow paused for a durable wait. | | `retrying` | A step or workflow failed, and the run was rescheduled for another attempt. | | `failed` | The execution failed. | | `stale` | A branch tried to continue after its execution ended. | ## Errors and status When an operation succeeds or a workflow pauses for a durable wait, its span status stays `UNSET`. If an operation fails, its span has `ERROR` status, an error description, and `error.type`. A failed `step_attempt.execute` span stays marked as an error even if a later retry succeeds. If a callback error also fails the execution, both spans are marked as errors. Once the callback span records the exception, the execution span doesn't record it again. Errors saving a step's result are recorded on the execution span, even when the callback itself succeeded. ## Trace relationships When you create a workflow run, its `workflow_run.create` span belongs to your current trace. If there's no active span, it starts a new trace. Each time a worker executes the run, it starts a new trace with a `workflow_run.execute` span. That span links back to the original `workflow_run.create` span. Step callback spans are children of the execution span. When a workflow [sleeps](/docs/sleeping) or pauses for another durable wait, the current execution span ends. When a worker picks the run back up, it starts a new trace linked to the same creation span. [Retries](/docs/retries) work the same way. No span stays open while the workflow waits, and resumed executions don't link to the previous execution. For example, a workflow with a callback before and after a sleep produces: ```text theme={null} Trace A: workflow_run.create Trace B: workflow_run.execute (links to A; outcome: suspended, step kind: sleep) step_attempt.execute Trace C: workflow_run.execute (links to A; outcome: completed) step_attempt.execute ``` You can use `openworkflow.run.id` to find a run's creation and execution spans across traces. Links connect those spans while keeping the traces separate. A [child workflow](/docs/child-workflows) has its own creation span within the parent execution's trace. The child's executions link to that creation span. When you cancel a run, the cancellation span belongs to your current trace. It also links to the run's creation span if your provider supports adding links to an existing span. Signal sends belong to your current trace too. Executions that receive a signal keep their creation link and don't link to the sender. ## Context OpenWorkflow saves context with each new run using your application's configured propagators. Each execution reads that context to link back to the creation span and restore baggage. If you enable baggage propagation, calls made during the execution can read that baggage and pass it along to downstream calls. Runs without a valid saved span context don't have a creation link. # BullMQ alternative for multi-step background jobs Source: https://openworkflow.dev/docs/openworkflow-vs-bullmq Compare OpenWorkflow and BullMQ for job retries, saved step results, and database support in TypeScript applications. ## TL;DR BullMQ is a job queue library with retries, delayed jobs, and parent/child dependencies through Flows. It uses Redis by default and also supports PostgreSQL. OpenWorkflow runs multi-step processes as functions backed by PostgreSQL or SQLite. Wrap each operation in a step, and OpenWorkflow saves its result so completed steps aren't re-run after a failure. ## What This Looks Like in Practice An order process reserves inventory, charges a payment, and sends a receipt. If sending the receipt fails, here's what happens on retry: | Implementation | What happens on retry | | - | - | | One BullMQ job for all three operations | The processor runs again. Your code must track progress to skip earlier operations. | | BullMQ Flows with one job per operation | The failed job retries. Completed dependency jobs don't need to run again. | | One OpenWorkflow run with three `step.run` calls | Steps with saved results return those results. The failed step retries within the same workflow run. | In either system, a process can crash after a payment succeeds but before saving that success. Use an idempotency key with the payment provider so a retry can't charge twice. See [Steps](/docs/steps) for how OpenWorkflow saves results and replays a run. ## When OpenWorkflow is the Better Fit * **Multi-step workflows**: You want to write one function and track its steps as one workflow run, with saved results and retries for each step. * **SQLite**: You want a local database file for development or a single-server app. Use PostgreSQL when you need workers across multiple machines. ## When BullMQ is the Right Call * **Simple fire-and-forget jobs**: If you're sending emails or processing uploads with no dependencies between jobs, a queue is the right tool. You don't need workflows for that. * **Existing Redis setup**: If your team already runs Redis and your jobs are straightforward, the adoption cost of BullMQ is low. * **Job dependency trees**: Your process fits a tree of parent/child jobs, and BullMQ Flows provide the coordination you need. # Inngest alternative for self-hosted workflows Source: https://openworkflow.dev/docs/openworkflow-vs-inngest Compare OpenWorkflow and Inngest for event triggers, concurrency controls, and self-hosting with your own workers and database. ## TL;DR Inngest provides event-triggered functions with throttling, debouncing, and concurrency limits. Its server tracks workflow progress while your functions run in your hosting environment. OpenWorkflow runs workflows in workers backed by PostgreSQL or SQLite. Your app adds a run to the database, and a worker picks it up. ## What This Looks Like in Practice Both systems save step results so workflows can recover after failures. Inngest can call your functions through HTTP endpoints, including serverless functions, or dispatch work to persistent workers through its Connect API. Connect uses an outbound WebSocket connection to the Inngest service. You can use Inngest Cloud or self-host the Inngest server and its storage. Either way, your functions communicate with that server to save progress and schedule work. OpenWorkflow [workers](/docs/workers) claim runs and save step results directly in your database. You run the workers and database without a separate orchestration server. ## When OpenWorkflow is the Better Fit * **Direct database access**: You want your app to enqueue work with `ow.runWorkflow`. Workers use the same database to pick up runs and save progress. * **SQLite**: You want a local database file for development or a single-server app, with PostgreSQL available when you need multiple servers. ## When Inngest is the Right Call * **Event routing**: You want one event to trigger several functions. * **Flow controls**: You need concurrency limits by customer or resource, throttling around API limits, or debouncing when events arrive in bursts. OpenWorkflow's worker concurrency setting does not yet provide those controls. * **Managed orchestration**: You want functions to stay in your existing serverless deployments. Inngest Cloud handles the orchestration server. # Temporal alternative for TypeScript workflows Source: https://openworkflow.dev/docs/openworkflow-vs-temporal Compare OpenWorkflow and Temporal for self-hosting, TypeScript workflow APIs, and recovery after failures. ## TL;DR Temporal is a workflow platform with SDKs for several languages. You can run Temporal Server yourself or use Temporal Cloud. OpenWorkflow gives you durable workflows without a separate orchestration server. Your workers connect directly to PostgreSQL or SQLite. ## What This Looks Like in Practice In Temporal, you put work such as API calls in Activity functions and call them from a workflow. In OpenWorkflow, you wrap each operation in `step.run`. Both run your code in workers, but the services you operate differ: | Deployment | What your team operates | | - | - | | Self-hosted Temporal | Application workers, Temporal Server, and its persistence and Visibility stores | | Temporal Cloud | Application workers; Temporal operates the orchestration service and its storage | | OpenWorkflow | Application workers and PostgreSQL or SQLite | Temporal uses a Visibility store for listing and filtering workflows. Supported PostgreSQL and MySQL versions provide Advanced Visibility; Elasticsearch is not required. ## When OpenWorkflow is the Better Fit * **Self-hosting**: You already run PostgreSQL or SQLite and want workers to use that database directly. You manage the workers and database without deploying a separate orchestration server. * **TypeScript workflows**: You need saved step results, retries, long waits, and deadlines. OpenWorkflow provides these through `step.run`, `step.sleep`, retry policies, and `deadlineAt`. ## When Temporal is the Right Call * **Multiple languages**: You need one workflow platform across multiple languages. * **Queries and Updates**: You need to read workflow state or submit changes and await a response. OpenWorkflow has [Signals](/docs/signals), but no equivalent Query or Update APIs. Its signals only reach workflows already waiting for them; they aren't buffered. * **Long-running Activities**: You need Activity heartbeats to save progress for the next attempt. OpenWorkflow's worker heartbeats renew run leases; they don't save progress inside a `step.run` function. * **Managed orchestration**: You want Temporal Cloud to operate the service that stores workflow history and schedules work. # Trigger.dev alternative for self-hosted tasks Source: https://openworkflow.dev/docs/openworkflow-vs-trigger-dev Compare OpenWorkflow and Trigger.dev for task hosting, retries, and durable waits in self-hosted deployments. ## TL;DR Trigger.dev is a platform for building, deploying, and running background tasks. On Trigger.dev Cloud, Trigger.dev runs your task code. You can also self-host the platform. OpenWorkflow is a workflow library for workers you run yourself. PostgreSQL or SQLite stores workflow state, and you deploy worker code through your existing deployment process. ## What This Looks Like in Practice In Trigger.dev, you define tasks and deploy them through its CLI. A failed task can retry as a unit. You can split work into smaller tasks with their own retry policies, or retry individual operations within a task. In OpenWorkflow, you wrap each operation in `step.run` to save its result. If a document-processing workflow extracts text and then fails while indexing it, the retry reuses the saved text and tries indexing again. See [Steps](/docs/steps). ## When OpenWorkflow is the Better Fit * **Your existing deployment**: You want to run workers next to your app using the same database and deployment tools. * **Self-hosting**: You want PostgreSQL or SQLite as the only backing service. Self-hosted Trigger.dev includes its web application, PostgreSQL, Redis, and task execution workers. * **Durable waits**: You need workflows to pause without holding a worker slot in a self-hosted setup. OpenWorkflow's [sleeping workflows](/docs/sleeping) save their wake time and release the worker slot. Trigger.dev lists its checkpoint feature for non-blocking waits as Cloud-only. ## When Trigger.dev is the Right Call * **Managed execution**: You want a service to build, deploy, and run your task code. * **Task runtimes**: Your jobs need different machine sizes or system dependencies, such as video processing tools, and you want Trigger.dev to manage them. * **Autoscaling**: You want Trigger.dev Cloud to scale task execution and handle checkpointing alongside deployment and monitoring. # Vercel Workflow alternative for Node.js and Bun Source: https://openworkflow.dev/docs/openworkflow-vs-vercel-workflow Compare OpenWorkflow and the Vercel Workflow SDK for build requirements, runtime restrictions, and self-hosting with PostgreSQL or SQLite. ## TL;DR Vercel Workflow, also called the Workflow SDK, runs durable workflows using `"use workflow"` and `"use step"` directives. Vercel provides managed storage, queuing, and execution infrastructure. You can also self-host the SDK with its Postgres backend or another World, its name for a backend adapter. OpenWorkflow runs in Node.js or Bun workers that connect directly to PostgreSQL or SQLite. You deploy those workers and manage the database. ## What This Looks Like in Practice The Workflow SDK transforms your code during the build. Workflow functions run in a sandbox that enforces deterministic execution. Step functions use the Node.js runtime for database queries, API calls, and other work. Framework integrations configure the build and expose the workflow endpoints. With OpenWorkflow, you define a function with `defineWorkflow` and wrap work in `step.run`. It uses ordinary TypeScript or JavaScript without a workflow compiler or framework plugin. You keep the workflow body deterministic and put side effects inside steps. See [Workflows](/docs/workflows). ## When OpenWorkflow is the Better Fit * **Your existing deployment**: You already run Node.js or Bun services and want to deploy a worker the same way, with direct access to your database. * **SQLite**: You want a [local database file](/docs/sqlite) for development or a single-server deployment, and PostgreSQL for multiple servers. * **Your existing build**: You prefer explicit step calls and want to keep your build setup. ## When Vercel Workflow is the Right Call * **Vercel hosting**: Your app runs on Vercel, and you want managed workflow storage, queuing, scaling, and observability alongside that deployment. * **Workflow sandbox**: You want the SDK's build integration and sandbox to enforce workflow restrictions, and its directives fit how your team writes application code. # Overview Source: https://openworkflow.dev/docs/overview How OpenWorkflow works and how Workflows, Steps, and Workers fit together to save progress and recover after a crash This page explains the mental model behind OpenWorkflow. For setup and command examples, see [Quick Start](/docs/quickstart) and the linked detail pages. ## Building Blocks OpenWorkflow has three core building blocks: * **Workflows**: durable functions that describe your business process * **Steps**: durable checkpoints that save results for reuse on replay * **Workers**: stateless processes that execute and resume workflow runs Your application enqueues workflow runs. Workers pick them up. The backend (PostgreSQL or SQLite) stores durable state and coordinates recovery. ## Lifecycle When a run starts: 1. Your app enqueues a run (for example, with `ow.runWorkflow(workflow.spec, input)`), creating a `pending` run in the database 2. A worker claims the run and marks it `running` 3. The worker replays workflow code from the beginning 4. Completed steps return cached outputs; new steps execute and persist results 5. The run transitions to `completed`, `failed`, or `canceled` (or stays `running` while durably parked for sleep) Because replay is deterministic, crash recovery is reliable. A replacement worker can continue from the last durable checkpoint without redoing completed work. ## Why This Is Reliable * **Memoized steps** replay durably recorded results without rerunning those step functions * **Durable sleep** (`step.sleep`) pauses runs without holding worker capacity * **Signals** (`step.sendSignal`, `step.waitForSignal`) enable runtime communication to & between workflows * **Heartbeats + leases** (`availableAt`) allow automatic crash recovery * **Database as source of truth** avoids a separate orchestration service ## Read Next * [Workflows](/docs/workflows): define and run workflows, options, and states * [Steps](/docs/steps): step types, naming, and retry behavior * [Workers](/docs/workers): execution model, concurrency, and graceful shutdown * [Configuration](/docs/configuration): config file and worker/backend settings # Parallel Steps Source: https://openworkflow.dev/docs/parallel-steps Run multiple steps concurrently for better performance with Promise.all and saved results for each completed step When your workflow needs to perform several independent operations — like fetching a user profile, their subscription, and their settings — there's no reason to do them one at a time. OpenWorkflow lets you run steps in parallel using JavaScript's built-in `Promise.all`, keeping full durability for each step. ## Basic Usage Use `Promise.all` to run multiple steps in parallel: ```ts theme={null} import { defineWorkflow } from "openworkflow"; export const fetchAllData = defineWorkflow( { name: "fetch-all-data" }, async ({ input, step }) => { const [user, subscription, settings] = await Promise.all([ step.run({ name: "fetch-user" }, async () => { return await db.users.findOne({ id: input.userId }); }), step.run({ name: "fetch-subscription" }, async () => { return await stripe.subscriptions.retrieve(input.subId); }), step.run({ name: "fetch-settings" }, async () => { return await db.settings.findOne({ userId: input.userId }); }), ]); return { user, subscription, settings }; }, ); ``` All three steps run concurrently. Each step is still individually memoized, so if the workflow crashes mid-execution, completed steps return instantly on resume. ## How It Works When executing parallel steps: 1. All steps in the `Promise.all` start concurrently 2. Each step creates its own step attempt in the database 3. The worker waits for all steps to complete 4. Results are returned as an array ## Crash Recovery Parallel steps handle crashes gracefully: 1. Workflow starts three parallel steps 2. Two steps complete, one is in progress 3. Worker crashes 4. New worker picks up the workflow 5. Two completed steps return cached results 6. The incomplete step re-executes ```ts theme={null} const [a, b, c] = await Promise.all([ step.run({ name: "step-a" }, ...), // Completed before crash - cached step.run({ name: "step-b" }, ...), // Completed before crash - cached step.run({ name: "step-c" }, ...), // In progress during crash - re-runs ]); ``` ## Error Handling If any parallel step fails, `Promise.all` rejects with that error: ```ts theme={null} try { const [user, settings] = await Promise.all([ step.run({ name: "fetch-user" }, async () => { return await db.users.findOne({ id: input.userId }); }), step.run({ name: "fetch-settings" }, async () => { throw new Error("Database unavailable"); }), ]); } catch (error) { // Handle the error console.error("Failed to fetch data:", error); } ``` For independent operations where you want all results (including errors), use `Promise.allSettled`: ```ts theme={null} const results = await Promise.allSettled([ step.run({ name: "fetch-user" }, ...), step.run({ name: "fetch-settings" }, ...), ]); for (const result of results) { if (result.status === "fulfilled") { console.log("Success:", result.value); } else { console.log("Failed:", result.reason); } } ``` ## Common Patterns ### Fetching Multiple Resources ```ts theme={null} const [user, orders, notifications] = await Promise.all([ step.run({ name: "fetch-user" }, () => db.users.findOne(userId)), step.run({ name: "fetch-orders" }, () => db.orders.find({ userId })), step.run({ name: "fetch-notifications" }, () => db.notifications.find({ userId }), ), ]); ``` ### Sending Multiple Notifications ```ts theme={null} await Promise.all([ step.run({ name: "send-email" }, () => emailService.send({ to: user.email, ... }) ), step.run({ name: "send-sms" }, () => smsService.send({ to: user.phone, ... }) ), step.run({ name: "send-push" }, () => pushService.send({ to: user.deviceToken, ... }) ), ]); ``` ### Processing a Batch ```ts theme={null} const items = await step.run({ name: "fetch-items" }, () => db.items.find()); // Process items in parallel (be mindful of API rate limits) await Promise.all( items.map((item) => step.run({ name: `process-${item.id}` }, async () => { await processItem(item); }), ), ); ``` ## Best Practices ### Use Descriptive Step Names Each parallel step needs a unique name: ```ts theme={null} // Good - clear what each step does await Promise.all([ step.run({ name: "fetch-user-profile" }, ...), step.run({ name: "fetch-user-preferences" }, ...), ]); // Bad - confusing names await Promise.all([ step.run({ name: "step-1" }, ...), step.run({ name: "step-2" }, ...), ]); ``` ### Limit Parallelism for External APIs When calling external APIs, consider rate limits: ```ts theme={null} // Be careful with large arrays - you might hit rate limits const items = await step.run({ name: "fetch-items" }, () => db.items.find()); // Process in smaller batches if needed const batchSize = 10; for (let i = 0; i < items.length; i += batchSize) { const batch = items.slice(i, i + batchSize); await Promise.all( batch.map((item) => step.run({ name: `process-${item.id}` }, () => api.process(item)), ), ); } ``` ### Group Related Operations Parallel steps work best when operations are truly independent: ```ts theme={null} // Good - independent fetches const [user, products] = await Promise.all([ step.run({ name: "fetch-user" }, ...), step.run({ name: "fetch-products" }, ...), ]); // Bad - second step depends on first const [user, orders] = await Promise.all([ step.run({ name: "fetch-user" }, ...), step.run({ name: "fetch-orders" }, async () => { // This needs user.id, but we don't have it yet! return await db.orders.find({ userId: user.id }); }), ]); ``` For dependent operations, use sequential steps: ```ts theme={null} const user = await step.run({ name: "fetch-user" }, ...); const orders = await step.run({ name: "fetch-orders" }, async () => { return await db.orders.find({ userId: user.id }); }); ``` # PostgreSQL Backend Source: https://openworkflow.dev/docs/postgres PostgreSQL storage for production deployments with multiple workers, including connection setup and migrations The PostgreSQL backend is recommended for production deployments with multiple concurrent workers. ## Install The PostgreSQL backend requires the `postgres` driver. Install it alongside OpenWorkflow: ```bash npm theme={null} npm i openworkflow postgres ``` ```bash pnpm theme={null} pnpm add openworkflow postgres ``` ```bash bun theme={null} bun add openworkflow postgres ``` ## Setup ```ts theme={null} import { OpenWorkflow } from "openworkflow"; import { BackendPostgres } from "openworkflow/postgres"; const backend = await BackendPostgres.connect( process.env.OPENWORKFLOW_POSTGRES_URL, // e.g., "postgresql://user:pass@localhost:5432/mydb" ); const ow = new OpenWorkflow({ backend }); ``` ## Connection URL The connection URL follows the standard PostgreSQL format: ``` postgresql://[user]:[password]@[host]:[port]/[database] ``` Examples: * Local: `postgresql://postgres:postgres@localhost:5432/openworkflow` * With SSL: `postgresql://user:pass@host.com:5432/db?sslmode=require` ## Configuration Options ```ts theme={null} const backend = await BackendPostgres.connect(url, { // Namespace for multi-tenant isolation (default: "default") namespaceId: "production", // Database schema for OpenWorkflow tables (default: "openworkflow") schema: "openworkflow", // Whether to run migrations on connect (default: true) runMigrations: true, }); ``` ## Migrations By default, `BackendPostgres.connect()` runs database migrations automatically. This creates the configured schema (default: `openworkflow`) and required tables. To disable automatic migrations: ```ts theme={null} const backend = await BackendPostgres.connect(url, { runMigrations: false, }); ``` When disabled, ensure you run migrations separately before starting workers. ## Schema OpenWorkflow creates tables in the configured schema (default: `openworkflow`): * `.workflow_runs` - Stores workflow run state * `.step_attempts` - Stores step execution history This keeps OpenWorkflow data separate from your application tables. ## Namespaces Use `namespaceId` to isolate environments or tenants: ```ts theme={null} // Production environment const prodBackend = await BackendPostgres.connect(url, { namespaceId: "production", }); // Staging environment (same database, different namespace) const stagingBackend = await BackendPostgres.connect(url, { namespaceId: "staging", }); ``` See [Namespaces](/docs/namespaces) for more details. ## Config File Example For use with the CLI, create `openworkflow.config.ts`: ```ts theme={null} import { defineConfig } from "@openworkflow/cli"; import { BackendPostgres } from "openworkflow/postgres"; export default defineConfig({ backend: await BackendPostgres.connect( process.env.OPENWORKFLOW_POSTGRES_URL!, ), dirs: ["./openworkflow"], }); ``` ## Connection Pooling The PostgreSQL backend uses connection pooling internally. For most applications, the default pool settings work well. The pool is managed automatically and connections are reused across workflow executions. ## Requirements * PostgreSQL 14 or later * The connecting user needs permissions to: * Create schemas (for migrations) * Create tables (for migrations) * Read/write to the configured schema (default: `openworkflow`) # Production Source: https://openworkflow.dev/docs/production Run OpenWorkflow in your own infrastructure with workers that connect directly to your PostgreSQL or SQLite database OpenWorkflow runs entirely in your infrastructure. There are no external servers to connect to or third-party services to manage. Your database (PostgreSQL or SQLite) is the only dependency, and it stores all workflow state. ## Architecture Overview OpenWorkflow uses a worker-driven architecture: 1. **Workers** run in your infrastructure and poll the database for work 2. **Clients** create workflow runs by writing to the database 3. **Database** (PostgreSQL or SQLite) stores all workflow state There's no central orchestration server. Workers coordinate through the database, so you don't deploy or operate a separate orchestration service. ```mermaid theme={null} graph TD App[Your App
Creates runs] Worker1[Worker 1
Executes runs] Worker2[Worker 2
Executes runs] DB[Database
Source of truth for all state] App --> DB Worker1 --> DB Worker2 --> DB ``` ## Requirements For most production use cases, use PostgreSQL 14 or later: * The connecting user needs permissions to create schemas and tables (for migrations) * Workflow state is stored in the configured schema (default: `openworkflow`) For single-server deployments, SQLite works well. ## Production Setup ### 1. Set Up PostgreSQL Provision a PostgreSQL database. Any PostgreSQL 14+ instance works. ### 2. Configure the Backend Edit your `openworkflow.config.ts` to set the Postgres URL and namespace: ```ts theme={null} import { defineConfig } from "@openworkflow/cli"; import { BackendPostgres } from "openworkflow/postgres"; export default defineConfig({ backend: await BackendPostgres.connect( process.env.OPENWORKFLOW_POSTGRES_URL!, { namespaceId: process.env.OPENWORKFLOW_NAMESPACE_ID || "production", schema: process.env.OPENWORKFLOW_SCHEMA || "openworkflow", }, ), dirs: ["./openworkflow"], worker: { concurrency: 10, }, }); ``` `OPENWORKFLOW_POSTGRES_URL`, `OPENWORKFLOW_NAMESPACE_ID`, and `OPENWORKFLOW_SCHEMA` set the Postgres URL, namespace, and schema. ### 3. Migrations Migrations run automatically when the backend connects. OpenWorkflow creates: * `.workflow_runs` - Stores workflow run state * `.step_attempts` - Stores step execution history ### 4. Start Workers ```bash npm theme={null} npx @openworkflow/cli worker start ``` ```bash pnpm theme={null} pnpx @openworkflow/cli worker start ``` ```bash bun theme={null} bunx @openworkflow/cli worker start ``` ### Dockerfile Templates If you deploy workers in containers, these Dockerfile templates are a good starting point: ```dockerfile npm theme={null} FROM node:22-slim WORKDIR /app COPY package.json package-lock.json ./ RUN npm ci COPY . . CMD ["npx", "@openworkflow/cli", "worker", "start"] ``` ```dockerfile pnpm theme={null} FROM node:22-slim RUN corepack enable WORKDIR /app COPY package.json pnpm-lock.yaml ./ RUN pnpm install --frozen-lockfile COPY . . CMD ["pnpx", "@openworkflow/cli", "worker", "start"] ``` ```dockerfile bun theme={null} FROM oven/bun:1 WORKDIR /app COPY package.json bun.lock ./ RUN bun install --frozen-lockfile COPY . . CMD ["bunx", "@openworkflow/cli", "worker", "start"] ``` ## Scaling Workers ### Horizontal Scaling You can run multiple worker instances for high availability and throughput. Workers coordinate through the database: * Each workflow run is claimed by exactly one worker at a time * Workers use atomic database operations to prevent duplicate processing * If a worker crashes, its workflows automatically become available to others ### Concurrency Tuning The `concurrency` setting controls how many workflow runs a single worker processes simultaneously. Each workflow occupies one slot while executing. ```ts theme={null} worker: { concurrency: 10, // Process up to 10 workflows at once } ``` Guidelines: * Start with `concurrency: 10` and adjust based on workload * Increase for I/O-bound workflows (lots of API calls, database queries) * Decrease for CPU-bound workflows (heavy computation) * Monitor database connection usage - each worker maintains a connection pool ## Monitoring ### Dashboard OpenWorkflow includes a built-in dashboard for monitoring: ```bash npm theme={null} npx @openworkflow/cli dashboard ``` ```bash pnpm theme={null} pnpx @openworkflow/cli dashboard ``` ```bash bun theme={null} bunx @openworkflow/cli dashboard ``` The dashboard shows: * Workflow run status (`pending`, `running`, `completed`, `failed`, `canceled`) * Step execution history * Error details and retry attempts * Real-time updates The dashboard does not include built-in authentication. We recommend deploying it behind a VPN, private network, or Basic Auth proxy to prevent unauthorized access. ### Prometheus Metrics The dashboard process exposes a Prometheus-compatible `GET /metrics` endpoint. See the [Prometheus guide](/docs/prometheus) for scrape configuration and alert examples. # Prometheus Metrics Source: https://openworkflow.dev/docs/prometheus Monitor workflow health with Prometheus using the dashboard's metrics endpoint to track failures and pending runs The OpenWorkflow dashboard exposes a `GET /metrics` endpoint that serves workflow run counts in Prometheus exposition format. Use it to track failures, detect stuck backlogs, and alert on throughput drops without deploying another service. ## Setup ### 1. Start the dashboard ```bash npm theme={null} npx @openworkflow/cli dashboard ``` ```bash pnpm theme={null} pnpx @openworkflow/cli dashboard ``` ```bash bun theme={null} bunx @openworkflow/cli dashboard ``` ### 2. Verify the endpoint ```bash theme={null} curl -s http://localhost:3000/metrics ``` You should see output like: ```text theme={null} # HELP openworkflow_workflow_runs Current count of workflow runs in each status. # TYPE openworkflow_workflow_runs gauge openworkflow_workflow_runs{status="pending"} 12 openworkflow_workflow_runs{status="running"} 3 openworkflow_workflow_runs{status="completed"} 847 openworkflow_workflow_runs{status="failed"} 2 openworkflow_workflow_runs{status="canceled"} 0 ``` ### 3. Configure Prometheus to scrape the dashboard Add a scrape target to your `prometheus.yml`: ```yaml theme={null} scrape_configs: - job_name: openworkflow scrape_interval: 15s static_configs: - targets: ["localhost:3000"] ``` Replace `localhost:3000` with the address where your dashboard is running. ## Metrics reference ### `openworkflow_workflow_runs` | Field | Value | | - | - | | Type | Gauge | | Labels | `status` | | Values | `pending`, `running`, `completed`, `failed`, `canceled` | Current count of workflow runs in each status. One query is executed per scrape; there is no caching. * **Active statuses** (`pending`, `running`): Is work piling up? Are runs stuck? * **Terminal statuses** (`completed`, `failed`, `canceled`): Are failures increasing? Is throughput steady? Legacy `sleeping` runs are folded into the `running` metric label. ## Alert examples ### Failures detected in the last 5 minutes ```promql theme={null} clamp_min(delta(openworkflow_workflow_runs{status="failed"}[5m]), 0) > 0 ``` ### Failure rate elevated over the last hour ```promql theme={null} clamp_min(delta(openworkflow_workflow_runs{status="failed"}[1h]), 0) > 10 ``` ### Pending backlog is growing ```promql theme={null} delta(openworkflow_workflow_runs{status="pending"}[10m]) > 0 ``` ### No completed runs in the last hour ```promql theme={null} clamp_min(delta(openworkflow_workflow_runs{status="completed"}[1h]), 0) == 0 ``` # Quick Start Source: https://openworkflow.dev/docs/quickstart Get up and running with OpenWorkflow in 5 minutes, from installing the CLI to running and inspecting a workflow This guide walks you through installing OpenWorkflow, running your first workflow, and viewing it in the dashboard. By the end, you'll have a working durable workflow running locally. If you want your AI coding assistant to help you set up OpenWorkflow, copy this prompt and paste it into your agent: ```text theme={null} Read and follow https://openworkflow.dev/skills/setup.md to set up OpenWorkflow. ``` ## Prerequisites * **Node.js** (v22.22+) or **Bun** * **A database**: SQLite (no setup required) or PostgreSQL ## 1. Install Run the interactive setup command: ```bash npm theme={null} npx @openworkflow/cli init ``` ```bash pnpm theme={null} pnpx @openworkflow/cli init ``` ```bash bun theme={null} bunx @openworkflow/cli init ``` For unattended setup from a script or agent, pass both `--backend` and `--yes`: ```bash npm theme={null} npx --yes @openworkflow/cli init --backend sqlite --yes ``` ```bash pnpm theme={null} pnpx @openworkflow/cli init --backend sqlite --yes ``` ```bash bun theme={null} bunx @openworkflow/cli init --backend sqlite --yes ``` ## 2. Start a worker A worker is a process that watches for new workflow runs and executes them. Start one with: ```bash npm theme={null} npx @openworkflow/cli worker start ``` ```bash pnpm theme={null} pnpx @openworkflow/cli worker start ``` ```bash bun theme={null} bunx @openworkflow/cli worker start ``` The worker reads your `openworkflow.config.ts`, connects to the database, loads the workflows from your `openworkflow/` directory, and begins polling for work. Leave this running in its own terminal. ## 3. Run your first workflow Open a second terminal and trigger the example workflow: ```bash npm theme={null} npx tsx openworkflow/hello-world.run.ts ``` ```bash pnpm theme={null} pnpx tsx openworkflow/hello-world.run.ts ``` ```bash bun theme={null} bun openworkflow/hello-world.run.ts ``` This script calls `ow.runWorkflow(helloWorld.spec, {})` to create a new workflow run in the database. The worker in your first terminal picks it up, executes each step, and marks it complete. The `hello-world.run.ts` file is a temporary helper for testing. In a real app, you'd call `ow.runWorkflow(workflow.spec, input)` from your API routes, scripts, or other application code. ## 4. View workflows in the dashboard OpenWorkflow includes a built-in web dashboard. Start it with: ```bash npm theme={null} npx @openworkflow/cli dashboard ``` ```bash pnpm theme={null} pnpx @openworkflow/cli dashboard ``` ```bash bun theme={null} bunx @openworkflow/cli dashboard ``` Open [http://localhost:3000](http://localhost:3000) to see your workflow run, its steps, and their results. ## What you just built The setup has four parts: 1. **`openworkflow.config.ts`**: Tells OpenWorkflow which database to use and where to find your workflow files. 2. **`openworkflow/hello-world.ts`**: Defines a workflow with one or more steps. Each step is a checkpoint: if anything crashes, the workflow resumes from the last completed step instead of starting over. 3. **`openworkflow/hello-world.run.ts`**: A script that enqueues a workflow run. In production, this would be your API handler or a cron job. 4. **The worker**: A long-running process that executes workflow runs. It polls the database, claims available work, and persists results. ## Next steps * [Overview](/docs/overview): Understand how workflows, steps, and workers fit together * [Workflows](/docs/workflows): Learn how to define and configure workflows * [Steps](/docs/steps): See how steps protect your operations from crashes * [Production](/docs/production): Deploy OpenWorkflow to your infrastructure # Retries Source: https://openworkflow.dev/docs/retries Automatic retry behavior for failed steps and workflows, with exponential backoff and reuse of completed step results In any app, things fail sometimes - a third-party API returns a 500, a database connection times out, or a network blip drops a request. These are transient failures that go away if you try again. OpenWorkflow handles this automatically by retrying failed steps. When a step throws, the workflow is rescheduled with an exponential backoff. Previously completed steps aren't re-run, only the failed step re-executes. ## How Retries Work When a step throws an error: 1. The step attempt is marked as `failed` and the error is recorded 2. The workflow run is rescheduled with exponential backoff 3. When the workflow resumes, it replays to the failed step 4. The step function executes again Steps are attempted up to 10 times by default (one initial attempt plus up to nine retries). If the step still fails after all attempts, the workflow is permanently marked as `failed`. To prevent runaway executions, each workflow run also has a hard cap of 1000 total step attempts. If the run reaches that cap, it fails immediately and is not retried. ## Rerun a Finished Workflow Use `rerunWorkflowRun` to create a new run from a failed, completed, or canceled run. Optionally pass a recorded step name to reuse earlier results: ```ts theme={null} const newRun = await ow.rerunWorkflowRun(originalRunId, { fromStep: "send-email", // <-- optional }); ``` The new run uses the original input and workflow version. It is ready to run immediately, with fresh retry counts and no deadline, idempotency key, or parent run. The source run stays unchanged. With `fromStep`, OpenWorkflow copies successful steps from before the selected step. The new run reuses those results. ## Step Retries Steps that throw are retried automatically: ```ts theme={null} await step.run({ name: "call-api" }, async () => { const response = await fetch("https://api.example.com/data"); if (!response.ok) { // Throwing here triggers a retry throw new Error(`API error: ${response.status}`); } return await response.json(); }); ``` ### Step Retry Policy Each step can define its own retry policy. If omitted, steps use these defaults: | Field | Default | Description | | - | - | - | | `initialInterval` | `"1s"` | Delay before the first retry | | `backoffCoefficient` | `2` | Multiplier applied to each subsequent retry delay | | `maximumInterval` | `"100s"` | Upper bound for retry delay | | `maximumAttempts` | `10` | Total attempts including the initial one (`0` = unlimited) | With these defaults, retry delays look like this: | Attempt | Delay | | - | - | | 1 | Immediate | | 2 | \~1s | | 3 | \~2s | | 4 | \~4s | | 5 | \~8s | | ... | ... | Override the defaults per step: ```ts theme={null} await step.run( { name: "call-api", retryPolicy: { initialInterval: "500ms", backoffCoefficient: 2, maximumInterval: "30s", maximumAttempts: 5, }, }, async () => { // step logic }, ); ``` Retries also stop early if the workflow has a `deadlineAt` and the next retry would exceed it. ## Workflow Retries Errors thrown outside of `step.run(...)` are workflow-level failures. They are not retried by default; the workflow is marked as `failed`. To enable workflow-level retries, set a `retryPolicy` on the workflow spec: ```ts theme={null} import { defineWorkflow } from "openworkflow"; defineWorkflow( { name: "charge-customer", retryPolicy: { initialInterval: "500ms", backoffCoefficient: 2, maximumInterval: "30s", maximumAttempts: 5, }, }, async ({ step }) => { // workflow implementation }, ); ``` Step retries and workflow retries are independent. Step failures use the step's own retry policy. The workflow retry policy only applies to errors thrown outside steps. ## Missing Workflow Definitions If a worker claims a run but doesn't have the matching workflow registered, it reschedules the run with exponential backoff (starting at 5s, capped at 5min). This keeps the run alive during rolling deploys or multi-worker setups where the right worker hasn't started yet. Once a worker with the correct definition comes online, it claims the run and executes normally. ## What Triggers a Retry Retries happen when: * A step function throws an error or returns a rejected promise * A worker crashes during step execution (the step is re-executed on recovery) Retries do not happen for: * Completed steps (cached results are returned) * Explicitly canceled workflows * Workflow-level errors (unless a workflow `retryPolicy` is configured) ## Error Handling You can catch step errors inside a workflow to run fallback logic: ```ts theme={null} defineWorkflow({ name: "with-error-handling" }, async ({ input, step }) => { try { await step.run({ name: "risky-operation" }, async () => { await externalApi.call(); }); } catch (error) { await step.run({ name: "fallback" }, async () => { await fallbackApi.call(); }); } }); ``` When you catch an error the workflow continues normally. The step is still recorded as failed, but no retry is triggered. ## Terminal Failures A workflow is permanently marked `failed` when step retries are exhausted (`maximumAttempts` reached), `deadlineAt` expires, or the run exceeds the step attempt cap. Once terminal, no more automatic retries occur. You can inspect and manually retry failed workflows from the [dashboard](/docs/dashboard). ## Monitoring Use the [dashboard](/docs/dashboard) to monitor retry health: ```bash npm theme={null} npx @openworkflow/cli dashboard ``` ```bash pnpm theme={null} pnpx @openworkflow/cli dashboard ``` ```bash bun theme={null} bunx @openworkflow/cli dashboard ``` # Roadmap Source: https://openworkflow.dev/docs/roadmap What's coming next for OpenWorkflow, what's in the current release, and where to report bugs or request features ## Live in current `npm` release * PostgreSQL and SQLite backends * CLI (`npx @openworkflow/cli`) * Dashboard (`npx @openworkflow/cli dashboard`) * Bun runtime support * Per-worker concurrency control * Step memoization & retries * Graceful shutdown * Parallel step execution * Sleeping (pausing) workflows * Workflow versioning * Workflow cancellation * Configurable retry policies * Idempotency keys * Prometheus `/metrics` endpoint * Child workflows (`step.runWorkflow`) * Signals (`step.sendSignal`, `step.waitForSignal`) * OpenTelemetry * Recurring cron schedules * Workflow-level reruns from the beginning or a saved step ## Planned * Rollback / compensation functions * Workflow priority and concurrency controls ## Bugs & feature requests Found a bug or have a feature request? Please open an issue on GitHub so we can track and prioritize it: [https://github.com/openworkflowdev/openworkflow/issues/new](https://github.com/openworkflowdev/openworkflow/issues/new) # Recurring schedules Source: https://openworkflow.dev/docs/schedules Run workflows on a recurring schedule using cron expressions and your existing workers Recurring schedules create workflow runs at times defined by a cron expression. Your existing workers create and execute these runs like any other workflow. Schedules and their progress are stored in PostgreSQL or SQLite, so they survive worker restarts. ## Defining a Schedule Add `schedules` to your workflow spec. When a worker starts, it saves these schedules before polling for work: ```ts theme={null} import { defineWorkflow } from "openworkflow"; export const report = defineWorkflow<{ accountId: string }, void>( { name: "daily-report", schedules: [ { id: "daily-report:acme", cron: "0 9 * * 1-5", timezone: "America/Chicago", input: { accountId: "acme" }, }, ], }, async ({ input, step }) => { await step.run({ name: "send-report" }, async () => { await sendReport(input.accountId); }); }, ); ``` This schedules a report for 9 a.m. every weekday in Chicago. Register the workflow with a worker as described in [Workers](/docs/workers). You can also define schedules with `ow.defineWorkflow(...)` or `defineWorkflowSpec(...)`. Each schedule has an ID, which defaults to the exact workflow name. To define multiple schedules for the same workflow, give each one a different ID. ## Scheduling at Runtime Use `ow.scheduleWorkflow()` to create or replace a schedule from your application: ```ts theme={null} await ow.scheduleWorkflow( report.spec, { accountId: "acme" }, { id: "daily-report:acme", cron: "0 9 * * 1-5", timezone: "America/Chicago", }, ); ``` If you define a workflow with `ow.defineWorkflow(...)`, you can call `.schedule(input, options)` on it directly. Both methods return the saved schedule, including its `id` and `nextRunAt`. A `null` value for `nextRunAt` means the schedule is paused. If the workflow has an input schema, OpenWorkflow validates and transforms the input when you register the schedule. The resulting input is stored and reused for each scheduled run. ## Schedule Options | Option | Description | | - | - | | `id` | Schedule ID. Defaults to the exact workflow name. | | `cron` | Required cron expression. Accepts five fields, or six fields with seconds first. | | `timezone` | IANA time zone. Defaults to `UTC`. | Randomized `H` fields and shorthand aliases are not supported. ## Timing A new schedule starts at the next scheduled time after registration. Workers check for due schedules about once per second. At least one worker must be running to create runs on time. After downtime, OpenWorkflow creates one run for each overdue schedule, then advances it to the next future occurrence. It does not create a separate run for every missed occurrence. Time zones and daylight saving time follow `cron-parser`'s default behavior. For example, a Chicago schedule set to 2:30 a.m. runs at 3:30 a.m. on the day clocks move forward. Each scheduled run is independent. A new run can start while a previous run is still in progress. Runs start when a worker has capacity, so the scheduled time is not a guaranteed start time. Scheduled runs use the workflow's existing [workflow and step retry policies](/docs/retries). ## Managing Schedules Use `ow.schedules` to get, list, pause, resume, or delete schedules: ```ts theme={null} const schedule = await ow.schedules.get("daily-report:acme"); const page = await ow.schedules.list({ limit: 100 }); await ow.schedules.pause("daily-report:acme"); await ow.schedules.resume("daily-report:acme"); await ow.schedules.delete("daily-report:acme"); ``` All operations use the client's [namespace](/docs/namespaces). `get` returns `null` if the schedule does not exist. `list` returns `data` and `pagination`. To get another page, pass a returned cursor as `after` or `before`. ### Pausing and Resuming Pausing a schedule sets `nextRunAt` to `null` and stops it from creating new runs. It does not cancel runs that are already queued or running. Resuming a schedule sets its next run to the first scheduled time strictly after the resume time. Times missed while paused are skipped. Pausing an already paused schedule or resuming an already active schedule leaves its state and schedule progress unchanged. Both methods throw an error if the schedule does not exist. Registering or updating a paused schedule does not resume it. Use `resume` to start it again. ### Deleting Schedules Deleting a schedule stops it from creating new runs. It does not cancel runs that are already queued or running. Deleting a schedule that does not exist succeeds. Removing a schedule from your code does not delete it from the database. To retire a schedule, remove its declaration and delete it with `ow.schedules.delete()`. If the declaration remains in your code, the next worker startup recreates the deleted schedule. ## Updating Schedules Call `.schedule()` or `ow.scheduleWorkflow()` with an existing ID to replace that schedule. These methods replace the full definition, not just the fields you provide. Omitted options use their defaults. What happens next depends on what changed: | Change | Behavior | | - | - | | Same cron expression and time zone | Preserves the schedule's current progress. | | Different cron expression or time zone | Moves an active schedule to its next future occurrence. A paused schedule stays paused. | | Different input | Preserves the next scheduled time. Runs that have not yet been queued use the new input. | Runs that have already been created keep their original input. ## Identifying Scheduled Runs Workflow run records include `workflowScheduleId`. For scheduled runs, this is the schedule's ID. For other runs, it is `null`. The ID stays on the run after retries or schedule deletion, so you can still associate past runs with their schedule. # Sentry Source: https://openworkflow.dev/docs/sentry View workflow traces in Sentry with OpenTelemetry to see which steps ran, how long they took, and where they failed You can use Sentry to see what happens during a workflow run: which steps ran, how long they took, and where they failed. OpenWorkflow already emits [OpenTelemetry spans](/docs/opentelemetry), so the setup connects your existing OpenTelemetry provider to Sentry. OpenWorkflow trace in Sentry ## 1. Install Install Sentry and the OpenTelemetry packages it needs: ```bash theme={null} npm install @sentry/node-core @opentelemetry/api @opentelemetry/exporter-trace-otlp-http @opentelemetry/sdk-trace-base ``` ## 2. Connect Sentry In your existing `instrumentation.ts`, add these imports and call `Sentry.init()` after starting your OpenTelemetry provider (`sdk.start()`): ```ts theme={null} import * as Sentry from "@sentry/node-core/light"; import { otlpIntegration } from "@sentry/node-core/light/otlp"; Sentry.init({ dsn: process.env["SENTRY_DSN"], integrations: [otlpIntegration()], }); ``` This adds Sentry as another destination for your spans. Your existing exporters and sampling settings still apply. See Sentry's [OTLP integration guide](https://docs.sentry.io/platforms/javascript/guides/node/install/lightweight/#using-with-opentelemetry-otlp) for more options. ## 3. Start your app Set your Sentry project's DSN and load the instrumentation before your app. For TypeScript with `tsx`: ```bash theme={null} SENTRY_DSN='' npx tsx --import ./instrumentation.ts index.ts ``` If your app and worker run in separate processes, load the instrumentation in both. You can pass the DSN as shown above; you don't need a `.env` file. During shutdown, stop the worker and await `sdk.shutdown()` to flush spans, then await `Sentry.close(5000)` to flush Sentry events. ## 4. View a workflow run Run a workflow, then open Sentry's Traces view. Filter by `openworkflow.run.id` using your run's ID. You'll see spans for creating the run, executing it, and running each step callback. Failed attempts stay marked as errors even when a retry succeeds. Sleeps and retries create separate execution traces. Each one links back to the run's creation span. See [Trace relationships](/docs/opentelemetry#trace-relationships) for how these fit together. To try a complete setup, follow the [OpenTelemetry example](https://github.com/openworkflowdev/openworkflow/tree/main/examples/opentelemetry). It starts a workflow from an HTTP request, calls a fake email endpoint, sleeps, and retries a failed step. # Signals Source: https://openworkflow.dev/docs/signals Send data between workflows at runtime without polling and wait for approvals or webhook callbacks with a timeout Signals let workflows communicate at runtime. A workflow can pause and wait for a signal, and another workflow (or your application code) can send that signal to wake it up with data attached. This is useful any time a workflow needs to wait for something that isn't on a timer: a human approval, a webhook callback, a payment confirmation, or a coordination message from another workflow. ## Basic Usage ### Waiting for a Signal Use `step.waitForSignal()` inside a workflow to pause until a matching signal arrives: ```ts theme={null} import { defineWorkflow } from "openworkflow"; export const approvalWorkflow = defineWorkflow( { name: "approval-workflow" }, async ({ input, step }) => { await step.run({ name: "request-approval" }, async () => { await slack.send({ channel: "#approvals", text: `Please approve order ${input.orderId}`, }); }); // wait until someone sends the "approval" signal const decision = await step.waitForSignal<{ approved: boolean }>({ signal: `approval:${input.orderId}`, timeout: "7d", }); if (decision?.data.approved) { await step.run({ name: "process-order" }, async () => { await orders.process(input.orderId); }); } }, ); ``` ### Sending a Signal Send a signal from another workflow using `step.sendSignal()`: ```ts theme={null} export const reviewWorkflow = defineWorkflow( { name: "review-workflow" }, async ({ input, step }) => { const verdict = await step.run({ name: "run-review" }, async () => { return await reviewService.evaluate(input.orderId); }); await step.sendSignal({ signal: `approval:${input.orderId}`, data: { approved: verdict.passed }, }); }, ); ``` Or send a signal from your application code using the client: ```ts theme={null} import { ow } from "./openworkflow/client"; // from an API route, webhook handler, etc. await ow.sendSignal({ signal: `approval:${orderId}`, data: { approved: true }, }); ``` Signals are not buffered. If you send a signal before any workflow is waiting for it, the signal is lost. ## Signal Names Signal names are arbitrary strings scoped to the backend namespace. Use descriptive, unique names, often including an entity ID, to avoid collisions: ```ts theme={null} // Good - scoped to a specific entity await step.waitForSignal({ signal: `payment:${invoiceId}` }); await step.waitForSignal({ signal: `approval:order:${orderId}` }); // Bad - too generic, could collide across workflows await step.waitForSignal({ signal: "done" }); ``` ## Step Names Like other step types, signal steps need unique names within a workflow. If you don't provide one, OpenWorkflow uses the signal name as the step name. ```ts theme={null} // Implicit: step name defaults to signal name await step.waitForSignal({ signal: `payment:${invoiceId}` }); // Explicit step name await step.waitForSignal({ name: "wait-for-payment", signal: `payment:${invoiceId}`, }); ``` ## Timeout `step.waitForSignal` accepts an optional `timeout`. If the signal doesn't arrive before the timeout, the step resolves with `null` instead of blocking forever. ```ts theme={null} const result = await step.waitForSignal({ signal: `approval:${orderId}`, timeout: "24h", }); if (result === null) { // Timed out: no signal arrived within 24 hours await step.run({ name: "escalate" }, async () => { await alerts.send("Approval timed out"); }); } ``` `timeout` accepts a [duration string](/docs/sleeping#duration-formats), a number of milliseconds, or a `Date`. If no timeout is specified, the wait defaults to 1 year. ## Schema Validation Validate signal payloads at receive time using any [Standard Schema](/docs/standard-schema) compatible validator: ```ts theme={null} import { z } from "zod"; const approvalSchema = z.object({ approved: z.boolean(), reviewedBy: z.string(), }); const decision = await step.waitForSignal({ signal: `approval:${orderId}`, schema: approvalSchema, timeout: "7d", }); // `decision` is typed as { data: { approved: boolean; reviewedBy: string } } | null ``` If the signal data doesn't match the schema, the step fails permanently (no retries) to surface the contract violation immediately. ## Idempotency When sending signals from the client, you can provide an idempotency key to safely retry without delivering the signal twice: ```ts theme={null} await ow.sendSignal({ signal: `payment:${invoiceId}`, data: { amount: 99.99 }, idempotencyKey: `payment-confirmed:${invoiceId}`, }); ``` If a signal with the same idempotency key has already been sent and delivered to at least one waiter, the call returns the original result without re-delivering. ## Fan-Out: One Signal, Many Waiters A single `sendSignal` call delivers to every workflow currently waiting on that signal name: ```ts theme={null} // Multiple workflows waiting on the same signal const workflowA = defineWorkflow({ name: "listener-a" }, async ({ step }) => { const config = await step.waitForSignal({ signal: "config-updated" }); // handle update }); const workflowB = defineWorkflow({ name: "listener-b" }, async ({ step }) => { const config = await step.waitForSignal({ signal: "config-updated" }); // handle update }); // One signal wakes both workflows await ow.sendSignal({ signal: "config-updated", data: { version: 42 }, }); ``` ## Sending Signals from Workflows Use `step.sendSignal()` inside a workflow to send signals durably. The send is recorded as a step attempt, so it won't be repeated on replay: ```ts theme={null} const result = await step.sendSignal({ signal: `approval:${orderId}`, data: { approved: true }, }); // result.workflowRunIds contains IDs of workflows that received the signal ``` ## Common Patterns ### Human-in-the-Loop Approval ```ts theme={null} export const purchaseWorkflow = defineWorkflow( { name: "purchase" }, async ({ input, step }) => { await step.run({ name: "send-approval-request" }, async () => { await email.send({ to: input.managerEmail, subject: `Approve purchase: $${input.amount}`, body: `Click to approve: ${approvalUrl(input.purchaseId)}`, }); }); const approval = await step.waitForSignal<{ approved: boolean }>({ signal: `purchase-approval:${input.purchaseId}`, timeout: "3d", }); if (!approval?.data.approved) { await step.run({ name: "notify-rejected" }, async () => { await email.send({ to: input.requesterEmail, subject: "Purchase request denied", }); }); return { status: "rejected" }; } await step.run({ name: "process-purchase" }, async () => { await purchasing.submit(input.purchaseId); }); return { status: "approved" }; }, ); // in your API route handler: app.post("/approve/:purchaseId", async (req, res) => { await ow.sendSignal({ signal: `purchase-approval:${req.params.purchaseId}`, data: { approved: req.body.approved }, }); res.json({ ok: true }); }); ``` ### Webhook Callback ```ts theme={null} export const paymentWorkflow = defineWorkflow( { name: "payment" }, async ({ input, step }) => { const checkout = await step.run({ name: "create-checkout" }, async () => { return await stripe.checkout.sessions.create({ metadata: { workflowSignal: `payment:${input.orderId}` }, // ... }); }); const payment = await step.waitForSignal({ signal: `payment:${input.orderId}`, timeout: "1h", }); if (!payment) { return { status: "expired" }; } await step.run({ name: "fulfill-order" }, async () => { await orders.fulfill(input.orderId); }); return { status: "paid" }; }, ); // in your Stripe webhook handler: app.post("/webhooks/stripe", async (req, res) => { const event = req.body; if (event.type === "checkout.session.completed") { await ow.sendSignal({ signal: event.data.object.metadata.workflowSignal, data: { sessionId: event.data.object.id }, idempotencyKey: event.id, }); } res.sendStatus(200); }); ``` ### Workflow-to-Workflow Coordination ```ts theme={null} const producer = defineWorkflow( { name: "data-producer" }, async ({ input, step }) => { const data = await step.run({ name: "generate-data" }, async () => { return await heavyComputation(input); }); await step.sendSignal({ signal: `data-ready:${input.batchId}`, data: { recordCount: data.length }, }); }, ); const consumer = defineWorkflow( { name: "data-consumer" }, async ({ input, step }) => { const notification = await step.waitForSignal({ signal: `data-ready:${input.batchId}`, timeout: "1h", }); if (notification) { await step.run({ name: "process-data" }, async () => { await processRecords(input.batchId, notification.data.recordCount); }); } }, ); ``` # Sleeping Source: https://openworkflow.dev/docs/sleeping Pause workflows for seconds, hours, or months without blocking resources, with timers that survive worker restarts Sometimes a workflow needs to wait — maybe you want to send a follow-up email a week after sign-up, or pause between API calls to respect rate limits. `step.sleep()` pauses a workflow for any duration (seconds to months) in a durable way: the resume time is stored in the database, and the worker slot is freed for other work. When the sleep finishes, a worker picks the workflow back up and continues from where it left off. This is different from `setTimeout` or a regular `await sleep()`. Those tie up a running process and are lost if the server restarts. With `step.sleep()`, you can have thousands of sleeping workflows without using any compute. ## Basic Usage ```ts theme={null} import { defineWorkflow } from "openworkflow"; export const reminderWorkflow = defineWorkflow( { name: "send-reminder" }, async ({ input, step }) => { await step.run({ name: "send-initial-email" }, async () => { await emailService.send({ to: input.email, subject: "Welcome!", }); }); // Pause for 7 days await step.sleep("wait-7-days", "7d"); await step.run({ name: "send-followup" }, async () => { await emailService.send({ to: input.email, subject: "How's it going?", }); }); }, ); ``` ## How Sleep Works When a workflow encounters `step.sleep()`: 1. A step attempt is created with the resume time 2. The workflow is durably parked in `running` with `workerId = null` and `availableAt` set to the resume time 3. The worker releases the workflow (frees the slot) 4. When the sleep duration elapses, the workflow becomes available again 5. A worker claims it and resumes from after the sleep ## Duration Formats The duration argument accepts a number followed by a unit: | Unit | Aliases | Examples | | - | - | - | | milliseconds | `ms`, `msec`, `msecs` | `100ms`, `1.5ms` | | seconds | `s`, `sec`, `secs` | `5s`, `0.25s` | | minutes | `m`, `min`, `mins` | `2m`, `1.5m` | | hours | `h`, `hr`, `hrs` | `1h`, `0.25h` | | days | `d`, `day(s)` | `1d`, `0.5d` | | weeks | `w`, `week(s)` | `1w`, `2w` | | months | `mo`, `month(s)` | `1mo`, `2mo` | | years | `y`, `yr`, `yrs` | `1y`, `2yr` | Examples: ```ts theme={null} await step.sleep("short-wait", "500ms"); await step.sleep("one-minute", "1m"); await step.sleep("half-hour", "30min"); await step.sleep("overnight", "12h"); await step.sleep("one-week", "1w"); await step.sleep("quarterly", "3mo"); ``` ## Sleep Names Like `step.run()`, each sleep needs a unique name within the workflow: ```ts theme={null} // Good - descriptive names await step.sleep("wait-for-trial-end", "14d"); await step.sleep("cool-off-period", "1h"); // Bad - generic names await step.sleep("sleep-1", "1h"); await step.sleep("sleep-2", "1h"); ``` ## Common Patterns ### Scheduled Follow-ups ```ts theme={null} export const onboardingSequence = defineWorkflow( { name: "onboarding-sequence" }, async ({ input, step }) => { await step.run({ name: "send-welcome" }, ...); await step.sleep("wait-1-day", "1d"); await step.run({ name: "send-tips" }, ...); await step.sleep("wait-3-days", "3d"); await step.run({ name: "send-checkin" }, ...); await step.sleep("wait-7-days", "7d"); await step.run({ name: "send-survey" }, ...); }, ); ``` ### Trial Expiration ```ts theme={null} export const trialWorkflow = defineWorkflow( { name: "trial-workflow" }, async ({ input, step }) => { await step.run({ name: "start-trial" }, async () => { await db.users.update(input.userId, { trialStartedAt: new Date() }); }); // Wait for trial to end await step.sleep("trial-period", "14d"); const user = await step.run({ name: "check-subscription" }, async () => { return await db.users.findOne({ id: input.userId }); }); if (!user.hasSubscription) { await step.run({ name: "end-trial" }, async () => { await db.users.update(input.userId, { trialEnded: true }); await emailService.send({ to: user.email, subject: "Your trial has ended", }); }); } }, ); ``` ### Rate Limiting ```ts theme={null} export const rateLimitedSync = defineWorkflow( { name: "sync-data" }, async ({ input, step }) => { for (const batch of input.batches) { await step.run({ name: `sync-batch-${batch.id}` }, async () => { await api.sync(batch); }); // Respect API rate limits await step.sleep(`rate-limit-${batch.id}`, "1s"); } }, ); ``` ## When to Use Sleep Use `step.sleep()` for: * Scheduled follow-ups (emails, notifications) * Trial periods and expiration workflows * Rate limiting between API calls * Retry backoff delays * Any pause longer than a few seconds For very short delays (under a few seconds), consider using regular `await` with a Promise. The overhead of a durable sleep isn't worth it for sub-second delays. ```ts theme={null} // For short delays, regular await is fine await new Promise((resolve) => setTimeout(resolve, 100)); // For longer delays, use step.sleep for durability await step.sleep("wait-one-minute", "1m"); ``` ## Sleep and Memoization Once a sleep completes, it's memoized like any other step. If the workflow replays after the sleep has finished, it returns immediately: ```ts theme={null} await step.sleep("wait-1-hour", "1h"); // If workflow replays after 1 hour passed, this returns instantly ``` # SQLite Backend Source: https://openworkflow.dev/docs/sqlite Lightweight SQLite storage for development and testing on Node.js or Bun without running a separate database server The SQLite backend is the easiest way to get started without running an external database. It's ideal for local development, testing, and single-server deployments. When you're ready for production with multiple workers, switch to [PostgreSQL](/docs/postgres). Uses `bun:sqlite` for Bun and `node:sqlite` for Node.js (requires Node.js 22.5+). ## Setup ```ts theme={null} import { OpenWorkflow } from "openworkflow"; import { BackendSqlite } from "openworkflow/sqlite"; const backend = BackendSqlite.connect("./openworkflow/backend.db"); const ow = new OpenWorkflow({ backend }); ``` Note: `BackendSqlite.connect()` is synchronous (no `await` needed). ## File Path The database file is created if it doesn't exist: ```ts theme={null} // Relative path (recommended for projects) const backend = BackendSqlite.connect("./openworkflow/backend.db"); // Absolute path const backend = BackendSqlite.connect("/var/lib/openworkflow/data.db"); // In-memory database (data lost on restart) const backend = BackendSqlite.connect(":memory:"); ``` ## Configuration Options ```ts theme={null} const backend = BackendSqlite.connect(path, { // Namespace for multi-tenant isolation (default: "default") namespaceId: "development", // Whether to run migrations on connect (default: true) runMigrations: true, }); ``` ## Git Ignore Add the database file and SQLite sidecar files to `.gitignore`: ```gitignore theme={null} openworkflow/backend.db* ``` The CLI's `init` command does this automatically when you choose SQLite. ## When to Use SQLite **Good for:** * Local development * Testing and CI * Single-server deployments * Prototyping * Small-scale production with low throughput **Consider PostgreSQL for:** * Multi-server deployments * High concurrency requirements * Production workloads with many workers * Environments requiring database-level backups ## Concurrency SQLite supports concurrent reads but uses locking for writes. The backend handles this with `BEGIN IMMEDIATE` transactions. For single-worker setups, this works well. For multiple workers, PostgreSQL is recommended. ## Namespaces Use `namespaceId` to isolate environments: ```ts theme={null} const devBackend = BackendSqlite.connect("./backend.db", { namespaceId: "development", }); const testBackend = BackendSqlite.connect("./backend.db", { namespaceId: "test", }); ``` See [Namespaces](/docs/namespaces) for more details. ## Config File Example For use with the CLI, create `openworkflow.config.ts`: ```ts theme={null} import { defineConfig } from "@openworkflow/cli"; import { BackendSqlite } from "openworkflow/sqlite"; export default defineConfig({ backend: BackendSqlite.connect("./openworkflow/backend.db"), dirs: ["./openworkflow"], }); ``` ## Development + Production Setup A common pattern uses SQLite for development and PostgreSQL for production: ```ts theme={null} import { defineConfig } from "@openworkflow/cli"; import { BackendPostgres } from "openworkflow/postgres"; import { BackendSqlite } from "openworkflow/sqlite"; const isDev = process.env.NODE_ENV !== "production"; const backend = isDev ? BackendSqlite.connect("./openworkflow/backend.db") : await BackendPostgres.connect(process.env.OPENWORKFLOW_POSTGRES_URL!); export default defineConfig({ backend, dirs: ["./openworkflow"], }); ``` # Standard Schema Source: https://openworkflow.dev/docs/standard-schema Use Zod, Valibot, ArkType, and more to validate workflow inputs at runtime and infer TypeScript types from schemas OpenWorkflow supports the [Standard Schema](https://standardschema.dev/) spec, which means you can use your preferred validation library to enforce type-safe contracts on workflow inputs. ## Overview When you define a workflow, you can provide a `schema`. OpenWorkflow uses this schema to: 1. **Validate inputs at runtime**: When you start a run (for example, with `ow.runWorkflow(workflow.spec, input)`), the input is validated immediately. If validation fails, an error is thrown before the workflow is enqueued, preventing invalid data from entering your system. 2. **Type Safety**: The `input` parameter in your workflow function is automatically typed based on your schema. ## Supported Libraries Any library that implements the Standard Schema specification works out of the box, including: * [Zod](https://zod.dev) * [Valibot](https://valibot.dev) * [ArkType](https://arktype.io) * [Yup](https://github.com/jquense/yup) And [many more](https://standardschema.dev/schema#what-schema-libraries-implement-the-spec). ## Examples ### Zod ```ts theme={null} import { defineWorkflow } from "openworkflow"; import { z } from "zod"; const emailSchema = z.object({ to: z.string().email(), subject: z.string().min(1), body: z.string(), }); export const sendEmail = defineWorkflow( { name: "send-email", schema: emailSchema, }, async ({ input, step }) => { // `input` is fully typed as { to: string; subject: string; body: string } await step.run({ name: "send-email" }, async () => { await emailProvider.send({ to: input.to, subject: input.subject, body: input.body, }); }); return { success: true, recipient: input.to }; }, ); ``` ### Valibot ```ts theme={null} import { defineWorkflow } from "openworkflow"; import * as v from "valibot"; const emailSchema = v.object({ to: v.pipe(v.string(), v.email()), subject: v.pipe(v.string(), v.minLength(1)), body: v.string(), }); export const sendEmail = defineWorkflow( { name: "send-email", schema: emailSchema, }, async ({ input, step }) => { // `input` is fully typed as { to: string; subject: string; body: string } await step.run({ name: "send-email" }, async () => { await emailProvider.send({ to: input.to, subject: input.subject, body: input.body, }); }); return { success: true, recipient: input.to }; }, ); ``` ### ArkType ```ts theme={null} import { type } from "arktype"; import { defineWorkflow } from "openworkflow"; const emailSchema = type({ to: "string.email", subject: "string>0", body: "string", }); export const sendEmail = defineWorkflow( { name: "send-email", schema: emailSchema, }, async ({ input, step }) => { // `input` is fully typed as { to: string; subject: string; body: string } await step.run({ name: "send-email" }, async () => { await emailProvider.send({ to: input.to, subject: input.subject, body: input.body, }); }); return { success: true, recipient: input.to }; }, ); ``` ## Validation Errors When validation fails, OpenWorkflow throws a detailed error that includes information about what went wrong: ```ts theme={null} import { ow } from "./openworkflow/client"; import { sendEmail } from "./workflows/send-email"; try { await ow.runWorkflow(sendEmail.spec, { to: "invalid-email", // Invalid email format subject: "", // Too short body: "Hello", }); } catch (error) { console.error("Validation failed:", error); } ``` # Steps Source: https://openworkflow.dev/docs/steps Durable checkpoints that don't repeat already completed work, saving step results for retries and crash recovery Steps are the building blocks inside workflows. Each step wraps a single operation — like calling an API, querying a database, or sending an email. When a step completes, its result is saved to the database. If the workflow restarts for any reason (a crash, a deploy, or a retry), completed steps return their saved results instantly instead of running again. Steps act as checkpoints that prevent your operations from being accidentally repeated. ## Defining Steps Use `step.run()` to define a step within a workflow: ```ts theme={null} const result = await step.run({ name: "fetch-user" }, async () => { return await db.users.findOne({ id: input.userId }); }); ``` The first argument is a config object with the step name (and optional `retryPolicy`). The second argument is the async function to execute. The function's return value is stored and returned on subsequent replays. ## Why Steps Matter Consider a workflow that charges a credit card and sends an email: ```ts theme={null} // Without steps (dangerous!) async function processOrder(orderId: string) { await chargeCard(orderId); // What if we crash here? await sendConfirmationEmail(orderId); } ``` If the server crashes after charging the card but before sending the email, a naive retry would charge the customer twice. With steps, each operation is memoized: ```ts theme={null} defineWorkflow({ name: "process-order" }, async ({ input, step }) => { await step.run({ name: "charge-card" }, async () => { await chargeCard(input.orderId); }); await step.run({ name: "send-email" }, async () => { await sendConfirmationEmail(input.orderId); }); }); ``` If the workflow crashes after the card is charged: 1. The workflow restarts from the beginning 2. The "charge-card" step sees it already completed and returns instantly 3. The "send-email" step runs normally The customer is charged exactly once. ## Step Names Step names identify checkpoints during replay and should be stable across code changes. Use descriptive names that reflect what the step does: ```ts theme={null} // Good - descriptive, stable names await step.run({ name: "fetch-user" }, ...); await step.run({ name: "send-welcome-email" }, ...); await step.run({ name: "update-user-status" }, ...); ``` If two steps share the same name in a single execution, OpenWorkflow automatically disambiguates them by appending `:1`, `:2`, and so on in encounter order. This is most useful for [dynamic steps](/docs/dynamic-steps) where the number of steps isn't known ahead of time. Changing step names after workflows are in-flight can cause replay errors. Completed steps won't be found in the history, causing them to re-execute. To change logic safely, see [Versioning](/docs/versioning). ## Return Values Steps can return any JSON-serializable value: ```ts theme={null} // Returning an object const user = await step.run({ name: "fetch-user" }, async () => { return await db.users.findOne({ id: input.userId }); }); // Returning a primitive const count = await step.run({ name: "count-items" }, async () => { return items.length; }); // Returning null/undefined await step.run({ name: "log-event" }, async () => { console.log("Event logged"); // implicitly returns undefined, stored as null }); ``` Step results must be JSON-serializable. Avoid returning functions, circular references, or class instances with methods. ## Step Types OpenWorkflow provides five step types: ### `step.run()` Executes arbitrary async code and memoizes the result: ```ts theme={null} const user = await step.run({ name: "fetch-user" }, async () => { return await db.users.findOne({ id: input.userId }); }); ``` ### `step.sleep()` Pauses the workflow until a specified duration has elapsed. See [Sleeping](/docs/sleeping) for details: ```ts theme={null} await step.sleep("wait-one-hour", "1h"); ``` ### `step.runWorkflow()` Starts a child workflow and waits for its result durably: ```ts theme={null} const childOutput = await step.runWorkflow( generateReportWorkflow.spec, { reportId: input.reportId }, { timeout: "5m" }, // optional, defaults to 1 year ); ``` ### `step.sendSignal()` Sends a signal to all workflows currently waiting on that signal name: ```ts theme={null} const { workflowRunIds } = await step.sendSignal({ signal: `approval:${orderId}`, data: { approved: true }, }); ``` ### `step.waitForSignal()` Pauses the workflow until a matching signal is sent, or the timeout expires: ```ts theme={null} const result = await step.waitForSignal({ signal: `approval:${orderId}`, timeout: "7d", }); // result is { data } or null if timed out ``` See [Signals](/docs/signals) for details. ## Retry Policy (Optional) Control backoff and retry limits for an individual step: ```ts theme={null} await step.run( { name: "charge-card", retryPolicy: { initialInterval: "1s", backoffCoefficient: 2, maximumInterval: "30s", maximumAttempts: 5, }, }, async () => { await payments.charge(); }, ); ``` Any `retryPolicy` fields you omit fall back to defaults. Step retry policies are independent from the workflow-level retry policy. See [Retries](/docs/retries) for the full behavior and defaults. ## Error Handling If a step throws an error, the error is recorded and the workflow fails: ```ts theme={null} await step.run({ name: "risky-operation" }, async () => { const response = await fetch("https://api.example.com/data"); if (!response.ok) { throw new Error(`API error: ${response.status}`); } return await response.json(); }); ``` Steps that throw are marked as failed. The workflow can be retried, and failed steps will re-execute (not return cached results). Each workflow run also has a hard cap of 1000 total step attempts. If that limit is reached, the run fails and does not retry. ## What Makes a Good Step Each step should be a meaningful unit of work. Good steps: * Represent a single logical operation (fetch user, send email, charge card) * Have side effects that shouldn't be repeated (payments, notifications) * Take a reasonable amount of time (not too granular, not too coarse) ```ts theme={null} // Good - each step is a meaningful operation await step.run({ name: "fetch-user" }, ...); await step.run({ name: "validate-subscription" }, ...); await step.run({ name: "charge-payment" }, ...); await step.run({ name: "send-receipt" }, ...); // Too granular - overhead outweighs benefits await step.run({ name: "parse-json" }, ...); await step.run({ name: "validate-field-1" }, ...); await step.run({ name: "validate-field-2" }, ...); // Too coarse - no benefit from checkpointing await step.run({ name: "do-everything" }, async () => { await fetchUser(); await chargeCard(); await sendEmail(); }); ``` If an operation has no side effects and is fast to compute, consider whether it really needs to be a step. Pure computations can happen outside of steps. ## Large Payloads Every step result is persisted to the database. If a step returns a large payload, your workflow history can become heavy — especially when you have many steps. A good pattern is to offload large data to external storage and return only a reference: ```ts theme={null} const data = await step.run({ name: "fetch-report" }, async () => { const report = await analyticsApi.generate(input.reportId); const objectKey = `reports/${input.reportId}.json`; await objectStore.put(objectKey, JSON.stringify(report)); // Store only the reference, not the full report return { reportId: input.reportId, objectKey }; }); ``` # Type Safety Source: https://openworkflow.dev/docs/type-safety End-to-end TypeScript type safety for workflows, with typed inputs and outputs and inferred types for step results OpenWorkflow uses schemas or explicit types to type workflow inputs and outputs. Step result types are inferred from their functions' return values. These types provide autocomplete and compile-time checking throughout. ## Schema Validation with Type Inference For runtime validation with type inference, use [Standard Schema](/docs/standard-schema) compatible libraries like Zod: ```ts theme={null} import { defineWorkflow } from "openworkflow"; 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 }) => { // input is typed as { email: string; amount: number } // and validated at runtime return { recipient: input.email, amount: input.amount }; }, ); ``` With a schema and no explicit generics, the input type comes from the schema and the output type is inferred from the workflow function's return value. Here, the output is `{ recipient: string; amount: number }`. ## Explicit Type Definitions To manually specify input and output types, use both generics with `defineWorkflow`. Generics provide compile-time checking; use a schema if you also need runtime input validation. This example uses your application's `validateOrder` and `shipOrder` functions. Follow the [Quick Start](/docs/quickstart) for a runnable example with setup instructions. ```ts theme={null} import { defineWorkflow } from "openworkflow"; interface OrderInput { orderId: string; customerId: string; } interface OrderResult { success: boolean; trackingNumber: string; } export const processOrder = defineWorkflow( { name: "process-order" }, async ({ input, step }) => { 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 supply only the input generic, as in `defineWorkflow<{ name: string }>(...)`, the workflow's output type is `unknown`, so `handle.result()` returns `Promise`. Supply both input and output types when you need a typed result without a schema: ```ts theme={null} import { defineWorkflow } from "openworkflow"; export const greet = defineWorkflow<{ name: string }, string>( { name: "greet" }, async ({ input }) => { return `Hello, ${input.name}!`; // Output checked against string }, ); ``` ## Workflow Specs When using `defineWorkflowSpec` for separate declaration and implementation, types flow through: ```ts theme={null} 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: ```ts theme={null} 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 const greetingHandle = await ow.runWorkflow(greet.spec, { name: "World" }); const greeting = await greetingHandle.result(); // string ``` ## Step Result Types Step return types are inferred automatically, even when you supply only the workflow's input generic: ```ts theme={null} import { defineWorkflow } from "openworkflow"; interface User { email: string; name: string; } 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 (await response.json()) as User; }); // TypeScript knows user has these properties console.log(user.email, user.name); }, ); ``` # Versioning Source: https://openworkflow.dev/docs/versioning Version workflows for backwards compatibility so existing runs finish with their old logic when you deploy changes Workflow versioning lets you update a workflow's logic without breaking runs that are already in progress. ## Why Version Workflows? Imagine you have a "process-order" workflow and you want to change how it validates orders. But there are already 50 order workflows running with the old logic. If you just swap in the new code, those in-flight runs might fail because their saved step history doesn't match the new code path. Versioning solves this by letting you run both versions side by side: * Existing runs continue with the old logic * New runs use the updated version * Once all old runs finish, you remove the old version ## Specifying a Version Add a `version` to your workflow spec: ```ts theme={null} import { defineWorkflow } from "openworkflow"; export const orderWorkflow = defineWorkflow( { name: "process-order", version: "2.0.0", }, async ({ input, step }) => { // New v2 logic await step.run({ name: "validate-v2" }, async () => { return validateOrderV2(input); }); await step.run({ name: "process-v2" }, async () => { return processWithNewLogic(input); }); }, ); ``` ## Running Multiple Versions Register multiple versions of the same workflow with different implementations: ```ts theme={null} import { OpenWorkflow } from "openworkflow"; import { BackendPostgres } from "openworkflow/postgres"; const backend = await BackendPostgres.connect( process.env.OPENWORKFLOW_POSTGRES_URL, ); const ow = new OpenWorkflow({ backend }); // Version 1 - for existing runs ow.implementWorkflow( { name: "process-order", version: "1.0.0" }, async ({ input, step }) => { await step.run({ name: "validate" }, () => validateOrderV1(input)); await step.run({ name: "process" }, () => processV1(input)); }, ); // Version 2 - for new runs ow.implementWorkflow( { name: "process-order", version: "2.0.0" }, async ({ input, step }) => { await step.run({ name: "validate-v2" }, () => validateOrderV2(input)); await step.run({ name: "process-v2" }, () => processWithNewLogic(input)); }, ); const worker = ow.newWorker(); await worker.start(); ``` ## Migration Strategy A typical version migration: 1. **Deploy new version alongside old** - Register both versions in your worker 2. **Start new runs on new version** - Update your client code to use the new version 3. **Wait for old runs to complete** - Monitor the dashboard for remaining v1 runs 4. **Remove old version** - Once all v1 runs are finished, remove the old implementation ## Version Matching The worker matches workflow runs to implementations by name and version: | Run Version | Registered Versions | Matched | | - | - | - | | `1.0.0` | `1.0.0`, `2.0.0` | `1.0.0` | | `2.0.0` | `1.0.0`, `2.0.0` | `2.0.0` | | `null` | `null`, `1.0.0` | `null` | | `3.0.0` | `1.0.0`, `2.0.0` | No match (retry) | If no matching version is found, the worker records an error and reschedules the run as `pending` with retry backoff so a worker that has the matching version can pick it up later. # Workers Source: https://openworkflow.dev/docs/workers Long-running processes that execute your workflows, poll for runs, and resume unfinished work after worker crashes A worker is a background process that executes your workflows. It polls the database for new workflow runs, picks them up, and executes them step by step. When it finishes one run, it looks for the next one. Workers are stateless — all workflow progress is stored in the database, not in the worker itself. This means you can stop a worker, restart it, or replace it with a new one at any time. If a worker crashes, another worker can seamlessly continue any in-progress workflows from the last completed step. ## Starting a Worker The recommended way to run a worker is via the CLI: ```bash npm theme={null} npx @openworkflow/cli worker start ``` ```bash pnpm theme={null} pnpx @openworkflow/cli worker start ``` ```bash bun theme={null} bunx @openworkflow/cli worker start ``` This command: 1. Loads your `openworkflow.config.ts` configuration 2. Discovers workflow files in the configured directories 3. Starts polling the database for work 4. Handles graceful shutdown on SIGINT/SIGTERM ## How Workers Execute Workflows When a worker picks up a workflow: 1. **Claim**: The worker atomically claims the workflow run from the database 2. **Load history**: The worker loads all completed step attempts 3. **Replay**: The worker executes the workflow function from the beginning 4. **Memoization**: Completed steps return cached results instantly 5. **Execute**: New steps are executed and their results are stored 6. **Complete**: The workflow status is updated to `completed` or `failed` ## Concurrency Workers can process multiple workflow runs simultaneously. Configure concurrency in your `openworkflow.config.ts`: ```ts theme={null} import { backend } from "./openworkflow/client"; import { defineConfig } from "@openworkflow/cli"; export default defineConfig({ backend, worker: { concurrency: 10, // Process up to 10 workflows at once }, }); ``` Or via the CLI: ```bash npm theme={null} npx @openworkflow/cli worker start --concurrency 10 ``` ```bash pnpm theme={null} pnpx @openworkflow/cli worker start --concurrency 10 ``` ```bash bun theme={null} bunx @openworkflow/cli worker start --concurrency 10 ``` Start with `concurrency: 10` and tune based on your workload and database capacity. ## Heartbeats and Crash Recovery Workers maintain their claim on workflow runs through a heartbeat mechanism: 1. When a worker claims a workflow, it sets an `availableAt` timestamp in the future 2. While executing, the worker periodically extends this timestamp 3. If the worker crashes, the heartbeat stops 4. The `availableAt` timestamp expires 5. Another worker can claim and resume the workflow This ensures workflows are never stuck if a worker crashes. The lease lasts 30 seconds by default, and workers heartbeat at half the lease duration. Configure it with `leaseDurationMs`: ```ts theme={null} export default defineConfig({ backend, worker: { leaseDurationMs: 60_000, // Reclaim runs from crashed workers after 60s }, }); ``` A shorter lease recovers crashed runs sooner, at the cost of more frequent heartbeat writes to the database. ## High Availability For production, run multiple worker instances: ```bash npm theme={null} # Terminal 1 npx @openworkflow/cli worker start --concurrency 10 # Terminal 2 npx @openworkflow/cli worker start --concurrency 10 ``` ```bash pnpm theme={null} # Terminal 1 pnpx @openworkflow/cli worker start --concurrency 10 # Terminal 2 pnpx @openworkflow/cli worker start --concurrency 10 ``` ```bash bun theme={null} # Terminal 1 bunx @openworkflow/cli worker start --concurrency 10 # Terminal 2 bunx @openworkflow/cli worker start --concurrency 10 ``` Workers coordinate through the database: * Each workflow run is claimed by exactly one worker at a time * Workers use atomic database operations to prevent duplicate processing * If a worker crashes, its workflows become available to other workers ## Graceful Shutdown When a worker receives SIGINT or SIGTERM: 1. It stops polling for new work 2. It waits for active workflow runs to complete their current step 3. Active workflows are persisted back to the database 4. The worker process exits This ensures no work is lost during deploys. ```bash npm theme={null} # The worker handles Ctrl+C gracefully npx @openworkflow/cli worker start ^C # [info] Shutting down worker... # [info] Worker stopped ``` ```bash pnpm theme={null} # The worker handles Ctrl+C gracefully pnpx @openworkflow/cli worker start ^C # [info] Shutting down worker... # [info] Worker stopped ``` ```bash bun theme={null} # The worker handles Ctrl+C gracefully bunx @openworkflow/cli worker start ^C # [info] Shutting down worker... # [info] Worker stopped ``` ## Logging Workers log operational errors, such as failed polls and heartbeats, to `console` by default. Pass a `logger` with `debug`, `info`, `warn`, and `error` methods to send them somewhere else, such as a structured logger: ```ts theme={null} import pino from "pino"; const log = pino(); const ow = new OpenWorkflow({ backend, logger: { debug: (message, ...args) => log.debug({ args }, message), info: (message, ...args) => log.info({ args }, message), warn: (message, ...args) => log.warn({ args }, message), // workers pass the error after the message; `err` gets pino's serializer error: (message, err, ...args) => log.error({ err, args }, message), }, }); ``` Workers created with `ow.newWorker()` use the client's logger. You can also pass `logger` to `newWorker()` or to `new Worker()` directly. ## Workflow Discovery The CLI automatically discovers workflows from directories specified in your config: ```ts theme={null} // openworkflow.config.ts export default defineConfig({ backend, dirs: ["./openworkflow"], // Default directory }); ``` Workflow files should export workflows created with `defineWorkflow()`: ```ts theme={null} // openworkflow/send-email.ts import { defineWorkflow } from "openworkflow"; export const sendEmail = defineWorkflow( { name: "send-email" }, async ({ input, step }) => { // ... }, ); ``` The CLI scans for `.ts`, `.js`, `.mts`, `.mjs`, `.cts`, and `.cjs` files. # Workflows Source: https://openworkflow.dev/docs/workflows Durable functions that survive crashes and resume where they left off, with completed steps saved in your database A workflow is a function that describes a multi-step process — like processing an order, onboarding a new user, or syncing data with an external service. What makes it special is that it's durable: OpenWorkflow saves the progress of each step to the database. If the process crashes or the server restarts, the workflow picks up right where it left off instead of starting over. ## Defining a Workflow Define a workflow with `defineWorkflow`: ```ts theme={null} import { defineWorkflow } from "openworkflow"; export const sendWelcomeEmail = defineWorkflow( { name: "send-welcome-email" }, async ({ input, step }) => { const user = await step.run({ name: "fetch-user" }, async () => { return await db.users.findOne({ id: input.userId }); }); await step.run({ name: "send-email" }, async () => { await emailService.send({ to: user.email, subject: "Welcome!", body: "Thanks for signing up.", }); }); return { sent: true }; }, ); ``` A workflow consists of: 1. **A spec** - Configuration including the workflow name and optional version 2. **A function** - The async function that contains your workflow logic ## Running a Workflow Start a workflow by calling `ow.runWorkflow()` with the workflow's `spec`: ```ts theme={null} import { ow } from "./openworkflow/client"; import { sendWelcomeEmail } from "./workflows/send-welcome-email"; const handle = await ow.runWorkflow(sendWelcomeEmail.spec, { userId: "user_123", }); ``` `ow.runWorkflow()` returns a handle immediately. The actual workflow execution happens in a worker process. This lets your application continue without waiting for the workflow to complete. If you define a workflow with `ow.defineWorkflow(...)` instead, it returns a runnable workflow that supports `.run(...)` directly. ### Scheduling a Workflow Run You can schedule a workflow run for a specific time by passing `availableAt`: ```ts theme={null} const runAt = new Date("2026-02-05T15:00:00.000Z"); const handle = await ow.runWorkflow( sendWelcomeEmail.spec, { userId: "user_123" }, { availableAt: runAt }, ); ``` You can also pass a duration string using the same [duration format](/docs/sleeping#duration-formats) as `step.sleep`: ```ts theme={null} const handle = await ow.runWorkflow( sendWelcomeEmail.spec, { userId: "user_123" }, { availableAt: "5m" }, ); ``` The run stays `pending` until `availableAt` is reached, then workers can claim it. If `availableAt` is in the past, the run is immediately available. ## Waiting for Results If you need to wait for a workflow to complete, use `.result()` on the handle: ```ts theme={null} const handle = await ow.runWorkflow(sendWelcomeEmail.spec, { userId: "user_123", }); // Wait for completion (polls the database) const result = await handle.result(); console.log(result); // { sent: true } ``` `result()` polls the database periodically. Use this for workflows that complete quickly (seconds to minutes). For long-running workflows, consider using events or callbacks instead. You can also configure a custom timeout: ```ts theme={null} const result = await handle.result({ timeoutMs: 60000 }); // 1 minute timeout ``` ## Workflow Options ### Name (Required) Every workflow needs a unique name. This name is used to identify the workflow in the database and must match when workers execute the workflow. ```ts theme={null} defineWorkflow({ name: "process-order" }, async ({ input, step }) => { // ... }); ``` ### Version (Optional) Use versioning when you need to support multiple implementations of the same workflow simultaneously. See [Versioning](/docs/versioning) for details. ```ts theme={null} defineWorkflow( { name: "process-order", version: "v2" }, async ({ input, step, version }) => { // version === "v2" }, ); ``` ### Schema (Optional) Validate inputs before the workflow is enqueued using any Standard Schema compatible validator. See [Standard Schema](/docs/standard-schema) for details. ```ts theme={null} import { z } from "zod"; defineWorkflow( { name: "send-email", schema: z.object({ to: z.string().email(), subject: z.string().min(1), }), }, async ({ input, step }) => { // input is typed and validated }, ); ``` ### Retry Policy (Optional) Control backoff and retry limits for this workflow: ```ts theme={null} defineWorkflow( { name: "process-order", retryPolicy: { initialInterval: "1s", backoffCoefficient: 2, maximumInterval: "30s", maximumAttempts: 5, }, }, async ({ input, step }) => { // ... }, ); ``` Any `retryPolicy` fields you omit fall back to defaults. See [Retries](/docs/retries) for the full behavior and defaults. ### Idempotency Key (Optional) You can prevent duplicate run creation by providing an idempotency key, though there is a performance cost to checking for duplicates, so use this only when necessary: ```ts theme={null} const handle = await ow.runWorkflow( sendWelcomeEmail.spec, { userId: "user_123" }, { idempotencyKey: "welcome-email:user_123" }, ); ``` Within a given namespace, when an existing run matches the same `workflowName` + `idempotencyKey`, OpenWorkflow returns that existing run immediately. This dedupe window is built-in and lasts 24 hours from the original run creation time. The same `idempotencyKey` used in a different namespace will create a separate run. ## Workflow Function Parameters 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.runWorkflow`, `step.sendSignal`, `step.waitForSignal`) | | `version` | `string \| null` | The workflow version, if specified | | `run` | `WorkflowRunMetadata` | Read-only run metadata snapshot (`run.id`, etc.) | ```ts theme={null} defineWorkflow({ name: "example" }, async ({ input, step, version, run }) => { console.log("Input:", input); console.log("Version:", version); console.log("Run ID:", run.id); await step.run({ name: "my-step" }, async () => { // step logic }); }); ``` ## Workflow Run States A workflow run progresses through these states: | Status | Description | | - | - | | `pending` | Created and waiting for a worker to claim it | | `running` | Actively executing, or durably parked with `workerId = null` until `availableAt` | | `completed` | Finished successfully | | `failed` | Failed after exhausting retries, hitting deadline, or step cap | | `canceled` | Explicitly canceled and will not continue | ## Determinism When a workflow resumes after a crash, OpenWorkflow replays it from the beginning. Completed steps return their saved results instantly, so no work is repeated. But for this to work, the workflow must take the same path every time it runs with the same inputs — it must be deterministic. This means you should avoid putting non-deterministic operations (like API calls, random numbers, or the current time) directly in the workflow body. Instead, wrap them in steps so their results are saved and replayed consistently. Avoid calling `Math.random()`, `Date.now()`, or making API calls directly in the workflow function, since those change on each run, meaning they are non-deterministic. Wrap them in steps instead. ```ts theme={null} // Bad - non-deterministic defineWorkflow({ name: "bad" }, async ({ step }) => { const random = Math.random(); // Different on replay! // ... }); // Good - deterministic defineWorkflow({ name: "good" }, async ({ step }) => { const random = await step.run({ name: "get-random" }, () => Math.random()); // ... }); ``` # OpenWorkflow Source: https://openworkflow.dev/index OpenWorkflow is an open-source durable workflow engine for TypeScript, powered by PostgreSQL or SQLite. ```bash npm theme={null} npx @openworkflow/cli init ``` ```bash pnpm theme={null} pnpx @openworkflow/cli init ``` ```bash bun theme={null} bunx @openworkflow/cli init ``` If you want your AI coding assistant to help you set up OpenWorkflow, copy this prompt and paste it into your agent: ```text theme={null} Read and follow https://openworkflow.dev/skills/setup.md to set up OpenWorkflow. ``` OpenWorkflow Dashboard ## What is OpenWorkflow? OpenWorkflow is an open-source durable workflow engine for TypeScript, backed by PostgreSQL or SQLite. Wrap each operation in a step and OpenWorkflow saves its result. After a crash or deploy, the worker resumes the workflow using those saved results so they aren't re-run. Use it for background jobs, multi-step integrations, data processing, and AI workflows that need retries, long waits, or coordination between tasks. ```ts theme={null} import { defineWorkflow } from "openworkflow"; export const processOrder = defineWorkflow( { name: "process-order" }, async ({ input, step }) => { // if the server crashes after this step, the card is NOT charged again await step.run({ name: "charge-card" }, async () => { await payments.charge(input.orderId); }); await step.run({ name: "send-confirmation" }, async () => { await emails.send({ to: input.email, subject: "Order confirmed!" }); }); }, ); ``` ## Features
**Durable** - Workflows survive process restarts and server crashes. Completed steps are never repeated.
**Resumable** - After a crash or deploy, workflows pick up from the last completed step automatically.
**Pausable** - Workflows can sleep for seconds or months without tying up resources.
**Type Safe** - First-class TypeScript support for inputs and outputs.
## Get Started Build your first workflow in 5 minutes. Understand the core concepts: workflows, steps, and workers. Set up your project, start workers, and open the dashboard. Use agent skills, Markdown docs, and MCP search with your coding assistant. # Setup Source: https://openworkflow.dev/skills/setup Instructions for coding agents to install OpenWorkflow and verify a local workflow run using SQLite or PostgreSQL ## When to use Use this skill to add OpenWorkflow to a TypeScript or JavaScript project or finish an incomplete setup. It covers local setup and verification. Finish with a workflow run that returns its expected result. ## 1. Inspect the project Read the project's instructions, `package.json`, lockfile, and any existing OpenWorkflow config or workflows. Identify the runtime, package manager, and the package where OpenWorkflow belongs. Run setup from that package's directory. Preserve the project's language, module format, scripts, and existing code. Choose a path before making changes: * **New setup:** Continue to step 2 to initialize OpenWorkflow. * **Existing setup:** Read the [configuration reference](https://openworkflow.dev/docs/configuration.md), adapt the existing configuration, and continue to step 3. Skip `init`. * **Incomplete setup:** Inspect the existing files and dependencies. Read the [CLI reference](https://openworkflow.dev/docs/cli.md) to identify what setup still needs. Complete the missing pieces, preserve existing scripts, and continue to step 3. ## 2. Initialize OpenWorkflow Use the backend the user requested or the project already uses. For a first local setup with no database preference, use SQLite; it needs no separate database server. Read the guide for the selected backend when configuring its connection: [SQLite](https://openworkflow.dev/docs/sqlite.md) or [PostgreSQL](https://openworkflow.dev/docs/postgres.md). Run the command for the project's package manager: ```bash npm theme={null} npx --yes @openworkflow/cli init --backend sqlite --yes ``` ```bash pnpm theme={null} pnpx @openworkflow/cli init --backend sqlite --yes ``` ```bash bun theme={null} bunx @openworkflow/cli init --backend sqlite --yes ``` Replace `sqlite` with `postgres` for PostgreSQL, or `both` for SQLite in development and PostgreSQL when `NODE_ENV=production`. For PostgreSQL, set `OPENWORKFLOW_POSTGRES_URL` in `.env` to a working database connection before verification. If the connection details are unavailable, ask the user for them and complete any file changes that can proceed while waiting. Resume verification once the database is reachable. Always pass both `--backend` and `--yes` for unattended setup. The default command is interactive. In the npm command, the first `--yes` also skips the package-install prompt. Setup installs dependencies and creates a config, client, example workflow, and runner. File extensions depend on the project. It also adds a `worker` script and updates `.gitignore` or `.env` for the backend. If setup reports an existing config or a conflicting `worker` script, inspect and integrate with those files instead of deleting them to rerun setup. Use `--skip-install` only when dependencies must be installed separately, then run the installation commands it prints before verification. Read the [CLI reference](https://openworkflow.dev/docs/cli.md) when you need setup options beyond these commands. ## 3. Verify the setup Use the project's package manager to run these CLI commands. The examples below use npm: 1. Run `npx @openworkflow/cli doctor` and resolve any reported problems. 2. Start `npx @openworkflow/cli worker start` in a separate terminal or background process. Keep it running while executing the example. 3. Run the generated `openworkflow/hello-world.run.*` file. For TypeScript, use `npx tsx openworkflow/hello-world.run.ts` or `bun openworkflow/hello-world.run.ts`; for JavaScript, use `node` or `bun` with the generated filename. With pnpm, use `pnpx tsx` for TypeScript. 4. Confirm the runner prints a workflow result with `"greeting": "Hello, World!"`. For an existing setup, use an example workflow that is safe to run locally and check its expected result. Run the project's relevant type checks or tests after integration. Stop any background processes you started for verification. ## 4. Hand off the setup Summarize the files changed, the backend selected, and the verification result. Give the user the commands to start the worker, rerun the example, and open the dashboard with `npx @openworkflow/cli dashboard` (or their package manager's equivalent). Explain that application code enqueues workflows and a running worker executes them. Link to the [Quick Start](https://openworkflow.dev/docs/quickstart.md) for the walkthrough and [Workflows](https://openworkflow.dev/docs/workflows.md) for building their own workflows. ## Done when * `doctor` passes with the project's configuration and database connection. * The worker executes the example workflow and the runner returns the expected result. * The user has the verification result and commands to run the setup again. If a check fails, investigate the failure. If you cannot resolve it, report the specific blocker and what remains unverified. Only report setup as complete after the workflow returns its expected result.