Skip to content

Concepts

Long Tail turns everything a workflow waits on into a row. Learn the dozen terms below and you can read any Long Tail workflow, form or API call.

A durable workflow owns causality: run this, then that, retry, wait. Long Tail owns the uncertainty: the person, machine, agent or system the workflow waits on but does not control.

  1. Intent. The workflow says what it needs: await conditional(signalId, { role, metadata, timeout }).
  2. Escalation. Long Tail writes the intent as a row that is searchable, claimable, deadlined and role-gated.
  3. Resolution. A person (dashboard), a service (HTTP, SDK), an operator (CLI), an agent (MCP) or a machine (MQTT) resolves the row. The workflow resumes with the typed answer, false on timeout or null on cancel.
const decision = await conditional<{ approved: boolean }>(`review-${workflowId}`, {
role: 'reviewer', // who may act
metadata: { orderId }, // what it is about (indexed)
timeout: '24h', // how long it may wait
schemaVersion: 1, // which form edition renders
});
Term What it is
Workflow An async function that takes an envelope. Its steps are checkpointed in Postgres, so it survives crashes and restarts.
Activity A function the workflow calls through proxyActivities. Side effects live here. Its result is stored once and replayed.
Intent The config passed to conditional: role, type, priority, description, metadata, envelope, timeout.
Escalation The row the intent becomes, one per wait. It lives in lt_escalations.
Role The queue an escalation belongs to, and the RBAC wall around it. Only members see, claim and resolve its rows.
Form schema A JSON Schema stored on the role. The dashboard renders it as the resolve form and the server can enforce it on every surface.
Schema version Every change to a role’s form is an immutable snapshot. schemaVersion pins a row to one snapshot.
Metadata (facets) GIN-indexed key-value pairs on the row (orderId, serialNumber). Search, filter, claim and resolve by them.
Envelope Unindexed context for the form and the resolver: the content to review, the analysis, defaults.
Signal key The signal id the workflow waits on, stored on the row. Resolving by signal key resumes the workflow without looking up the row id.
Claim A time-boxed work lock: assigned_to names the holder, assigned_until bounds the window.
Resolution The payload that settles the row and resumes the workflow, plus an optional metadata patch recording the outcome.
Timeout The row’s deadline. When it passes first, the row expires and the wait returns false.
Cancel A terminal settlement. The wait returns null, and the workflow moves to a resting state it chooses.
Field Indexed Holds
role, type, subtype, priority, status Yes Routing, ordering and lifecycle
metadata Yes (GIN) Facets you query with containment (metadata @> '{"orderId":"ORD-42"}'), plus reserved keys such as schema_version and resolved_by
envelope No Context the form reads
resolver_payload No The answer, stored as submitted
signal_key Unique The resume key

Put anything you search, filter or claim by in metadata. Put anything only the form reads in envelope.

A row has four stored statuses: pending, resolved, expired and cancelled. Claimed is a state of a pending row, read from the claim columns.

  1. Pending, available. conditional writes the row in the same commit that checkpoints the workflow. The row appears in its role’s queue, ordered by priority (lower numbers first) and age. A system.escalation.{role}.{id}.created event fires.
  2. Pending, claimed. A member claims it for a duration. assigned_to and assigned_until are set. The dashboard form unlocks for the holder, and claiming again extends the window.
  3. Pending, available again. If the claim window lapses, the row returns to the queue with no status change. Anyone in the role can claim it. The previous holder must claim again before resolving.
  4. Resolved. A resolver submits a payload. In one statement the row becomes resolved, stores the payload, merges any outcome metadata, and wakes the workflow, which receives T.
  5. Expired. The timeout fires before anyone resolves. The row becomes expired and the workflow receives false. A late resolve answers 409.
  6. Cancelled. Someone cancels the row, or the workflow is terminated. The row becomes cancelled and the workflow receives null.
claim resolve
pending ─────────────► claimed ─────────────► resolved (wait returns T)
▲ │ │
│ │ │ window lapses
│ └───────────────────┘
│
├── timeout fires ───────────────────────────► expired (wait returns false)
└── cancel / terminate ──────────────────────► cancelled (wait returns null)

Every terminal status is final. Exactly one of resolve, expire and cancel wins, because each one locks the row and checks that it is still pending.

The same row resolves from every surface, through the same validation and RBAC:

Surface Resolve call
Dashboard Claim, fill the form, submit
HTTP POST /api/escalations/:id/resolve, /resolve-by-signal-key, /resolve-by-metadata
SDK lt.escalations.resolve, resolveBySignalKey, resolveByMetadata
CLI ltc esc resolve <id> --data '{...}', ltc esc resolve-by-meta <key> <value> --data '{...}'
MCP resolve_escalation, claim_and_resolve, resolve_by_signal_key
MQTT A machine’s gateway resolves rows on machine roles