Skip to content

Analytics

Every escalation is one interval an entity spends in one state. It opens at created_at and closes when the row leaves the live set. The queue is therefore a complete state history for every process you run. This page shows how to instrument a process with two role dials and read it back three ways: across the fleet, sliced by a facet, and for one entity.

role is the queue that attends the state. subtype names the state within the role when the role holds several. metadata carries the entity key and any categorical facts worth slicing by.

workflows/printer.ts
import { conditional } from '@hotmeshio/long-tail';
// Park the printer in the fleet queue, printing.
const done = await conditional(`printer-${serialNumber}-job-${jobId}`, {
role: 'printer-fleet', // the queue that attends it
type: 'fleet',
subtype: 'printing', // the state, within the role
metadata: {
serialNumber: 'PRN-001', // the entity
model: 'model-a', // categorical facts, sliceable
enclosed: true,
},
});

Set them on the role page’s Pace Board section, or with PATCH /api/roles/:role:

Dial Meaning
entity_facet The metadata key naming what moves through this role, here serialNumber. Roles sharing an entity key form that entity’s system, derived on read.
entity_state_source 'role' (default): being in this queue is one state. 'subtype': the role’s subtypes are its states.
printer-fleet entity_facet: serialNumber states from: subtype (idle, printing)
printer-pickup entity_facet: serialNumber states from: role
printer-service entity_facet: serialNumber states from: role

That is the whole contract. Everything below reads it back.

Scope to the entity’s system, group by the derived state, and measure dwell: open seconds, clipped to the window.

Terminal window
ltc esc aggregate-facets --entity serialNumber --group-state \
--window '{"from":"2026-08-01T00:00:00Z","to":"2026-08-01T12:00:00Z"}'

Membership is the other measure: rows open at an instant. With distinctBy, it counts distinct entities instead of rows.

// How many printers are in each state right now?
await client.escalations.aggregateByFacets({
query: { entity: 'serialNumber' },
groupBy: { state: true },
measure: { kind: 'membership' },
distinctBy: 'serialNumber',
});

Omit measure.asOf for now. A past asOf (--as-of on the CLI) reconstructs the live set at that moment.

Measure Answers Inputs
Dwell How long entities spent in each state over a period window: { from, to }
Membership How many rows or entities sit in each state at an instant measure.asOf (default now), distinctBy

Add groupBy.facets. Each value gets its own state split, and rows missing the facet form a real NULL group:

Terminal window
ltc esc aggregate-facets --entity serialNumber --group-state --group-facets model \
--window '{"from":"2026-08-01T00:00:00Z","to":"2026-08-01T12:00:00Z"}'
# model-a and model-b, each with its own idle, printing, pickup and service dwell

Filter the same way: --facets '{"enclosed":true}' restricts the whole aggregate to enclosed printers.

timelineByFacet returns the ordered interval sequence across the whole system:

Terminal window
ltc esc timeline serialNumber PRN-001 --entity serialNumber

Open intervals report endedAt: null. Gaps between intervals are untracked time and stay in the result: the settle time between queues is a signal in its own right.

A long history pages recent-first with order: 'desc' and the before cursor, a strict upper bound on startedAt:

Terminal window
ltc esc timeline serialNumber PRN-001 --entity serialNumber --order desc --limit 100
ltc esc timeline serialNumber PRN-001 --entity serialNumber --order desc --limit 100 \
--before 2026-08-01T09:15:00Z

Each row is [created_at, ended_at). ended_at comes from the terminal transition: resolved_at for resolved rows, updated_at for cancelled or expired rows, NULL while the row is live. The lt_escalations view exposes it as a computed column, and a partial index on the terminal instant keeps history scans bounded.

liveStatuses (default ['pending']) declares which statuses count as live.

Filter Does
prefix Case-insensitive prefix match on a facet value: --prefix '{"serialNumber":"PRN-00"}'
anyOf Rows carrying at least one of the given facet sets, GIN-served, up to 200: { "anyOf": [{ "serialNumber": "PRN-001" }, { "serialNumber": "PRN-002" }] }

Result groups page with orderBy, limit and offset. overflow: true means another page exists.

Question Where
Q1 /operations: pick an entity system in the view selector to see state dwell and membership-now counts
Q2 The entity view’s Slice by control
Q3 The entity table’s timeline action, or the history action on any metadata value in a list

/operations?lens=serialNumber&entity=PRN-003 opens the fleet view focused on one printer with its timeline raised. Every surface refreshes from escalation events.

The filter takes role or roles (or entity), facets, block, range, exists, prefix and anyOf. Anything that would quietly change what an aggregate means is a 400:

  • status, available or jeopardy on the filter (liveness comes from the interval and the measure)
  • orderBy, limit or offset on the filter (paging applies to result groups)
  • entity together with role or roles
  • an entity key no role declares (the response names the gap)
  • groupBy.state together with states[]
  • a window wider than LT_ANALYTICS_MAX_WINDOW_DAYS, a future asOf, malformed keys, unknown statuses

Result groups are capped by LT_ANALYTICS_MAX_GROUPS.

Aggregates and timelines need read_all on every role in scope. A query with no role scope spans every queue and needs a global principal. With the public Pace Board flag on, counts-only aggregates (no facet keys among the group keys) are readable by any login.

Surface Name
SDK client.escalations.aggregateByFacets, client.escalations.timelineByFacet
HTTP POST /api/escalations/aggregate-by-facets, POST /api/escalations/timeline-by-facet
MCP aggregate_by_facets, timeline_by_facet
CLI ltc esc aggregate-facets, ltc esc timeline

Cache tuning: LT_ANALYTICS_CACHE_TTL_MS, LT_ANALYTICS_PAST_CACHE_TTL_MS, LT_ANALYTICS_CACHE_MAX_ENTRIES.