Skip to content

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.

Terminal window
npm install @hotmeshio/long-tail-temporal @temporalio/worker

The package works with the Temporal TypeScript SDK 1.25. The Long Tail process also installs @hotmeshio/long-tail.

  1. 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.

  2. 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.

  3. 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.

  4. 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();
  5. 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.

conditional<T>(key, ask) returns the resolver’s payload, typed as T, or null when the escalation expired or was cancelled.

FieldMeaning
roleThe queue. Required
typeThe kind of work. Required
subtypeA finer category
descriptionThe line people read in the queue
priority1 (critical) to 4 (low)
metadataIndexed facets, validated against the role’s metadata schema
dataContext shown to the resolver
timeoutThe 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.

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.
  • timeout becomes the escalation’s expiry, computed from workflow time. Long Tail expires the escalation when it passes, and the workflow receives null. Long Tail owns the SLA, so the workflow holds no timer for it.
  • A malformed timeout fails 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.

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.

OptionMeaning
addressThe Temporal frontend, such as temporal:7233
namespaceThe Temporal namespace
dataConverterThe workers’ data converter and payload codec. Long Tail uses it to decode history for the timeline and encode the outcome signal
clientAn 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).

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.

startLongTailTemporal() from @hotmeshio/long-tail-temporal starts Long Tail with the Temporal provider from environment variables:

VariableMeaning
TEMPORAL_ADDRESSThe Temporal frontend, such as temporal:7233. Required
TEMPORAL_NAMESPACEDefault default
TEMPORAL_UI_URLThe Temporal UI base URL; turns on the dashboard link
DATABASE_URLLong Tail’s database (or POSTGRES_HOST, POSTGRES_PORT, POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB)
PORT, JWT_SECRETLong Tail’s HTTP port and token secret
LT_ADMIN_USER, LT_ADMIN_PASSWORD, LT_ADMIN_EMAILThe first superadmin
LT_ROLESComma-separated roles to declare at boot, such as reviewer
LT_START_CONFIGA JSON file with further start() settings, such as role titles or SSO

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.