Skip to content

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

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.

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.

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.

const result = await lt.escalations.listTypes();

List every distinct escalation type value.

Parameters: none.

Returns: LTApiResult<{ types: string[] }>

Auth: Not required.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

const result = await lt.escalations.releaseExpired();

Release every claim past its assigned_until deadline.

Parameters: none.

Returns: LTApiResult<{ released: number }>

Auth: Not required.

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.

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.

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.

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

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.

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.

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

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.

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

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

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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

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

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.

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.

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.

// How a set of machines spent the window, one row per state
const 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 value
await 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 past asOf reconstructs 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.

// One machine's movement across the queues
const 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.

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.

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

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

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 first
if (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.