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).
createClient
Section titled “createClient”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 |
Auth context
Section titled “Auth context”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 callawait 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 |
LTApiResult
Section titled “LTApiResult”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.
Embedded and remote use
Section titled “Embedded and remote use”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.
@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.
Events
Section titled “Events”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 queueconst unsub = lt.events.on('system.escalation.reviewer.*.claimed', (event) => { console.log('claimed:', event.escalationId);});
// Every task eventlt.events.on('system.task.>', (event) => { metrics.increment(event.type);});
// Every eventlt.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.
Namespaces
Section titled “Namespaces”| 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' });