Roster
A roster lists every member a container row expects, each marked waiting, in or flagged, with the count in its heading (“3 of 5 in · 1 Missing”). The person works from it: a member that arrives ticks in, an action on an entry acts in place, and the rest of the form can wait until every member is accounted for.
| Token | Placement | Purpose |
|---|---|---|
x-lt-widget: "roster" |
Field | Renders the field as a roster |
x-lt-roster |
Field | The expected set, how arrivals are read, and flag rules |
x-lt-columns |
Field | Columns per entry |
x-lt-actions |
Field | Acts on an entry: resolve, fill, flag, unflag |
Like the other embed widgets, a roster is display-only: declare it readOnly, keep it out of required, and it adds nothing to the resolver payload.
"orders": { "type": "string", "readOnly": true, "title": "Orders in this box", "description": "Scan each bag into the box. Mark a bag missing if it is not in the bin.", "x-lt-widget": "roster", "x-lt-roster": { "expected": { "query": { "roles": ["packing"], "facets": { "packingBox": "{{escalation.id}}" } } }, "key": "{{metadata.orderId}}", "in": ["items", "resolver.bags"], "flagged": [ { "when": "resolver.outcome=missing", "label": "Missing" }, { "when": "resolver.outcome=needs-manager", "label": "With a manager" } ] }, "x-lt-columns": [ { "label": "Order", "value": "{{metadata.orderId}}" }, { "label": "SKU", "value": "{{metadata.sku}}" } ], "x-lt-actions": [ { "label": "Missing", "when": "waiting", "resolverPayload": { "outcome": "missing" }, "confirm": "Mark {{metadata.orderId}} missing?" }, { "label": "Send to a manager", "when": "waiting", "resolverPayload": { "outcome": "needs-manager" } } ]}x-lt-roster
Section titled “x-lt-roster”| Key | Description |
|---|---|
expected |
The members the row expects: one of query, list or batch (below) |
key |
For a query set: a template naming each member row’s entry. Default {{escalation.id}} |
in |
Where arrivals are read. items reads the row’s held keys (its accumulated items, or a batch’s filled keys); any other entry is a domain.path naming a string or an array, such as a scan field in the live form (resolver.bags). Default ["items"] |
flagged |
Rules over each member row’s own context: when is an x-lt-showIf condition (or an array of them), label is the text the entry shows. The first matching rule wins |
countedBy |
The separator of counted keys ("#" for <code>#1, <code>#2). The nth occurrence of a code in an in source ticks <code>#n, so two scans of one code into a form field tick both of its keys |
Expected sets
Section titled “Expected sets”| Set | Shape | Entries |
|---|---|---|
query |
{ "role": "...", "roles": [...], "facets": {...}, "status": "..." } |
Member rows matching the role(s) and facets. Facet values are {{domain.path}} templates over the host row. Every status is read unless status names one, so a member resolved as missing stays on the roster. Up to 200 rows. When several rows share a key (a member that parked again), the entry reads the newest |
list |
"metadata.members" |
The keys in an array on the host row |
batch |
true, with optional "labels": "envelope.boxLabels" |
The row’s batch keys; a batch key is in once filled. labels names a per-key map whose entry each column reads as {{item.label.*}} |
Stamp the container’s id on each member row when the container opens (packingBox above). The expected set is then the container’s own, and a member that joins the source later stays off it.
An entry is in when its key is held or appears in an in source, flagged when its member row matches a flagged rule (on a batch set, when its key carries a flag), and waiting otherwise. In reads before flagged: a member a manager confirms into the box reads in, even though its own row was resolved as “needs a manager”.
Columns
Section titled “Columns”x-lt-columns take the same { label, value, format } entries as the escalation list. Values read the member row’s domains for a query set (the host row’s otherwise), beside {{item.*}}:
| Token | Value |
|---|---|
{{item.itemKey}} |
The entry key |
{{item.label.*}} |
The entry’s value in a batch labels map |
{{item.payload.*}}, {{item.actor}}, {{item.at}} |
The item once it is in: its payload, who filled it, and when. A batch entry reads them from batch_items, batch_filled_by and batch_filled_at, whether the key was filled by a scan, the API, or conditional’s filled |
format: "date" renders a time and format: "user" a person’s name: { "label": "Received", "value": "{{item.at}}", "format": "date" }, { "label": "By", "value": "{{item.actor}}", "format": "user" }.
Without x-lt-columns, each entry shows its key.
Actions
Section titled “Actions”Each x-lt-actions entry is one act on an entry:
| Act | Shape | Offered by default | What it does |
|---|---|---|---|
| Resolve | "resolverPayload": { ... } |
waiting |
Resolves the entry’s member row (a query set). String leaves interpolate against the member row |
| Fill | "fill": { "payload": { ... }, "enterCode": true } |
waiting, flagged |
Fills the entry’s batch key, as a scan would. With enterCode, the person types the code from the label and the server checks it against the key (a UPC-A matches its GTIN-14 key); the payload records it as enteredCode. The key-in field takes keys and scans as its own answer |
| Flag | "flag": { "label": "Missing", "payload": { ... } } |
waiting |
Flags the entry’s batch key. The key stays pending and still fills; the entry reads flagged under the label and counts in roster.<field>.flagged |
| Unflag | "unflag": true |
flagged |
Clears the entry’s flag while the row is pending (“Found it”) |
| Key | Description |
|---|---|
label |
Button text |
when |
The entry state(s) that offer the action: "waiting", "in", "flagged", or an array |
confirm |
A confirmation asked in place, beside the entry, before firing |
roles |
The action runs only for a person holding one of these roles |
refuse |
What a person without the roles is told |
"x-lt-actions": [ { "label": "Missing", "flag": { "label": "Missing" }, "confirm": "Mark this item missing?" }, { "label": "Found it", "unflag": true }, { "label": "Key in (manager)", "fill": { "payload": { "source": "key-in" }, "enterCode": true }, "roles": ["manager"], "refuse": "A manager's badge is needed to key a box in." }]Actions run on the server. The dashboard sends only which action and which entry (POST /api/escalations/:id/roster-action). The server re-reads the action from the row’s pinned form schema, confirms the entry is on the roster it computes, builds the payload from the declaration, and checks roles against the person acting. The act then runs through its own endpoint (resolve, batch fill, flag, unflag) under that person’s RBAC and enforce_schema, records them as the resolver, filler or flagger, and shows a refusal’s reason on the entry.
Roles and badges. An action with roles shows to everyone. A person without one of the roles is asked for a badge, and the action runs as whoever badges in; a badge without the roles is refused with refuse, and the badge is not spent. On a read-only station login every action runs through a badge. A role limit is an act control layered on RBAC: the person must also be allowed to write the row.
On a wait declared with wakeOnFlag, a flag ends the wait: the workflow wakes with the items filled so far, $flags, and $trigger: "flag", and can route the order (for example, to a manager). Without it, the flag is recorded and the wait continues.
Moving work to another person is a resolve with an outcome. “Send to a manager” above resolves the member row with outcome: "needs-manager"; the member’s workflow opens a row in the manager’s role, where only a manager can resolve it, and the manager’s confirm adds the bag to the box.
Completeness: roster.* and the roster guard
Section titled “Completeness: roster.* and the roster guard”Each roster’s counts join the form context as roster.<field>: expected, in, flagged, waiting and outstanding (entries neither in nor flagged). x-lt-showIf and help tokens read them, so later steps can wait for the roster:
"weight": { "type": "number", "title": "Box weight", "description": "Weigh the closed box.", "x-lt-showIf": "roster.orders.outstanding=0"}A submit guard can name the roster. Submit stays disabled while entries are outstanding:
"x-lt-submit-guard": { "roster": "orders", "message": "{{count}} orders are still unaccounted for" }{{count}} is the outstanding count. A roster guard never auto-submits; the person finishes the steps the roster revealed.
Enforced on both sides. For roles that set enforce_schema, the server computes the same roster at resolve, from the same shared code: roster.* showIf conditions evaluate as the form showed them, and a resolve with an outstanding entry is refused with the guard’s message as a 422, on every surface. The server reads member rows regardless of the resolver’s read scope, so a resolver who cannot see a member still cannot clear the guard. Give the people who work the roster read on the member role so the list they see matches.
Live updates
Section titled “Live updates”The roster reads member rows through the same list query as every list, so it refreshes on the same escalation events: a bag scanned at another station, or added from another device, ticks in without a reload. A scan into a form field ticks its entry in at once.
