Temporal
Long Tail runs beside a self-hosted Temporal cluster. A Temporal workflow calls conditional() where it needs a person, Long Tail puts the work in the role’s queue, and the answer comes back as the Temporal signal lt.outcome. Temporal keeps owning time, retries, history, task queues and workers.
npm install @hotmeshio/long-tail-temporal @temporalio/workerThe package works with the Temporal TypeScript SDK 1.25. The Long Tail process also installs @hotmeshio/long-tail.
Set it up
Section titled “Set it up”-
Run Temporal. Your own Temporal server cluster, as you run it today. Long Tail’s database can share the Postgres server with Temporal’s persistence, on its own database.
-
Run Long Tail with the Temporal provider.
long-tail.ts import { start } from '@hotmeshio/long-tail';import { temporalProvider } from '@hotmeshio/long-tail-temporal/provider';const lt = await start({database: { connectionString: process.env.DATABASE_URL },roles: [{ role: 'reviewer', title: 'Order review' }],durable: {provider: temporalProvider({ address: 'temporal:7233', namespace: 'default' }),consoleUrl: 'http://localhost:8080/namespaces/default/workflows/{workflowId}',},});process.on('SIGTERM', () => lt.shutdown());shutdown()also stops the delivery relay. -
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. -
Register the two Long Tail activities on your worker.
worker.ts import { Worker } from '@temporalio/worker';import { longTailActivities } from '@hotmeshio/long-tail-temporal/worker';import * as activities from './activities';const worker = await Worker.create({taskQueue: 'orders',workflowsPath: require.resolve('./workflows'),activities: {...activities,...longTailActivities({ url: process.env.LT_URL!, apiKey: process.env.LT_API_KEY! }),},});await worker.run(); -
Call
conditional()in the workflow.workflows.ts import { conditional } from '@hotmeshio/long-tail-temporal/workflow';const decision = await conditional<{ approved: boolean }>(`review-${orderId}`, {role: 'reviewer',type: 'order-review',description: `Review order ${orderId}`,priority: 2,metadata: { orderId },timeout: '48h',});
The workflow entry point imports only @temporalio/workflow, so Temporal’s workflow bundler accepts it.
The call
Section titled “The call”conditional<T>(key, ask) returns the resolver’s payload, typed as T, or null when the escalation expired or was cancelled.
| Field | Meaning |
|---|---|
role | The queue. Required |
type | The kind of work. Required |
subtype | A finer category |
description | The line people read in the queue |
priority | 1 (critical) to 4 (low) |
metadata | Indexed facets, validated against the role’s metadata schema |
data | Context shown to the resolver |
timeout | The SLA: '30s', '15m', '48h', '7d', '2w' or milliseconds |
outcomeOf(key) returns the full outcome for a key conditional() already returned: its status (resolved, expired or cancelled), resolvedBy and resolvedAt.
Keys and runs
Section titled “Keys and runs”The key names one wait within one workflow run, and opening the same key again returns the same escalation, so an activity retry reuses it.
- Use a unique key per wait. In a loop, add the round:
`review-${orderId}-${round}`. - Keys are scoped to the run. After continue-as-new, the same key opens a new escalation.
- The relay can deliver an outcome twice on a retry. The workflow keeps the first outcome per key.
SLAs and cancellation
Section titled “SLAs and cancellation”timeoutbecomes the escalation’s expiry, computed from workflow time. Long Tail expires the escalation when it passes, and the workflow receivesnull. Long Tail owns the SLA, so the workflow holds no timer for it.- A malformed
timeoutfails the workflow with a non-retryable error. - When a person cancels the escalation, the workflow receives
null. - When the workflow is cancelled while it waits,
conditional()closes the escalation and rethrows the cancellation. - When a workflow ends some other way (terminated, timed out), Long Tail’s reconcile pass cancels its pending escalations.
Delivery
Section titled “Delivery”Each terminal outcome goes to the run that opened the wait as the signal lt.outcome. Temporal stores signals while no worker is running, so a review resolved overnight is waiting for the workflow in the morning. When the run no longer exists, the delivery is recorded as gone.
Provider options
Section titled “Provider options”| Option | Meaning |
|---|---|
address | The Temporal frontend, such as temporal:7233 |
namespace | The Temporal namespace |
dataConverter | The workers’ data converter and payload codec. Long Tail uses it to decode history for the timeline and encode the outcome signal |
client | An existing Temporal Client; the connection options are then ignored |
longTailActivities({ url, apiKey, timeoutMs }) takes the Long Tail URL, the bot key and a per-request timeout (default 15 seconds).
The dashboard
Section titled “The dashboard”Everything on the escalation side: queues, escalation detail with the form, claims, facets, the pace board, roles, users and bots. Executions (under Workflows) lists Temporal workflows from Temporal’s visibility store, filtered by workflow id prefix, type and state, with each workflow id once as its latest run. A workflow’s page shows its timeline built from Temporal history (activities, timers, child workflows and each wait from open to outcome) and links into the Temporal UI through consoleUrl.
The container
Section titled “The container”startLongTailTemporal() from @hotmeshio/long-tail-temporal starts Long Tail with the Temporal provider from environment variables:
| Variable | Meaning |
|---|---|
TEMPORAL_ADDRESS | The Temporal frontend, such as temporal:7233. Required |
TEMPORAL_NAMESPACE | Default default |
TEMPORAL_UI_URL | The Temporal UI base URL; turns on the dashboard link |
DATABASE_URL | Long Tail’s database (or POSTGRES_HOST, POSTGRES_PORT, POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB) |
PORT, JWT_SECRET | Long Tail’s HTTP port and token secret |
LT_ADMIN_USER, LT_ADMIN_PASSWORD, LT_ADMIN_EMAIL | The first superadmin |
LT_ROLES | Comma-separated roles to declare at boot, such as reviewer |
LT_START_CONFIG | A JSON file with further start() settings, such as role titles or SSO |
Workers in other languages
Section titled “Workers in other languages”The contract is small: POST /api/escalations/waits opens a wait, and the signal lt.outcome carries { waitKey, escalationId, status, data }. A Go, Python or Java worker can implement conditional() against it. See Durable providers.
