lt.escalations
lt.escalations is the full escalation surface: create rows, search queues, claim and resolve work, fill batches and accumulators, route by facets and read analytics. Every method returns an LTApiResult and mirrors a route under /api/escalations (see the HTTP reference).
Work-surface scope
Section titled “Work-surface scope”Escalation methods enforce the caller’s role work-surface scope on the server. The method signatures stay the same. For a member:
| Axis | Values | Governs |
|---|---|---|
read_scope |
self, all (default all) |
Search: list, listAvailable, findByMetadata, getStats and a single get return only the rows the member may see. self limits this to rows assigned to the member. |
write_scope |
none, self, all (default all) |
Claim, resolve and cancel. With write_scope=self a member may only claim, resolve and cancel rows already assigned to them. release, escalate and create are queue-management verbs and require write_scope=all. |
admin and superadmin ignore scope and act on the whole queue. Scope is set when a role is assigned, through lt.users.addRole. See Identity for the member profiles.
create
Section titled “create”const result = await lt.escalations.create({ type: 'approval', role: 'reviewer', description: 'Review deployment to production', metadata: { form_schema: { properties: { approved: { type: 'boolean', default: false, description: 'Approve?' }, environment: { type: 'string', enum: ['staging', 'production'] }, api_key: { type: 'string', format: 'password', description: 'Deploy key' }, }, }, },});Create a standalone escalation. The caller must hold the target role or be a superadmin.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
type |
string |
Yes | Escalation category |
role |
string |
Yes | Target role (the queue) |
subtype |
string |
No | Subcategory (defaults to type) |
description |
string |
No | Human-readable reason |
priority |
number |
No | 1 (highest) to 4 (lowest), default 2 |
envelope |
string |
No | JSON-serialized workflow envelope |
metadata |
object |
No | Arbitrary metadata. Include form_schema for a typed resolver form |
escalation_payload |
string |
No | JSON context data shown to the reviewer |
Returns: LTApiResult<Escalation> with status 201.
Auth: Required (RBAC enforced).
const result = await lt.escalations.list({ status: 'pending', role: 'reviewer', limit: 25,});
// Faceted query: the filter and the count run in SQL, role-scoped.const faceted = await lt.escalations.list({ status: 'pending', facets: { flags: 'too_short' }, range: [{ facet: 'confidence', op: '<=', value: 0.7 }], block: [{ outcome: 'success' }], exists: ['needsReview'], orderBy: [{ field: 'metadata.confidence', numeric: true, direction: 'asc' }], limit: 50,});List escalations with optional filters, scoped to the caller’s roles. A facet is a key and value inside the row’s metadata JSONB. When any faceted element (facets, block, range, exists, roles, available, orderBy) is present, the request runs through the scoped faceted query; otherwise the simple list path runs. See Faceted routing.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
status |
string |
No | pending, resolved, cancelled or expired |
role |
string |
No | Filter by role |
roles |
string[] |
No | Restrict to these roles (role = ANY). Narrows within the caller’s scope and never widens it |
type |
string |
No | Filter by type |
subtype |
string |
No | Filter by subtype |
assigned_to |
string |
No | Filter by assigned user ID |
priority |
number |
No | Filter by priority (1 to 4) |
facets |
Record<string, any> |
No | Required metadata facets: metadata @> facets (AND, GIN-served). For nested values and arrays this is JSONB containment |
block |
Record<string, any>[] |
No | Exclude rows whose metadata contains any of these facet sets |
range |
{ facet, op, value }[] |
No | Numeric range over a metadata facet |
exists |
string[] |
No | Metadata keys that must be present (metadata ? key) |
available |
boolean |
No | true returns unclaimed or expired-claim rows only; false returns rows held now |
search |
string |
No | Exact match on a correlation id: escalation id, workflow id or origin id. To match a value inside metadata, use facets |
groupBy |
string |
No | A metadata key. Pages by groups of rows sharing it; the response adds groups: [{ key, ids }] and total counts groups |
limit |
number |
No | Max results (default 50) |
offset |
number |
No | Pagination offset |
sort_by |
string |
No | Column to sort by (for example created_at, priority) |
order |
string |
No | asc or desc |
orderBy |
{ field, direction?, numeric? }[] |
No | Multi-key sort over columns or a metadata path written metadata.<key> |
Returns: LTApiResult<{ escalations, total }>
Auth: Required.
listAvailable
Section titled “listAvailable”const result = await lt.escalations.listAvailable({ role: 'reviewer', limit: 10,});List escalations available to claim: pending and not actively claimed. Also accepts the faceted parameters of list (facets, block, range, exists, roles, orderBy, groupBy), pinned to the available pool.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
role |
string |
No | Filter by role |
type |
string |
No | Filter by type |
subtype |
string |
No | Filter by subtype |
priority |
number |
No | Filter by priority (1 to 4) |
limit |
number |
No | Max results (default 50) |
offset |
number |
No | Pagination offset |
sort_by |
string |
No | Column to sort by |
order |
string |
No | asc or desc |
Returns: LTApiResult<{ escalations, total }>
Auth: Required.
listTypes
Section titled “listTypes”const result = await lt.escalations.listTypes();List every distinct escalation type value.
Parameters: none.
Returns: LTApiResult<{ types: string[] }>
Auth: Not required.
listFacetKeys
Section titled “listFacetKeys”const result = await lt.escalations.listFacetKeys({});// result.data.keys → ['orderId', 'serialNumber', ...]List the metadata keys present on the escalations the caller can read. Powers key autocomplete in faceted queries: only keys that exist in the caller’s role-scoped rows are returned.
Parameters: none (pass {}).
Returns: LTApiResult<{ keys: string[] }>
Auth: Required.
getStats
Section titled “getStats”const result = await lt.escalations.getStats({ period: '24h' });Aggregate escalation statistics scoped to the caller’s roles.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
period |
string |
No | Time window: 1h, 24h, 7d, 30d |
Returns: LTApiResult<{ pending, claimed, created, resolved, by_role, by_type }>
Auth: Required.
getStationMetrics
Section titled “getStationMetrics”const result = await lt.escalations.getStationMetrics({ period: '24h' });Per-role throughput and latency for every station visible to the caller. The Operations view uses it for the pace chart and station table.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
period |
'15m' | '1h' | '24h' | '7d' | '30d' |
No | Lookback window for resolved and percentile stats (default '24h') |
Returns: LTApiResult<{ stations: StationMetric[] }>
interface StationMetricPeriod { p99: number | null; // minutes p50: number | null; avg: number | null; max: number | null;}
interface StationMetric { role: string; pending: number; // queued now: pending and not claimed claimed: number; // claimed with a live assignment resolved: number; // resolved within the lookback period priority_count: number; // pending unclaimed rows past the role's age threshold throughput_pct: number | null; // resolved / (target_per_hour × hours) × 100 wait: StationMetricPeriod; // queue time: created_at → claimed_at work: StationMetricPeriod; // processing time: claimed_at → resolved_at}pending is the live queue depth regardless of period. claimed, resolved and both percentile objects use the lookback window. priority_count counts pending, unclaimed rows whose age exceeds the role’s threshold: age runs from the priority_facet metadata timestamp (created_at when unset) against priority_threshold_minutes (sla_minutes when unset). A station with neither threshold returns 0. throughput_pct is null when target_per_hour is not set.
Auth: Required. Same RBAC as getStats: callers see only the stations for roles they hold.
const result = await lt.escalations.get({ id: 'esc_123' });Get one escalation by ID.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
id |
string |
Yes | Escalation UUID |
Returns: LTApiResult<Escalation>. 403 if the caller lacks the role, 404 if not found.
Auth: Required.
getLookups
Section titled “getLookups”const result = await lt.escalations.getLookups({ id: 'esc_123' });// result.data.lookups → [{ domain, key, version, as?, data, missing? }]Resolve the versioned knowledge lookups pinned on an escalation (envelope.lookups). Enforces the same read scope as get. The refs on the row grant exactly the pinned editions they name. See Lookups.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
id |
string |
Yes | Escalation UUID |
Returns: LTApiResult<{ lookups }>. Each entry is { domain, key, version, as?, data, missing? }.
Auth: Required.
getByWorkflowId
Section titled “getByWorkflowId”const result = await lt.escalations.getByWorkflowId({ workflowId: 'wf_abc' });const mine = await lt.escalations.getByWorkflowId({ workflowId: 'wf_abc' }, { userId: 'user-456' });List the escalations for a workflow. With an auth context as the second argument, only the escalations that caller may read are returned; without it, every escalation.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
workflowId |
string |
Yes | Workflow ID |
Returns: LTApiResult<{ escalations }>
Auth: Optional. Filters to the caller’s readable escalations when provided.
const result = await lt.escalations.claim({ id: 'esc_123', durationMinutes: 60,});Claim a pending escalation for the caller. A claim sets assigned_to and a deadline.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
id |
string |
Yes | Escalation UUID |
durationMinutes |
number |
No | Claim duration (default 30) |
Returns: LTApiResult<{ escalation, isExtension }>. 403 if the caller lacks the role, 404 if not found, 409 if not available.
Auth: Required.
release
Section titled “release”const result = await lt.escalations.release({ id: 'esc_123' });Release a claimed escalation back to the pool.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
id |
string |
Yes | Escalation UUID |
Returns: LTApiResult<{ escalation }>. 409 if the caller does not hold the claim.
Auth: Required.
resolve
Section titled “resolve”const result = await lt.escalations.resolve({ id: 'esc_123', resolverPayload: { approved: true, comment: 'Looks good' }, metadata: { outcome: 'approved', reviewedBy: 'alice', durationMs: 1_240 },});Resolve a pending escalation. A row created by a conditional wait resolves and wakes the workflow in one guarded transaction; the workflow resumes with resolverPayload. Older rows resolve through signal routing or a re-run that injects the resolver data. Password-format fields are replaced with ephemeral tokens.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
id |
string |
Yes | Escalation UUID |
resolverPayload |
Record<string, any> |
Yes | The decision. Delivered to the waiting workflow; not indexed |
metadata |
Record<string, any> |
No | Outcome facets merged into the row’s GIN-indexed metadata. See Recording the outcome |
Returns: LTApiResult<{ signaled, escalationId, workflowId }> (signal path) or LTApiResult<{ started, escalationId, workflowId }> (re-run path). 404 if not found, 409 if not pending, 422 when the role enforces its schema and the payload fails it.
Auth: Required.
resolveBySignalKey
Section titled “resolveBySignalKey”const result = await lt.escalations.resolveBySignalKey({ signalKey: 'approval-order-42', resolverPayload: { approved: true },});Resolve an escalation by its signal_key and resume the waiting workflow in place. Use it when the caller knows the deterministic signal id and wants to skip the id lookup. RBAC-scoped to the escalation’s role.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
signalKey |
string |
Yes | The signal id the workflow waits on |
resolverPayload |
Record<string, any> |
Yes | The decision delivered to the workflow |
metadata |
Record<string, any> |
No | Outcome patch merged into the row’s metadata |
Returns: LTApiResult<{ signaled: true, escalationId, workflowId }>
Auth: Required.
resolveByIds
Section titled “resolveByIds”const result = await lt.escalations.resolveByIds({ ids: ['esc_1', 'esc_2', 'esc_3'], resolverPayload: { stationId: 'st-7' }, metadata: { outcome: 'settled' },});Resolve a set of escalations by id in one guarded statement. This is the set-based sibling of resolve for bookkeeping rows woken collectively, and it delivers no per-row signal. Rows backing a live condition() waiter (signal_key set) stay pending and are left out of the result; settle those with resolve, which carries the wake. A scoped caller may only resolve rows whose role they hold.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
ids |
string[] |
Yes | Escalation ids to resolve as one set |
resolverPayload |
Record<string, any> |
Yes | Payload applied to every row |
metadata |
Record<string, any> |
No | Outcome patch merged into each row |
Returns: LTApiResult<{ resolved: number, escalationIds: string[] }>. Only rows still pending resolve.
Auth: Required.
resolveAllOrNone
Section titled “resolveAllOrNone”const result = await lt.escalations.resolveAllOrNone({ items: [ { id: 'esc_left', resolverPayload: { jobRef: 'job-left', unit: 'left' } }, { id: 'esc_right', resolverPayload: { jobRef: 'job-right', unit: 'right' } }, ], metadata: { outcome: 'dispatched' }, requireClaimed: true,});
if (result.status === 409) { // Nothing resolved. result.data.failed names the blocking rows: // not-found | already-resolved | already-cancelled | already-expired | // assignee-mismatch | unsupported-resolution-path}Resolve every listed escalation with its own payload in one SQL statement, or resolve nothing. Rows backing a live condition() waiter are supported: each waiter’s wake commits with its resolve and delivers that row’s payload as the wait’s return value. Use it when each member of a group must receive a distinct payload and a partial batch is unacceptable.
RBAC matches resolveByIds: per-item write scope, and any missing or out-of-scope id returns 404 with nothing resolved. requireClaimed asserts, inside the same statement, that every row is assigned to the caller.
Rows that resolve through legacy signal routing (metadata.signal_id or metadata.signal_routing) need the single resolve path; including one blocks the batch with reason unsupported-resolution-path. Password-format fields are redacted per row against that row’s own form_schema.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
items |
Array<{ id, resolverPayload }> |
Yes | The batch. Ids must be unique; at most 100 items (LT_ESCALATION_BULK_RESOLVE_MAX) |
metadata |
Record<string, any> |
No | Shared outcome patch merged into every row |
requireClaimed |
boolean |
No | Assert every row is assigned to the caller, inside the atomic statement |
Returns: LTApiResult<{ resolved: number, escalationIds: string[] }> on success. On 409 the data carries { error, failedIds, failed: [{ id, reason }] }, listing only the rows that blocked the batch; the others stay pending.
Auth: Required.
escalate
Section titled “escalate”const result = await lt.escalations.escalate({ id: 'esc_123', targetRole: 'senior-reviewer',});Route a pending escalation to a different role.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
id |
string |
Yes | Escalation UUID |
targetRole |
string |
Yes | Destination role |
Returns: LTApiResult<Escalation>. 403 if not authorized, 404 if not found, 409 if not pending.
Auth: Required.
releaseExpired
Section titled “releaseExpired”const result = await lt.escalations.releaseExpired();Release every claim past its assigned_until deadline.
Parameters: none.
Returns: LTApiResult<{ released: number }>
Auth: Not required.
updatePriority
Section titled “updatePriority”const result = await lt.escalations.updatePriority({ ids: ['esc_1', 'esc_2'], priority: 1,});Set the priority of one or more escalations.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
ids |
string[] |
Yes | Escalation UUIDs |
priority |
number |
Yes | 1 critical, 2 high, 3 medium, 4 low |
Returns: LTApiResult<{ updated: number }>
Auth: Required.
bulkClaim
Section titled “bulkClaim”const result = await lt.escalations.bulkClaim({ ids: ['esc_1', 'esc_2'], durationMinutes: 60,});Claim several escalations at once for the caller.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
ids |
string[] |
Yes | Escalation UUIDs |
durationMinutes |
number |
No | Claim duration (default 30) |
Returns: LTApiResult<{ claimed, skipped }>
Auth: Required.
bulkAssign
Section titled “bulkAssign”const result = await lt.escalations.bulkAssign({ ids: ['esc_1', 'esc_2'], targetUserId: 'user_456', durationMinutes: 60,});Assign several escalations to a specific user.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
ids |
string[] |
Yes | Escalation UUIDs |
targetUserId |
string |
Yes | User to assign to |
durationMinutes |
number |
No | Assignment duration (default 30) |
Returns: LTApiResult<{ assigned, skipped }>
Auth: Required.
bulkUnassign
Section titled “bulkUnassign”const result = await lt.escalations.bulkUnassign({ ids: ['esc_1', 'esc_2'] });Return claimed escalations to the available pool. This is the admin override of someone else’s live claim (a person returning their own claim uses release). One guarded statement; unclaimed and terminal rows are skipped. A released event is published per returned row with the acting admin in the delta.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
ids |
string[] |
Yes | Escalation UUIDs |
Returns: LTApiResult<{ unassigned, skipped }>. 400 for an empty ids, 403 without admin access.
Auth: Required (admin or superadmin).
bulkEscalate
Section titled “bulkEscalate”const result = await lt.escalations.bulkEscalate({ ids: ['esc_1', 'esc_2'], targetRole: 'senior-reviewer',});Route several escalations to a different role.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
ids |
string[] |
Yes | Escalation UUIDs |
targetRole |
string |
Yes | Destination role |
Returns: LTApiResult<{ updated: number }>
Auth: Required.
bulkTriage
Section titled “bulkTriage”const result = await lt.escalations.bulkTriage({ ids: ['esc_1', 'esc_2'], hint: 'Check if the content violates policy section 3.2',});Send several escalations to AI triage. Each escalation resolves and starts a triage workflow that uses MCP tools to analyze the issue and, where it can, resolve it.
Rows backing a live condition() waiter (signal_key set) stay pending and are left out of triaged, because their resolution must carry the workflow’s wake. Settle those individually with resolve or cancel them. See Triage.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
ids |
string[] |
Yes | Escalation UUIDs |
hint |
string |
No | Natural-language guidance for the triage agent |
Returns: LTApiResult<{ triaged, workflows }>
Auth: Required.
findByMetadata
Section titled “findByMetadata”const result = await lt.escalations.findByMetadata({ key: 'orderId', value: 'order-123', status: 'pending',});Find escalations by one metadata key and value. Uses JSONB containment backed by a GIN index.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
key |
string |
Yes | Metadata field name |
value |
string |
Yes | Metadata field value |
status |
string |
No | Filter by status |
limit |
number |
No | Max results (default 50) |
offset |
number |
No | Pagination offset |
Returns: LTApiResult<{ escalations, total }>
Auth: Required (scoped to visible roles).
claimByMetadata
Section titled “claimByMetadata”const result = await lt.escalations.claimByMetadata({ key: 'orderId', value: 'order-123', durationMinutes: 30, assignee: 'jane.doe', metadata: { station: 'scanning' }, provisionIfAbsent: { displayName: 'Jane Doe', email: 'jane@example.com', roles: [{ role: 'station-operator', type: 'member' }], },});Find and claim an escalation by metadata key and value in one atomic call. RBAC is enforced in the SQL WHERE clause.
provisionIfAbsent accepts { displayName?, email?, roles?: [{ role, type?, read_scope?, write_scope? }] }. Each role entry forwards the optional scope fields read_scope (self or all, default all) and write_scope (none, self or all, default all), subject to write ⊆ read; scope is ignored for admin and superadmin. To provision a one-time user who sees and acts on exactly the claimed row, use read_scope: 'self' and write_scope: 'self'. Only callers with global escalation access can use this field. When the user exists and holds the role, it adds zero queries.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
key |
string |
Yes | Metadata field name |
value |
string |
Yes | Metadata field value |
durationMinutes |
number |
No | Claim duration (default 30) |
assignee |
string |
No | Claim as this Long Tail user (looked up by external_id) |
metadata |
object |
No | Merged into the escalation metadata in the same SQL call as the claim |
provisionIfAbsent |
object |
No | Provision the assignee if they do not exist or lack the role (superadmin only) |
Returns: LTApiResult<{ escalation, isExtension }>. 404 if no match, 409 if already claimed.
Auth: Required.
resolveByMetadata
Section titled “resolveByMetadata”const result = await lt.escalations.resolveByMetadata({ key: 'orderId', value: 'order-123', resolverPayload: { approved: true },});// Plain row: result.data.escalation.status === 'resolved'// Signal-backed row: result.data.signaled === true, result.data.workflowIdFind and resolve an escalation by metadata key and value in one query with a signal guard. When the row carries metadata.signal_id, the call signals the running workflow, and conditional resolves the row durably inside the workflow.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
key |
string |
Yes | Metadata field name |
value |
string |
Yes | Metadata field value |
resolverPayload |
object |
Yes | Resolution data passed to the workflow |
assignee |
string |
No | Resolve as this Long Tail user (looked up by external_id) |
metadata |
object |
No | Merged into the escalation metadata before resolving |
Returns: LTApiResult<{ escalation }> for a plain row, LTApiResult<{ signaled, escalationId, workflowId }> for a signal-backed row. 404 if no match.
Auth: Required.
resolveBatchItem
Section titled “resolveBatchItem”const first = await lt.escalations.resolveBatchItem({ id: 'esc_123', itemKey: 'weld', resolverPayload: { ok: true, notes: 'weld complete' },});// first.data → { outcome: 'accepted', remaining: 2, escalationId }
const last = await lt.escalations.resolveBatchItem({ id: 'esc_123', itemKey: 'paint', resolverPayload: { ok: true },});// last.data → { outcome: 'completed', remaining: 0, signaled: true, escalationId, workflowId }Submit one declared item of a batch escalation (a conditional wait created with batch: [...]). One atomic statement: the payload lands in envelope.batch_items only while the key is still pending, the batch_pending and batch_count facets recompute, and the last item resolves the row and wakes the workflow with the full collection. Each fill records the caller under envelope.batch_filled_by[itemKey]. See Batches and accumulators.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
id |
string |
Yes | Escalation UUID |
itemKey |
string |
Yes | The declared batch key this submission fills |
resolverPayload |
Record<string, any> |
Yes | The item’s payload, validated against the same versioned role form as a single resolve |
metadata |
Record<string, any> |
No | Outcome patch merged into the row’s metadata in the same fill |
assertClaim |
boolean |
No | Require the caller’s own live claim, asserted inside the statement. Without it, fills accept contributions from several principals |
Returns: LTApiResult<{ outcome: 'accepted', remaining, escalationId }> on an interim fill, LTApiResult<{ outcome: 'completed', remaining: 0, signaled, escalationId, workflowId }> on the completing fill. 409 duplicate-item for an already-filled key (safe under retries), 400 for an undeclared key or a non-batch row, 404 if not found, 422 on schema violations.
Auth: Required.
resolveBatchItemBySignalKey
Section titled “resolveBatchItemBySignalKey”const result = await lt.escalations.resolveBatchItemBySignalKey({ signalKey: `fanout-home-${parentWorkflowId}`, itemKey: 'u1-L', resolverPayload: { ok: true },});Submit one batch item, selecting the escalation by its signal_key: the signal id the waiting workflow parked on. Suited to callers that already own that identity, such as a child workflow calling home or a webhook. Claim-agnostic and write-scope gated with 404 non-disclosure, like resolveBySignalKey.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
signalKey |
string |
Yes | The row’s signal_key (the id passed to conditional) |
itemKey |
string |
Yes | The declared batch key this submission fills |
resolverPayload |
Record<string, any> |
Yes | The item’s payload |
metadata |
Record<string, any> |
No | Outcome patch merged in the same fill |
Returns: the same outcomes as resolveBatchItem.
Auth: Required.
resolveBatchItemByMetadata
Section titled “resolveBatchItemByMetadata”const result = await lt.escalations.resolveBatchItemByMetadata({ key: 'orderId', value: 'order-123', itemKey: 'cut', resolverPayload: { ok: true },});Submit one batch item, selecting the escalation by metadata facet. It uses the same selector as resolveByMetadata: the highest-priority pending match, with RBAC folded into the SQL filter. Roles with enforce_schema validate the item against the selected row’s pinned schema before the fill.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
key |
string |
Yes | Metadata field name |
value |
string |
Yes | Metadata field value |
itemKey |
string |
Yes | The declared batch key this submission fills |
resolverPayload |
Record<string, any> |
Yes | The item’s payload |
metadata |
Record<string, any> |
No | Outcome patch merged in the same fill |
restrictRoles |
string[] |
No | Intersect the caller’s write scope with these roles inside the SQL filter |
Returns: the same outcomes as resolveBatchItem. 404 when no scoped pending row matches, 409 when the selected row went terminal to a concurrent resolution.
Auth: Required.
flagBatchItem
Section titled “flagBatchItem”await lt.escalations.flagBatchItem({ id: 'esc_123', itemKey: '00812999000011#1', label: 'Missing', payload: { note: 'not on the pallet' },});Flag one pending key of a batch escalation, for example Missing. The flag lands in envelope.batch_flags[itemKey] with the caller as its actor. The key stays pending and still fills. A wait declared with wakeOnFlag: true ends on the flag, and the workflow wakes with the items so far, $flags and $trigger: 'flag'.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
id |
string |
Yes | Escalation UUID |
itemKey |
string |
Yes | The pending batch key to flag |
label |
string |
Yes | The flag label |
payload |
Record<string, any> |
No | Detail stored with the flag |
Returns: LTApiResult<{ outcome: 'flagged' | 'completed', itemKey, escalationId, workflowId? }>. 409 for a key already filled or a row no longer pending, 400 for an undeclared key or a non-batch row.
Auth: Required.
unflagBatchItem
Section titled “unflagBatchItem”await lt.escalations.unflagBatchItem({ id: 'esc_123', itemKey: '00812999000011#1' });Clear one key’s flag while the row is pending.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
id |
string |
Yes | Escalation UUID |
itemKey |
string |
Yes | The flagged batch key |
Returns: LTApiResult<{ outcome: 'unflagged', itemKey, escalationId, workflowId? }>. 409 for a key without a flag or a row no longer pending.
Auth: Required.
runRosterAction
Section titled “runRosterAction”await lt.escalations.runRosterAction({ id: matchingRowId, field: 'items', action: 2, entry: '00812999000011#1', code: '812999000011',});Run one action that a roster entry offers on a row’s form, by pointer: the roster field, the action’s index, the entry key, and a key-in’s entered code. The server re-reads the action, checks the entry and any roles limit, and runs the act as the caller, or as the badged person when actingToken is given. See the form token reference.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
id |
string |
Yes | The host row: the escalation whose form declares the roster |
field |
string |
Yes | The roster field |
action |
number |
Yes | The action’s index among the field’s actions |
entry |
string |
Yes | The entry’s key |
code |
string |
No | A key-in’s entered code |
actingToken |
string |
No | A badge grant: the act runs as its person |
Returns: the act’s own result, or a refusal with outcome needs_badge, role_refused, not_primed or code_mismatch.
Auth: Required.
accumulate
Section titled “accumulate”const added = await lt.escalations.accumulate({ id: binId, itemKey: orderId, payload: { weight: 2 }, reciprocal: { id: bagEscalationId },});// added.data → { outcome: 'accepted', count: 1, remaining: 11, escalationId,// reciprocal: { outcome: 'completed', count: 1, remaining: 0, escalationId, signaled: true } }Add one item to an open accumulator escalation (conditionalAccumulator with accumulate: {...}). Interim adds return accepted with the count held and the slots remaining. The add that reaches max returns completed, and the waiting workflow resumes with the ordered collection in the same statement. See Batches and accumulators.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
id |
string |
Yes | The accumulator escalation UUID |
itemKey |
string |
Yes | The key the item is held under (1 to 128 characters) |
payload |
object |
No | Stored on the entry and delivered inside $accumulated; validates against the container’s versioned role form |
metadata |
object |
No | Merge patch for the container metadata in the same statement; reserved accumulate_* keys are rejected |
assertClaim |
boolean |
No | Require the caller’s own live claim |
reciprocal |
object |
No | { id | signalKey | key + value, payload? }: a second accumulator row written in the same statement. Both rows change or neither |
initiatedBy |
string |
No | The lt_users.id of the person who caused the add, recorded as the entry’s actor. Global-access callers only; attribution, never privilege |
Returns: LTApiResult<{ outcome: 'accepted' | 'completed', count, remaining, escalationId, signaled?, workflowId?, reciprocal? }>. remaining is null on an unbounded accumulator. 409 on Item already held, Accumulator is full, claim blocks, terminal rows and reciprocal blocks; 400 on a non-accumulator row; 404 non-disclosure.
Auth: Required.
accumulateBySignalKey
Section titled “accumulateBySignalKey”await lt.escalations.accumulateBySignalKey({ signalKey: `bin-${binKey}`, itemKey: orderId });Add one item to an accumulator selected by its signal_key. Takes the same fields as accumulate except id and assertClaim.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
signalKey |
string |
Yes | The accumulator’s signal_key |
itemKey |
string |
Yes | The key the item is held under |
payload |
object |
No | Stored on the entry |
metadata |
object |
No | Merge patch for the container metadata |
reciprocal |
object |
No | A second accumulator row written in the same statement |
initiatedBy |
string |
No | The person who caused the add (global-access callers only) |
Returns: the same outcomes as accumulate.
Auth: Required.
accumulateByMetadata
Section titled “accumulateByMetadata”await lt.escalations.accumulateByMetadata({ key: 'binKey', value: binKey, itemKey: orderId, restrictRoles: ['bin'],});Add one item to an accumulator selected by metadata facet. The facet selects among pending accumulator rows only; other rows sharing it are never candidates, and when none is open the call answers 404.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
key |
string |
Yes | Metadata field name |
value |
string |
Yes | Metadata field value |
itemKey |
string |
Yes | The key the item is held under |
payload |
object |
No | Stored on the entry |
metadata |
object |
No | Merge patch for the container metadata |
reciprocal |
object |
No | A second accumulator row written in the same statement |
restrictRoles |
string[] |
No | Expected queues, intersected with the caller’s write scope |
container |
{ types?, subtypes?, facets? } |
No | Narrows the container to accumulators of these types or subtypes that also carry these facets. The add’s own reciprocal row is never its container |
initiatedBy |
string |
No | The person who caused the add (global-access callers only) |
Returns: the same outcomes as accumulate.
Auth: Required.
removeItem
Section titled “removeItem”await lt.escalations.removeItem({ id: binId, itemKey: orderId, reciprocal: { id: bagEscalationId } });// → { outcome: 'removed', count: 11, escalationId, reciprocal?: { count, escalationId } }Remove one held item from a pending accumulator in one guarded statement. The row stays pending and the waiter does not wake. With reciprocal, the paired row changes in the same statement or neither row changes.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
id |
string |
Yes | The accumulator escalation UUID |
itemKey |
string |
Yes | The held item’s key |
reciprocal |
{ id | signalKey | key + value } |
No | The paired accumulator row |
initiatedBy |
string |
No | The person who caused the removal (global-access callers only) |
Returns: LTApiResult<{ outcome: 'removed', count, escalationId, reciprocal? }>. 404 Item not held by this escalation.
Auth: Required.
removeItemBySignalKey
Section titled “removeItemBySignalKey”await lt.escalations.removeItemBySignalKey({ signalKey: `bin-${binKey}`, itemKey: orderId });Remove one held item from an accumulator selected by its signal_key.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
signalKey |
string |
Yes | The accumulator’s signal_key |
itemKey |
string |
Yes | The held item’s key |
reciprocal |
{ id | signalKey | key + value } |
No | The paired accumulator row |
initiatedBy |
string |
No | The person who caused the removal |
Returns: the same outcomes as removeItem.
Auth: Required.
removeItemByMetadata
Section titled “removeItemByMetadata”await lt.escalations.removeItemByMetadata({ key: 'binKey', value: binKey, itemKey: orderId });Remove one held item from an accumulator selected by metadata facet.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
key |
string |
Yes | Metadata field name |
value |
string |
Yes | Metadata field value |
itemKey |
string |
Yes | The held item’s key |
restrictRoles |
string[] |
No | Expected queues, intersected with the caller’s write scope |
reciprocal |
{ id | signalKey | key + value } |
No | The paired accumulator row |
initiatedBy |
string |
No | The person who caused the removal |
Returns: the same outcomes as removeItem.
Auth: Required.
getItems
Section titled “getItems”const { data } = await lt.escalations.getItems({ id: binId });// data → { escalationId, kind: 'accumulate' | 'batch', status, count, max,// items: [{ itemKey, payload?, at, actor?, reciprocalId? }], pending? }Read the held items of an accumulator or batch row, in arrival order. max is the count trigger (accumulator) or the declared size (batch), null when unbounded. pending lists the batch keys still awaiting submission.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
id |
string |
Yes | Escalation UUID |
Returns: LTApiResult<{ escalationId, kind, status, count, max, items, pending? }>
Auth: Required (read access; 404 non-disclosure).
getItemsBySignalKey
Section titled “getItemsBySignalKey”const { data } = await lt.escalations.getItemsBySignalKey({ signalKey: `bin-${binKey}` });Read the held items of an accumulator or batch row by the signal_key the parked workflow knows.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
signalKey |
string |
Yes | The row’s signal_key |
Returns: the same shape as getItems.
Auth: Required (read access; 404 non-disclosure).
searchByFacets
Section titled “searchByFacets”const result = await lt.escalations.searchByFacets({ role: 'assembly-pool', status: 'pending', available: true, facets: { state: 'ready' }, limit: 50,});Item-level faceted search over one pool role, scoped to the caller’s role. This is the faceted-routing read primitive. types and subtypes narrow by the escalation’s declared type and subtype (type = ANY(types), subtype = ANY(subtypes)). Every other FacetQuery element applies as described in Faceted routing.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
role |
string |
Yes | The pool role |
... |
FacetQuery |
No | status, available, facets, block, range, exists, types, subtypes, orderBy, limit, offset |
Returns: LTApiResult<{ escalations, total }>
Auth: Required.
claimGroups
Section titled “claimGroups”const result = await lt.escalations.claimGroups({ query: { role: 'assembly-pool', available: true, facets: { material: 'steel', size_class: 'standard' } }, limit: 4, durationMinutes: 30, sizeFacet: 'order_size',});Claim complete origin groups (for example, every unit of an order) in priority order over a pool, assigned to the caller. The caller must hold the pool role with claim (write_scope=all) scope, or be a superadmin.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
query |
FacetQuery |
Yes | The pool query; query.role is required |
limit |
number |
No | Max groups to claim |
durationMinutes |
number |
No | Claim duration |
sizeFacet |
string |
No | Metadata key holding each group’s size |
Returns: LTApiResult<{ groups }>. 400 without query.role, 403 without claim scope on the pool.
Auth: Required.
claimByFacets
Section titled “claimByFacets”const result = await lt.escalations.claimByFacets({ query: { role: 'assembly-pool', facets: { state: 'ready', material: 'steel' } }, limit: 3,});Claim individual rows matching a facet query (FOR UPDATE SKIP LOCKED), assigned to the caller. With allOrNone, the claim commits only when the full limit is acquired.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
query |
FacetQuery |
Yes | The pool query; query.role is required |
limit |
number |
No | Max rows to claim |
durationMinutes |
number |
No | Claim duration |
allOrNone |
boolean |
No | Commit only when limit rows are acquired |
Returns: LTApiResult<{ claimed }>. 400 without query.role, 403 without claim scope on the pool.
Auth: Required.
aggregateByFacets
Section titled “aggregateByFacets”// How a set of machines spent the window, one row per stateconst result = await lt.escalations.aggregateByFacets({ query: { entity: 'serialNumber' }, groupBy: { state: true }, measure: { kind: 'dwell', window: { from: '2026-08-01T00:00:00Z', to: '2026-08-02T00:00:00Z' } },});
// The same bands per model valueawait lt.escalations.aggregateByFacets({ query: { entity: 'serialNumber' }, groupBy: { state: true, facets: ['model'] }, measure: { kind: 'dwell', window: { from: '2026-08-01T00:00:00Z', to: '2026-08-02T00:00:00Z' } },});Grouped analytics over escalation intervals. Every escalation is one interval [created_at, ended_at), with ended_at null while live. Two measures:
- membership: rows (or, with
distinctBy, distinct entities) open at an instant. A pastasOfreconstructs the live set at that time. - dwell: open seconds per group within a half-open
[from, to)window, clipped to it.
query.entity names an entity facet key and scopes to every role declaring it as its entity_facet (mutually exclusive with role and roles). groupBy.state groups by the derived state label, each role contributing per its entity_state_source. See Analytics.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
query |
AnalyticsQuery |
Yes | role/roles or entity, plus facets, anyOf (rows carrying any of the given facet sets, max 200), block, range, exists and prefix (case-insensitive prefix match on facet values). status, available, jeopardy and query-level paging are rejected with 400 |
groupBy |
FacetGroupBy |
Yes | columns (role, subtype, status), facets (metadata keys), state (derived label; exclusive with states). An empty object returns one total row |
measure |
AggregateMeasure |
Yes | { kind: 'membership', asOf? } or { kind: 'dwell', window } |
distinctBy |
string |
No | Membership only: count distinct values of this facet |
states |
Array<{ name, match }> |
No | Tag each group with the first matching state name |
liveStatuses |
string[] |
No | Statuses considered live (default ['pending']) |
orderBy |
No | Order the result groups | |
limit |
number |
No | Page the result groups (capped at LT_ANALYTICS_MAX_GROUPS) |
offset |
number |
No | Offset into the result groups |
Returns: LTApiResult<{ groups: AggregateRow[], overflow: boolean }>. Each group carries the grouped keys, count or dwellSeconds, and sampleCount.
Auth: Required. read_all on every role in scope (entity-derived roles included). A roleless filter requires a global principal. Counts-only groupings (no groupBy.facets) are open to any login while features.publicPaceBoard is on.
timelineByFacet
Section titled “timelineByFacet”// One machine's movement across the queuesconst result = await lt.escalations.timelineByFacet({ facet: { key: 'serialNumber', value: 'PRN-001' }, query: { entity: 'serialNumber' },});One entity’s ordered interval sequence: every escalation the facet appeared in, as [startedAt, endedAt) spans with durations, in created_at order. Open intervals report endedAt: null. Gaps are untracked time and are preserved. The match is GIN-served containment, so the stored facet value must be a JSON string.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
facet |
{ key, value } |
Yes | The entity facet to trace |
query |
AnalyticsQuery |
No | Extra filter or role scope (or entity) |
window |
{ from, to } |
No | Only intervals overlapping the window (overlap-filtered, not clipped) |
select |
{ columns?, facets? } |
No | Columns and facets to return per interval (default: all three columns) |
liveStatuses |
string[] |
No | Statuses considered live (default ['pending']) |
order |
'asc' | 'desc' |
No | Order by start instant (default asc). desc with before pages a long history recent-first |
before |
Date | string |
No | Strict upper bound on startedAt (the “load earlier” cursor) |
limit |
number |
No | Max intervals |
Returns: LTApiResult<{ intervals: TimelineInterval[], overflow: boolean }>
Auth: Required. Always takes the full read gate, because an entity’s movement history is item-level disclosure.
getRelated
Section titled “getRelated”const page = await lt.escalations.getRelated({ role: 'consolidate', ids: rows.map((r) => r.id) });// page.data.sets.contents.aggregates['reciprocal.metadata.serviceLevel'][rowId]// → { count: 3, splits: { standard: 2, express: 1 } }Aggregate every related set a role’s list schema declares (x-lt-related) for one page of list rows, under the caller’s read scope. One statement runs per set and aggregate, whatever the page size.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
role |
string |
Yes | The role whose list schema declares the sets |
ids |
string[] |
Yes | The host rows on the page |
version |
number |
No | List schema version (defaults to the latest) |
Returns: LTApiResult<{ sets }>. 400 when ids is not an array or exceeds the host limit, 404 when the role has no list schema.
Auth: Required.
getRelatedRows
Section titled “getRelatedRows”const orders = await lt.escalations.getRelatedRows({ role: 'consolidate', id: rowId, set: 'contents' });// orders.data.rows → [{ item: { itemKey, at, reciprocalId }, escalation }]Read one host row’s rows in one related set, for a row detail section.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
role |
string |
Yes | The role whose list schema declares the set |
id |
string |
Yes | The host escalation id |
set |
string |
Yes | The related set name |
version |
number |
No | List schema version |
limit |
number |
No | Max rows |
Returns: LTApiResult<{ rows }>. 404 when the list schema declares no such set.
Auth: Required.
Cancelling an escalation
Section titled “Cancelling an escalation”Cancelling a pending or claimed row ends the wait with null. Cancel runs through POST /api/escalations/:id/cancel and its bulk route (see the HTTP reference), the MCP tools or the dashboard, and requires admin or superadmin for the escalation’s role.
Recording the outcome
Section titled “Recording the outcome”Every resolve path (resolve, resolveBySignalKey, the HTTP routes, the MCP tools and the in-process EscalationService.resolveEscalation) takes an optional metadata patch. It is merged into the row’s GIN-indexed metadata, so the outcome lives on the same row as the intent.
await lt.escalations.resolve({ id: 'esc_123', resolverPayload: { approved: true }, // resumes the workflow; not indexed metadata: { outcome: 'approved', durationMs: 1_240 }, // recorded on the row; @>-queryable});
await lt.escalations.findByMetadata({ key: 'outcome', value: 'approved' });The payload is the wait’s return value. The patch is the durable, queryable record. The in-process library takes the same patch as a third argument: resolveEscalation(id, payload, metadata) and resolveEscalationBySignalKey(signalKey, payload, metadata).
Workflow helpers
Section titled “Workflow helpers”Escalations usually start inside a workflow. conditional turns an intent into a row and waits for its resolution:
import { conditional } from '@hotmeshio/long-tail';
const decision = await conditional<{ approved: boolean }>(signalId, { role: 'reviewer', metadata: { orderId }, timeout: '24h',});if (decision === false) return { expired: true }; // the timeout fired firstif (decision === null) return { cancelled: true }; // the row was cancelled| Return | Meaning |
|---|---|
T |
The resolver’s payload |
false |
The timeout fired first; the row is now expired |
null |
The escalation was cancelled |
The row is written in the workflow’s own checkpoint with signal_key = signalId, so resolve and resolveBySignalKey resume the job in place. conditionalAccumulator is the typed form for open accumulators. conditionLT is a deprecated alias of conditional. See Conditional and Batches and accumulators.
Resolver forms come from the target role’s versioned form_schema, or from metadata.form_schema on the row. See Forms.
