Skip to content

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.

Terminal window
npm install @hotmeshio/long-tail-dbos @dbos-inc/dbos-sdk

The package works with the DBOS TypeScript SDK 5.5. The Long Tail process also installs @hotmeshio/long-tail.

  1. 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());
  2. 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.

  3. Point your app at Long Tail, then launch DBOS. Import the package before DBOS.launch(): it registers the lt.open and lt.close steps, 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();
  4. 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',
    });
  5. 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.

conditional<T>(key, ask) returns the resolver’s payload, typed as T, or null when the escalation expired or was cancelled.

FieldMeaning
roleThe queue. Required
typeThe kind of work. Required
subtypeA finer category
descriptionThe line people read in the queue
priority1 (critical) to 4 (low)
metadataIndexed facets, validated against the role’s metadata schema
dataContext shown to the resolver
timeoutThe SLA: '30s', '15m', '48h', '7d', '2w' or milliseconds
maxWaitSecondsThe 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 rewindWorkflow that re-runs the wait reopens it, and Long Tail delivers the outcome again.
  • timeout becomes the escalation’s expiry. Long Tail expires it on time and delivers expired; conditional() returns null.
  • conditional() sets its own recv deadline: the SLA plus ten minutes, so Long Tail’s expiry arrives first.
  • With no SLA, maxWaitSeconds is the deadline. When it passes before any outcome, the lt.close step cancels the escalation and conditional() returns null.

When an escalation reaches a terminal state, by any path, Long Tail’s relay delivers it:

  1. It checks that the workflow is still waiting (DBOSClient.getWorkflow). A finished or missing workflow is recorded as gone.
  2. It calls DBOSClient.send(workflowId, outcome, waitKey, escalationId). The escalation id is the DBOS idempotency key, so a retried delivery stores one message.
  3. 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.

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.

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, or executorID in DBOS.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.patch for code changes. DBOS recovers a workflow only on a process running the same application version (DBOS__APPVERSION, or applicationVersion). Pin it. When a workflow’s code path changes, gate the new branch with DBOS.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 with DBOS.resumeWorkflow(id) from a running replica.
  • Memory per parked wait. A workflow parked in recv is a promise in the app process. Size the app for the number of open escalations it holds.

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.

startLongTailDbos() from @hotmeshio/long-tail-dbos starts Long Tail with the DBOS provider from environment variables:

VariableMeaning
DATABASE_URLLong Tail’s database (or POSTGRES_HOST, POSTGRES_PORT, POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB)
DBOS_SYSTEM_DATABASE_URLThe DBOS system database your app uses. Defaults to DATABASE_URL
PORT, JWT_SECRETLong Tail’s HTTP port and token secret
LT_ADMIN_USER, LT_ADMIN_PASSWORD, LT_ADMIN_EMAILThe first superadmin
LT_ROLESComma-separated roles to create, such as reviewer,finance
LT_START_CONFIGA JSON file with further start() settings, such as role titles or SSO