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.
The contract
Section titled “The contract”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.
- Intent. The workflow says what it needs:
await conditional(signalId, { role, metadata, timeout }). - Escalation. Long Tail writes the intent as a row that is searchable, claimable, deadlined and role-gated.
- 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,
falseon timeout ornullon 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});Glossary
Section titled “Glossary”| 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. |
Where data lives on a row
Section titled “Where data lives on a row”| 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.
The lifecycle of one row
Section titled “The lifecycle of one row”A row has four stored statuses: pending, resolved, expired and cancelled. Claimed is a state of a pending row, read from the claim columns.
- Pending, available.
conditionalwrites 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. Asystem.escalation.{role}.{id}.createdevent fires. - Pending, claimed. A member claims it for a duration.
assigned_toandassigned_untilare set. The dashboard form unlocks for the holder, and claiming again extends the window. - 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.
- 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 receivesT. - Expired. The
timeoutfires before anyone resolves. The row becomesexpiredand the workflow receivesfalse. A late resolve answers409. - Cancelled. Someone cancels the row, or the workflow is terminated. The row becomes
cancelledand the workflow receivesnull.
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.
One row, many resolvers
Section titled “One row, many resolvers”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 |
