Skip to content

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.

Terminal window
npm install @hotmeshio/long-tail

The package brings @hotmeshio/hotmesh, the durable engine your workflows import Durable from, and the ltc CLI.

Long Tail keeps workflow state, escalations, roles and configuration in Postgres. Use Postgres 15 or later.

Terminal window
docker run -d --name lt-postgres -p 5432:5432 \
-e POSTGRES_PASSWORD=password -e POSTGRES_DB=mydb postgres:16
  1. 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}` } };
    }
  2. 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();
  3. 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
  4. Open http://localhost:3000 and sign in as admin with the password you chose. hello appears 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.

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 } })

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:

src/index.ts
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,
});

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.

src/longtail.ts
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.