Skip to content

SDK

The SDK is the Long Tail API as in-process function calls. Each method runs the same validation, RBAC and event publishing as its HTTP route, without the HTTP transport. Use it when Long Tail runs inside your own Node.js process (Express, NestJS, Next.js or a plain worker).

server.ts
import { start, createClient } from '@hotmeshio/long-tail';
await start({
database: { connectionString: process.env.DATABASE_URL },
server: { enabled: false },
workers: [{ taskQueue: 'default', workflow: reviewOrder }],
});
const lt = createClient({ auth: { userId: 'order-service' } });
const pending = await lt.escalations.list({ status: 'pending', role: 'reviewer' });

createClient(options?) returns a client whose namespaces mirror the REST routes (lt.escalations for /api/escalations, lt.workflows for /api/workflows and so on). Call it after start() in the same process. The client calls the same API layer the HTTP routes use.

Option Type Description
auth LTApiAuth Default auth context bound to every call that takes one
interface LTApiAuth {
userId: string; // the acting account's id
role?: string;
scopes?: string[];
}

The client’s auth binds to every method that takes an auth context. Pass a second argument to override it for one call:

// Uses the client default ('order-service')
await lt.escalations.list({ status: 'pending' });
// Acts as another account for this call
await lt.escalations.claim({ id: 'esc_123' }, { userId: 'user-456' });

Each method page lists its Auth requirement:

Auth Behavior
Required Uses the per-call auth, else the client default. With neither, the call returns { status: 401, error: 'Auth context required' }. RBAC and work-surface scope apply to that account
Optional The auth context is forwarded when present (to record a creator, or to scope what is returned)
Not required The method takes no auth context. The matching HTTP route applies its own gate (for example, admin access) before calling it

Every client method returns an LTApiResult envelope. Failures come back in the envelope rather than as thrown errors, so check status.

interface LTApiResult<T = any> {
status: number; // HTTP-equivalent status code
data?: T; // the payload on 2xx; the structured error body on 422
error?: string; // the message on 4xx and 5xx
code?: string; // machine-readable error code, for example 'schema_validation'
}
const result = await lt.tasks.get({ id: 'task-123' });
if (result.status === 200) {
console.log(result.data);
} else if (result.status === 404) {
console.log('not found');
} else {
console.error(result.error);
}

Status codes follow the REST API:

Status Meaning
200 Success
201 Created
202 Accepted: the work started (workflows.invoke) or is still running (workflows.getResult)
400 Invalid input
401 No auth context for a method that requires one, or invalid credentials
403 The account lacks the role, scope or capability
404 Not found, or not visible to the caller (non-disclosure)
409 Conflict: the row is not in the required state, already claimed, already held, or a duplicate key
413 Payload too large (me.patchPreferences)
422 The payload failed the role’s enforced schema
500 Unexpected server error

A 422 from a resolve method carries code: 'schema_validation' and the full error body in data: { error, code, violations: [{ field, message }], role, schemaVersion }. See Schema enforcement.

The client runs where start() runs. Two common shapes:

Shape Setup
Embedded, no HTTP start({ server: { enabled: false } }), then call createClient(). Your process owns every route.
Embedded, alongside the HTTP server Leave the server on (port 3000 by default) and use the client from your own code in the same process. Dashboard, REST and SDK share one Postgres foundation.

Code in another process reaches the same operations through the HTTP API, the CLI (ltc) or MCP. Each client namespace maps to a route group under /api.

orders.controller.ts
@Controller('workflows')
export class WorkflowController {
private lt = createClient({ auth: { userId: 'nest-service' } });
@Post(':type/invoke')
async invoke(@Param('type') type: string, @Body() body: any) {
return this.lt.workflows.invoke({ type, data: body.data });
}
@Get(':id/status')
async status(@Param('id') id: string) {
return this.lt.workflows.getStatus({ workflowId: id });
}
}

See Express, NestJS and Run modes.

lt.events.on(pattern, callback) subscribes to Long Tail events in-process and returns an unsubscribe function. Patterns use NATS rules: * matches one token, > matches the rest, and a bare * matches every event.

// One verb on one queue
const unsub = lt.events.on('system.escalation.reviewer.*.claimed', (event) => {
console.log('claimed:', event.escalationId);
});
// Every task event
lt.events.on('system.task.>', (event) => {
metrics.increment(event.type);
});
// Every event
lt.events.on('*', (event) => auditLog.append(event));
unsub();

Events fire from the same publish calls that feed Socket.IO and NATS, and the registry publishes to every adapter in parallel, so in-process callbacks and NATS subscribers both receive each event:

await start({
database: { connectionString: process.env.DATABASE_URL },
server: { enabled: false },
events: { nats: { url: 'nats://localhost:4222' } },
});
const lt = createClient({ auth: { userId: 'system' } });
lt.events.on('system.workflow.*.failed', handleFailure);

createClient() registers a CallbackEventAdapter for you. To subscribe without a client, register one directly:

import { eventRegistry, CallbackEventAdapter } from '@hotmeshio/long-tail';
const adapter = new CallbackEventAdapter();
eventRegistry.register(adapter);
await adapter.connect();
const unsub = adapter.on('system.task.*.completed', (event) => { /* ... */ });

See lt.events for subjects and fields.

Namespace Description Methods
lt.escalations Create, search, claim, resolve, fill and analyze escalations 48
lt.agents Event-driven agents and their topic subscriptions 9
lt.auth Username and password login for a JWT 1
lt.botAccounts Bot accounts, their roles and API keys 11
lt.controlplane Namespaces, worker roll call, throttles and stream stats 6
lt.dba On-demand pruning and prune-function deployment 2
lt.events In-process event subscriptions 1
lt.exports Job listings, workflow state and execution trees 5
lt.files Browse, inspect, share and delete stored files 4
lt.insight Natural-language MCP queries and YAML workflow generation 4
lt.knowledge The versioned, domain-scoped knowledge store 9
lt.maintenance The scheduled prune and delete rules 2
lt.mcp MCP server registrations, connections, credentials and tool calls 11
lt.namespaces List and register namespaces 2
lt.personas Role bundles assigned to users 11
lt.pipelines YAML pipeline runs and interrupts (lt.mcpRuns is a deprecated alias) 4
lt.roles Roles, versioned schemas and escalation chains 12
lt.settings Platform settings 1
lt.tasks Task records and processes 6
TopicService The event topic catalog (direct import) 8
lt.users Users, properties and role assignments, plus lt.me preferences 12
lt.workflowSets Plan, build and deploy groups of YAML workflows 6
lt.workflows Invoke durable workflows, read results, manage configuration 13
lt.yamlWorkflows YAML workflow lifecycle, versions and cron 18

TopicService is imported as a namespace rather than read from the client:

import { TopicService } from '@hotmeshio/long-tail';
const { topics, total } = await TopicService.listTopics({ category: 'task' });