Install
Add Long Tail to a Node.js project you already have. One start() call runs migrations, starts your workers and serves the dashboard and API on port 3000. Postgres is the only required dependency.
Install the package
Section titled “Install the package”npm install @hotmeshio/long-tailThe package brings @hotmeshio/hotmesh, the durable engine your workflows import Durable from, and the ltc CLI.
Point it at Postgres
Section titled “Point it at Postgres”Long Tail keeps workflow state, escalations, roles and configuration in Postgres. Use Postgres 15 or later.
docker run -d --name lt-postgres -p 5432:5432 \ -e POSTGRES_PASSWORD=password -e POSTGRES_DB=mydb postgres:16Start it
Section titled “Start it”-
Write a workflow. Any async function that takes an envelope works.
src/workflows/hello.ts import type { LTEnvelope } from '@hotmeshio/long-tail';export async function hello(envelope: LTEnvelope) {return { data: { greeting: `Hello, ${envelope.data.name}` } };} -
Call
start()with a database and your workers.src/index.ts import { start } from '@hotmeshio/long-tail';import { hello } from './workflows/hello';async function main() {const lt = await start({database: { connectionString: process.env.DATABASE_URL },seed: { admin: { externalId: 'admin', password: process.env.ADMIN_PASSWORD } },workers: [{ taskQueue: 'default', workflow: hello, config: { invocable: true, description: 'Say hello' } },],});process.on('SIGTERM', () => lt.shutdown());}main(); -
Run it.
Terminal window DATABASE_URL=postgres://postgres:password@localhost:5432/mydb \ADMIN_PASSWORD='choose-a-password' \JWT_SECRET='a-long-random-string' \npx tsx src/index.ts -
Open http://localhost:3000 and sign in as
adminwith the password you chose.helloappears under Invoke Tool.
start() runs migrations first, so a fresh database is ready on the first boot. Migrations and the configuration pass each hold a Postgres advisory lock, so several replicas can boot against one database at once.
What start() returns
Section titled “What start() returns”| Field | Use |
|---|---|
client |
The durable client for starting workflows and reading their state |
shutdown() |
Stops workers, the server and adapters |
adminUserId |
The seeded admin’s id, when seed.admin is set. Pass it to createClient({ auth: { userId } }) |
Options
Section titled “Options”Only database is required.
| Option | What it sets |
|---|---|
database |
Postgres connection: connectionString, or the individual fields, plus ssl |
server |
{ enabled, port }. Enabled on port 3000 by default (PORT overrides). enabled: false runs without HTTP |
workers |
{ taskQueue, workflow, config?, connection? }[]. config registers the workflow: invocable, invocationRoles, description, inputSchema and more |
roles |
Declared roles with titles, versioned form_schema, list_schema, enforce_schema and ops dials |
seed |
seed.admin creates a superadmin on first boot (externalId, displayName, email, password) |
configSource |
'db' (default) seeds declarations once; 'code' compares and applies them on every boot |
auth |
secret for the built-in JWT adapter (defaults to JWT_SECRET), a custom adapter, oauth, sso, oauthServer |
mcp |
Built-in MCP server, serverFactories for your own tools, exposure rules, the domain dictionary |
agents, topics |
Declared agents (schedules, subscriptions) and the event topic catalog |
events |
NATS settings or custom event adapters |
telemetry |
Honeycomb settings or a custom OpenTelemetry adapter |
logging |
pino options or a custom logger adapter |
maintenance |
true for the weekly cleanup, or a custom schedule |
escalation |
Resolution strategy for re-run escalations: 'default', 'mcp' or a custom adapter |
machines |
MQTT gateways that let machines resolve rows on machine roles |
scanSchemes, graphWorkflows |
Declared scan schemes and YAML graph workflows |
features, search, display, branding |
Dashboard flags, the header search bar, time zone, app name and themes |
examples |
true loads the built-in example workflows and seeds sample data |
A fuller configuration:
const lt = await start({ database: { connectionString: process.env.DATABASE_URL }, workers: [{ taskQueue: 'default', workflow: reviewContent }], seed: { admin: { externalId: 'admin', password: process.env.ADMIN_PASSWORD } }, mcp: { server: { enabled: true }, serverFactories: { 'my-tools': createMyToolsServer } }, auth: { secret: process.env.JWT_SECRET }, telemetry: { honeycomb: { apiKey: process.env.HNY } }, logging: { pino: { level: 'info' } }, maintenance: true,});Embed in an existing app
Section titled “Embed in an existing app”Turn the HTTP server off and call the SDK in process. Every SDK call returns an LTApiResult with the same status codes, validation and RBAC as the REST API.
import { start, createClient } from '@hotmeshio/long-tail';
const instance = await start({ database: { connectionString: process.env.DATABASE_URL }, server: { enabled: false }, seed: { admin: { externalId: 'system' } }, workers: [{ taskQueue: 'default', workflow: reviewContent }],});
const lt = createClient({ auth: { userId: instance.adminUserId } });const pending = await lt.escalations.list({ status: 'pending' });Mount the dashboard and API inside your Express app at a subpath:
import { LTExpressAdapter } from '@hotmeshio/long-tail';
const adapter = new LTExpressAdapter();adapter.setBasePath('/admin/longtail');app.use('/admin/longtail', adapter.getRouter());The same codebase also runs as a worker-only process (server: { enabled: false } with workers). All modes share one Postgres database and scale independently. See Run modes.
