Embedded records
Three display-only widgets show related information inside a resolver form. Their keys are dropped from the submitted payload, they stay out of the schema’s required array, and they are declared readOnly: true. The invoke and roster widgets follow the same rules.
| Widget | Tokens | What it shows |
|---|---|---|
link |
x-lt-href |
A named link to any dashboard view, with a pre-applied deep-link query |
escalation |
x-lt-source, x-lt-fields |
One embedded escalation record with a facts panel |
escalation-list |
x-lt-query, x-lt-columns, x-lt-actions |
A compact list of escalations from a facet query, with optional inline actions |
The top-level x-lt-submit-guard holds the resolve while a related query still returns rows.
Link widget
Section titled “Link widget”Renders a named anchor whose URL is resolved from a {{domain.path}} template at render time. Use it to give reviewers a one-click path to a related queue, filtered to the current item.
{ "originator_queue": { "type": "string", "readOnly": true, "x-lt-widget": "link", "x-lt-href": "/escalations/available?role=rel-originator&facets={\"orderId\":\"{{metadata.orderId}}\"}", "x-lt-span": 2, "x-lt-section": "Related", "title": "View originator queue for this order", "description": "Opens the originator queue filtered to this order" }}x-lt-href
Section titled “x-lt-href”Required. A URL template. {{domain.path}} tokens interpolate from the same five domains as x-lt-help (escalation, metadata, envelope, payload, resolver), resolved against the current form’s escalation context.
A path starting with / navigates inside the dashboard. Any other URL opens in a new tab with rel="noreferrer". A URL that interpolates to blank renders a muted “No link configured” placeholder.
Display text
Section titled “Display text”The field’s title is the link label, and description renders as a one-line helper beneath it. Without a title, description is the label.
Escalation widget
Section titled “Escalation widget”Embeds one escalation record as a compact card. x-lt-source resolves the escalation ID from the current form’s context; x-lt-fields picks the facts the card shows.
{ "parent_escalation": { "type": "string", "readOnly": true, "x-lt-widget": "escalation", "x-lt-source": "metadata.parent_escalation_id", "x-lt-fields": [ { "label": "Order ID", "value": "{{metadata.orderId}}" }, { "label": "Decision", "value": "{{resolver.decision}}" }, { "label": "Reason", "value": "{{resolver.reason}}" }, { "label": "Age", "value": "{{escalation.created_at}}", "format": "age" } ], "x-lt-span": 2, "x-lt-section": "Related", "title": "Originating request", "description": "The escalation that created this item" }}x-lt-source
Section titled “x-lt-source”Required. A "domain.path" expression, the same convention as the checklist widget, resolved against the current form’s context to the embedded escalation’s UUID. "metadata.parent_escalation_id" reads parent_escalation_id from the row’s metadata.
When the path resolves to nothing, or the escalation is missing or outside the viewer’s roles, the widget renders a quiet “No linked record” placeholder.
x-lt-fields
Section titled “x-lt-fields”Optional. An array of { label, value, format? }, the same shape as x-lt-active.fields and x-lt-card.fields in list schemas. The value tokens resolve against the embedded escalation’s own data:
| Domain | Resolves against |
|---|---|
escalation |
The embedded escalation’s columns: escalation.status, escalation.role, escalation.type |
metadata |
Its metadata |
envelope |
Its workflow envelope |
payload |
Its context payload |
resolver |
Its resolver payload |
format: "age" renders an ISO timestamp as a compact relative age (12m, 3h). Without x-lt-fields, the card shows the type and status header, the description and the footer.
Card anatomy
Section titled “Card anatomy” esc-type · subtype [status] ───────────────────────────── The escalation description ───────────────────────────── Order ID ORD-123 Decision Approved ───────────────────────────── rel-originator · 2h ago Detail →Escalation list widget
Section titled “Escalation list widget”Embeds a compact, unpaginated list of escalations from a facet query. Use it to show sibling items: pending work in a related role, filtered to the same customer, order or any other metadata facet.
{ "sibling_items": { "type": "string", "readOnly": true, "x-lt-widget": "escalation-list", "x-lt-query": { "role": "rel-originator", "facets": { "customerId": "{{metadata.customerId}}" }, "status": "pending", "limit": 5 }, "x-lt-columns": [ { "label": "Order", "value": "{{metadata.orderId}}" }, { "label": "Status", "value": "{{escalation.status}}" }, { "label": "Age", "value": "{{escalation.created_at}}", "format": "age" } ], "x-lt-span": 2, "x-lt-section": "Related", "title": "Other pending items for this customer", "description": "Up to 5 items waiting in the originator queue" }}x-lt-query
Section titled “x-lt-query”Required. String values inside facets interpolate {{domain.path}} tokens against the current item’s context.
| Field | Type | Description |
|---|---|---|
role |
string |
Role queue to query |
status |
string |
pending (default), resolved, cancelled, expired |
facets |
Record<string, string> |
Metadata containment filter; values interpolate tokens |
assigned |
"me" | "any" |
Ownership scope, below |
limit |
number |
Max rows to show (default 5) |
available |
boolean |
Legacy availability flag, honored only when assigned is omitted |
The widget stays idle (no list, no request) until role or a facet value is set, which keeps every query bounded.
Ownership scope (assigned)
Section titled “Ownership scope (assigned)”Ownership is one more dimension of the same faceted query. “Claimed” is the implied status: pending, with a live claim window.
"me": rows claimed by the viewing user. An earlier step assigned a set of order items to whoever claimed the batch; the follow-up form shows that person their own items with working inline actions."any": every matching row, whatever its claim state.- Omitted: available rows only (unclaimed, or the claim window lapsed).
The escalation-list widget, its x-lt-actions and x-lt-submit-guard share one mapping from query to rows, so a guard and an embed declaring the same query always agree on count and rows. Inline actions on rows claimed by the viewer resolve through the standard endpoint as the claim holder, with RBAC and enforce_schema applied.
"x-lt-query": { "role": "order-items", "facets": { "orderId": "{{metadata.orderId}}" }, "assigned": "me"}x-lt-columns
Section titled “x-lt-columns”Optional. An array of { label, value, format? }, the same shape as x-lt-columns in list schemas. Tokens resolve against each displayed escalation’s own context (its row, metadata, envelope, payload, resolver). Without it, three default columns render: Description, Role, Age. Each row links to the escalation’s detail page.
x-lt-actions
Section titled “x-lt-actions”Optional. Inline row actions: each row gains a button that resolves that escalation with a canned payload through the standard resolve endpoint. RBAC and enforce_schema apply exactly as for a full-form resolve.
"x-lt-actions": [ { "label": "Picked ✓", "resolverPayload": { "approved": true, "checks": { "picked": true, "scanned": true }, "orderId": "{{metadata.orderId}}" }, "confirm": "Mark {{metadata.sku}} picked for {{metadata.orderId}}?" }]| Field | Type | Description |
|---|---|---|
label |
string |
Button text |
resolverPayload |
object |
The canned payload. String leaves interpolate {{domain.path}} against the row’s own context; booleans and numbers pass through typed |
confirm |
string |
Optional confirmation asked beside the row before firing; tokens interpolate per row |
On success the row updates in place. A rejected resolve (validation failure, lost claim) shows the server’s message in the row, and the detail link stays available for rejects and anything the canned payload cannot express.
Declare one action for the common path and leave exceptions to the full form. A batch of N sibling items becomes N clicks on one page.
An action limited by roles belongs on a roster, whose actions run on the server where the limit is checked. The escalation list offers actions without roles.
Empty and loading states
Section titled “Empty and loading states”- Loading: three skeleton rows.
- Empty: a muted “No items found” message.
Submit guard (x-lt-submit-guard)
Section titled “Submit guard (x-lt-submit-guard)”A top-level form schema token, a peer of x-lt-help, that holds the resolve while related work is open. The submit stays disabled while the declared query returns rows, with the message and live count beside it. When the last row resolves, the live update reruns the query and the button enables.
{ "x-lt-submit-guard": { "query": { "role": "order-items", "facets": { "orderId": "{{metadata.orderId}}" }, "assigned": "me" }, "mustBeEmpty": true, "message": "{{count}} item(s) still pending. Pick them before closing the order.", "autoResolveWhenEmpty": true }, "type": "object", "properties": {}}| Field | Type | Description |
|---|---|---|
query |
object |
Same shape as x-lt-query, including assigned; facet values interpolate against the host escalation |
mustBeEmpty |
boolean |
The gate condition (default true) |
message |
string |
Shown beside the disabled submit; {{count}} is the live row count, and {{domain.path}} tokens interpolate |
autoResolveWhenEmpty |
boolean |
Submit the claimed parent the moment the query is confirmed empty |
roster |
string |
In place of query: a roster field whose outstanding entries hold the submit; {{count}} is the outstanding count. A roster guard does not auto-submit |
Pair the guard with an escalation-list embed on the same query so the operator sees which rows hold the gate.
Only a confirmed empty read clears the gate: a successful query returning zero rows. While loading, or on an error or a 403, the submit stays disabled.
Enforced on both sides. The dashboard disables the submit. For enforce_schema roles the API runs the same query and answers a resolve with a canonical 422 while it returns rows, so the rule holds through the raw API. The server counts the true children whatever the resolver’s read scope, so a role that cannot see them cannot clear the gate. A resolve with no resolving user (an MCP resolve of an assigned: "me" guard) and roles without enforce_schema rely on the consuming workflow’s own check (reject and re-park with the remainder). Send to Triage stays available, for when the guarded work is itself the problem.
autoResolveWhenEmpty closes the loop. The claimed parent submits itself the moment the query is confirmed empty, rechecked on page load and after each inline child resolve. A person clears the children in place with x-lt-actions, and the parent closes with no extra click. A parent launched from a list with x-lt-submit-on-claim claims, shows its children and closes as they drain; a parent with an x-lt-transition then hands off to the follow-on. The parent’s own form must still validate; an incomplete parent waits for the person.
Composing with other tokens
Section titled “Composing with other tokens”The embed widgets compose with the rest of the vocabulary:
{ "related_link": { "type": "string", "readOnly": true, "x-lt-widget": "link", "x-lt-href": "/escalations/available?role=rel-originator&facets={\"orderId\":\"{{metadata.orderId}}\"}", "x-lt-span": 2, "x-lt-section": "Related resources", "x-lt-showIf": "metadata.orderId", "title": "Open originator queue for this order" }, "parent_esc": { "type": "string", "readOnly": true, "x-lt-widget": "escalation", "x-lt-source": "metadata.parent_escalation_id", "x-lt-span": 2, "x-lt-section": "Related resources", "x-lt-showIf": "metadata.parent_escalation_id", "title": "Originating escalation" }}x-lt-showIf hides an embed when its data is absent: the embedded escalation when parent_escalation_id is missing, or the list when metadata carries no customerId.
Security
Section titled “Security”RBAC is enforced on the server. The escalation and escalation-list widgets render only what the viewer’s roles permit. An escalation the viewer cannot access renders “No linked record”, the same as an unknown ID.
The link widget builds a client-side URL; the queue it opens applies its own RBAC.
Example workflow
Section titled “Example workflow”The related-escalations example workflow in the long-tail repo uses all three widgets in a two-stage review followed by a claimed walk (forms.ts and forms-walk.ts hold the schemas). The walk is the ownership-scope reference:
- Three item rows park in one role, each faceted with the walk’s
originId. - A walk-claim row parks in a second role. Resolving it is the “start walk” button: the resolver’s identity reaches the workflow through
$resolution, and one atomic bulk assign claims every item for that person. - The closeout row declares the walk query once,
{ role, facets: { originId: "{{metadata.originId}}" }, assigned: "me" }, consumed by the embedded list, its inline actions and the submit guard. The walker marks each item in place, the count beside the locked submit falls with each click, and the last item unlocks the submit.
The journey is two form submits and N inline clicks on one page.
