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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion apps/docs/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,12 @@
},
{
"group": "Backends",
"pages": ["docs/postgres", "docs/sqlite", "docs/namespaces"]
"pages": [
"docs/postgres",
"docs/sqlite",
"docs/namespaces",
"docs/custom-backends"
]
},
{
"group": "Reference",
Expand Down
89 changes: 89 additions & 0 deletions apps/docs/docs/custom-backends.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
---
title: Custom Backends
description: Implement the Backend interface to store workflow state in your own database or driver, and check it with the backend test suite
---

OpenWorkflow ships with [PostgreSQL](/docs/postgres) and [SQLite](/docs/sqlite)
backends. To store workflow state somewhere else, implement the `Backend`
interface from `openworkflow/backend` and pass it to the client and worker like
any other backend:

```ts
import { MyBackend } from "./my-backend";
import { OpenWorkflow } from "openworkflow";

const backend = new MyBackend();
const ow = new OpenWorkflow({ backend });
```

<Warning>
The backend API is experimental. It can change in a minor release before 1.0
(for example, when a feature adds new backend methods), so pin the
`openworkflow` versions your backend supports, such as `>=0.11.0 <0.12.0`.
</Warning>

## The Backend Interface

`openworkflow/backend` exports the `Backend` interface and the types its methods
take and return:

```ts
import type {
Backend,
ClaimWorkflowRunParams,
WorkflowRun,
} from "openworkflow/backend";

export class MyBackend implements Backend {
async claimWorkflowRun(
params: ClaimWorkflowRunParams,
): Promise<WorkflowRun | null> {
// ...
}

// ...
}
```

A backend owns its connections and its schema. `stop()` is called when the
client or worker shuts down.
Comment on lines +48 to +49

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Automatic cleanup is incorrectly promised

The guide says the client or worker calls the backend’s stop(), but OpenWorkflow has no shutdown method and Worker.stop() never calls backend.stop(). Readers relying on this promise can leave database connections open. Say that callers must explicitly call backend.stop() after stopping their workers.

Suggested change
A backend owns its connections and its schema. `stop()` is called when the
client or worker shuts down.
A backend owns its connections and its schema. Call `backend.stop()` explicitly
when you're done with it, after stopping any workers that use it. The client and
worker do not call it for you.


## Shared Helpers

The built-in backends keep the rules that don't depend on storage in shared
helpers. Custom backends should use the same helpers, so they behave the same
way:

| Helper | Use |
| ---------------------------------- | --------------------------------------------------------------------------- |
| `computeFailedWorkflowRunUpdate` | Decide whether a failed run is retried, and when, or fails |
| `prepareWorkflowRerun` | Validate a rerun and build the new run's parameters from the source run |
| `resolveCancelWorkflowRunConflict` | Choose the result when a cancel matched no row (already canceled, or final) |
| `nextScheduleOccurrence` | Compute a schedule's next run time from its cron and timezone |
| `isTerminalStatus` | Check whether a run status is final |
| `decodeListCursor` | Decode the `after` / `before` cursor of a list call |
| `buildPaginatedResponse` | Build a list response, with cursors, from `limit + 1` fetched rows |
| `toWorkflowRunCounts` | Turn `GROUP BY "status"` rows into counts for every status |
| `requireRow` | Throw `Failed to <operation>` when a mutation matched no row |
| `wrapError` | Wrap a driver error with a message, keeping the cause |

## Testing Your Backend

`openworkflow/testing` exports the test suite that the built-in backends run.
It requires [Vitest](https://vitest.dev):

```ts
import { MyBackend } from "./my-backend";
import { randomUUID } from "node:crypto";
import { testBackend } from "openworkflow/testing";

testBackend({
setup: async () => new MyBackend({ namespaceId: randomUUID() }),
teardown: async (backend) => {
await backend.stop();
},
});
```

Each `setup()` should return a backend on its own namespace, so tests don't see
each other's runs. Run the suite against each OpenWorkflow version you support.
6 changes: 5 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 34 additions & 0 deletions packages/openworkflow/backend.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Backend contract and the helpers the built-in backends share, for authors of
// custom backends. Experimental: may change in a minor release before 1.0.

// contract
export * from "./core/backend.js";
export type { SerializedError } from "./core/error.js";
export type { JsonValue } from "./core/json.js";
export type {
WorkflowSchedule,
WorkflowScheduleDefinition,
} from "./core/schedule.js";
export type {
StepAttempt,
StepAttemptContext,
StepAttemptStatus,
StepKind,
} from "./core/step-attempt.js";
export type { WorkflowRun, WorkflowRunStatus } from "./core/workflow-run.js";

// helpers
export type { Cursor } from "./core/cursor.js";
export {
buildPaginatedResponse,
DEFAULT_PAGINATION_PAGE_SIZE,
decodeListCursor,
} from "./core/cursor.js";
export { requireRow, wrapError } from "./core/error.js";
export { prepareWorkflowRerun } from "./core/rerun.js";
export { nextScheduleOccurrence } from "./core/schedule.js";
export { computeFailedWorkflowRunUpdate } from "./core/workflow-definition.js";
export {
isTerminalStatus,
resolveCancelWorkflowRunConflict,
} from "./core/workflow-run.js";
16 changes: 15 additions & 1 deletion packages/openworkflow/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./backend": {
"types": "./dist/backend.d.ts",
"default": "./dist/backend.js"
},
"./internal": {
"types": "./dist/internal.d.ts",
"default": "./dist/internal.js"
Expand All @@ -41,12 +45,18 @@
"./sqlite": {
"types": "./dist/sqlite.d.ts",
"default": "./dist/sqlite.js"
},
"./testing": {
"types": "./dist/testing.d.ts",
"default": "./dist/testing.js"
}
},
"files": [
"dist",
"!*.test.*",
"!*.testsuite.*",
"dist/testing/backend.testsuite.*",
"dist/testing/schedules.testsuite.*",
"!*.tsbuildinfo",
"!vitest.global-setup.*"
],
Expand All @@ -69,14 +79,18 @@
},
"peerDependencies": {
"@opentelemetry/api": "1",
"postgres": "^3.4.9"
"postgres": "^3.4.9",
"vitest": "^5.0.0"
},
"peerDependenciesMeta": {
"@opentelemetry/api": {
"optional": true
},
"postgres": {
"optional": true
},
"vitest": {
"optional": true
}
},
"engines": {
Expand Down
2 changes: 2 additions & 0 deletions packages/openworkflow/testing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export type { TestBackendOptions } from "./testing/backend.testsuite.js";
export { testBackend } from "./testing/backend.testsuite.js";
Loading