DBOS
DBOS is a library over Postgres; Long Tail is a container over Postgres. Together they run on one database: DBOS keeps its system tables in the dbos schema beside Long Tail’s tables. A DBOS workflow calls conditional() where it needs a person, parks in DBOS.recv, and gets the answer back as a typed value.
npm install @hotmeshio/long-tail-dbos @dbos-inc/dbos-sdkThe package works with the DBOS TypeScript SDK 5.5. The Long Tail process also installs @hotmeshio/long-tail.
Set it up
Section titled “Set it up”-
Run Long Tail with the DBOS provider. Point it at the database your app’s DBOS uses.
long-tail.ts import { start } from '@hotmeshio/long-tail';import { dbosProvider } from '@hotmeshio/long-tail-dbos/provider';const lt = await start({database: { connectionString: process.env.DATABASE_URL },roles: [{ role: 'reviewer', title: 'Order review' }],durable: { provider: await dbosProvider({ systemDatabaseUrl: process.env.DATABASE_URL! }) },});process.on('SIGTERM', () => lt.shutdown()); -
Create a bot and its key. Once, in the dashboard: a bot whose roles may create escalations in the roles your workflows use. Its key is
LT_API_KEY. -
Point your app at Long Tail, then launch DBOS. Import the package before
DBOS.launch(): it registers thelt.openandlt.closesteps, and DBOS registers steps before launch.app.ts import { DBOS } from '@dbos-inc/dbos-sdk';import { configureLongTail } from '@hotmeshio/long-tail-dbos';import { reviewOrder } from './workflows';configureLongTail({ url: process.env.LT_URL!, apiKey: process.env.LT_API_KEY! });DBOS.setConfig({ name: 'orders', systemDatabaseUrl: process.env.DATABASE_URL });await DBOS.launch(); -
Call
conditional()in the workflow.workflows.ts import { conditional } from '@hotmeshio/long-tail-dbos';const decision = await conditional<{ approved: boolean }>(`review-${orderId}`, {role: 'reviewer',type: 'order-review',description: `Review order ${orderId}`,priority: 2,metadata: { orderId },timeout: '48h',}); -
Start and stop workflows as usual.
DBOS.startWorkflow(reviewOrder, { workflowID })(orderId)starts one;await DBOS.shutdown()stops the app.
conditional() runs in the workflow body. Calling it inside a step throws.
The call
Section titled “The call”conditional<T>(key, ask) returns the resolver’s payload, typed as T, or null when the escalation expired or was cancelled.
| Field | Meaning |
|---|---|
role | The queue. Required |
type | The kind of work. Required |
subtype | A finer category |
description | The line people read in the queue |
priority | 1 (critical) to 4 (low) |
metadata | Indexed facets, validated against the role’s metadata schema |
data | Context shown to the resolver |
timeout | The SLA: '30s', '15m', '48h', '7d', '2w' or milliseconds |
maxWaitSeconds | The recv deadline for a wait with no SLA. Default 365 days |
Long Tail opens a wait once per workflow id and key, so a retried lt.open step reuses it.
- A loop that asks again uses a new key per round:
`review-${orderId}-${round}`. - A forked workflow has a new id, so it opens new waits.
- A
rewindWorkflowthat re-runs the wait reopens it, and Long Tail delivers the outcome again.
SLAs and the recv deadline
Section titled “SLAs and the recv deadline”timeoutbecomes the escalation’s expiry. Long Tail expires it on time and deliversexpired;conditional()returnsnull.conditional()sets its ownrecvdeadline: the SLA plus ten minutes, so Long Tail’s expiry arrives first.- With no SLA,
maxWaitSecondsis the deadline. When it passes before any outcome, thelt.closestep cancels the escalation andconditional()returnsnull.
Delivery
Section titled “Delivery”When an escalation reaches a terminal state, by any path, Long Tail’s relay delivers it:
- It checks that the workflow is still waiting (
DBOSClient.getWorkflow). A finished or missing workflow is recorded as gone. - It calls
DBOSClient.send(workflowId, outcome, waitKey, escalationId). The escalation id is the DBOS idempotency key, so a retried delivery stores one message. - It records the delivery. A failed send retries with capped backoff, and a lease lets one Long Tail replica deliver each wait.
If the app is down when someone resolves, the message waits in the DBOS system database. When the app restarts and recovers the workflow, recv reads it.
One database or two
Section titled “One database or two”By default DBOS keeps its system tables in Long Tail’s database, in the dbos schema. To keep them apart, give DBOS its own database and pass that URL as systemDatabaseUrl. dbosProvider also takes systemDatabaseSchemaName when the DBOS tables live in another schema.
Recovery on your own servers
Section titled “Recovery on your own servers”Self-hosted DBOS recovers workflows with the DBOS library and your Postgres, provided each replica keeps a stable identity.
- A stable executor id per replica. DBOS tags each workflow with the executor that runs it (
DBOS__VMID, orexecutorIDinDBOS.setConfig). On launch, a process recovers the pending workflows tagged with its own id. Give every replica a fixed id, such as the compose service name or a StatefulSet ordinal. - A pinned application version, and
DBOS.patchfor code changes. DBOS recovers a workflow only on a process running the same application version (DBOS__APPVERSION, orapplicationVersion). Pin it. When a workflow’s code path changes, gate the new branch withDBOS.patch('name'): waits parked under the old code replay the old path, and new runs take the new one. - Retiring a replica. Find its workflows with
DBOS.listWorkflows({ status: 'PENDING', executorId: 'replica-a' })and adopt each withDBOS.resumeWorkflow(id)from a running replica. - Memory per parked wait. A workflow parked in
recvis a promise in the app process. Size the app for the number of open escalations it holds.
The dashboard
Section titled “The dashboard”Everything on the escalation side: queues, claims, forms, history, analytics, roles and users. Executions (under Workflows) lists DBOS workflows newest first, filtered by workflow id prefix, type and state. Running covers PENDING, ENQUEUED, DELAYED and SUSPENDED; failed covers ERROR, CANCELLED and MAX_RECOVERY_ATTEMPTS_EXCEEDED.
A workflow’s page builds its timeline from DBOS’s own records: steps as activities, each wait as a span from lt.open to the recv that read the outcome, timers, child workflows and failures. DBOS records step outputs, so the timeline shows each step’s output and the workflow’s input.
The container
Section titled “The container”startLongTailDbos() from @hotmeshio/long-tail-dbos starts Long Tail with the DBOS provider from environment variables:
| Variable | Meaning |
|---|---|
DATABASE_URL | Long Tail’s database (or POSTGRES_HOST, POSTGRES_PORT, POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB) |
DBOS_SYSTEM_DATABASE_URL | The DBOS system database your app uses. Defaults to DATABASE_URL |
PORT, JWT_SECRET | Long Tail’s HTTP port and token secret |
LT_ADMIN_USER, LT_ADMIN_PASSWORD, LT_ADMIN_EMAIL | The first superadmin |
LT_ROLES | Comma-separated roles to create, such as reviewer,finance |
LT_START_CONFIG | A JSON file with further start() settings, such as role titles or SSO |
