Lookups
Keep a pick list in one place and let every row’s form read it. A lookup is a knowledge entry. A row carries a small reference to it, and the form reads the content through the lookup.* domain. Every change to the entry is a new immutable edition, so a row can follow the newest list or pin one exact edition.
const decision = await conditional<DefectReportV1>(signalId, { role: 'inspector', description: 'Record the defect for this part', metadata: { serialNumber }, lookups: [{ domain: 'catalog', key: 'defects' }], schemaVersion: 1,});"defect": { "type": "string", "title": "Defect", "x-lt-options": "lookup.defects.items" }Set it up
Section titled “Set it up”-
Store the list as a knowledge entry. A flat option list lives at
data.items.await lt.knowledge.storeEntry({domain: 'catalog',key: 'defects',data: {items: [{ value: 'scratch', label: 'Surface scratch' },{ value: 'warp', label: 'Warping' },{ value: 'crack', label: 'Crack' },],},replace: true,});POST /api/knowledge/entrytakes the same body. A write that changes the data mints the next edition. A write that leaves the data unchanged mints nothing. -
Reference it from the wait with
lookups. -
Point a form field at it with
lookup.<as ?? key>.<path>.
The ref
Section titled “The ref”{ "domain": "catalog", "key": "defects" }{ "domain": "catalog", "key": "defects", "version": 2, "as": "defectsV2" }| Field | Required | Meaning |
|---|---|---|
domain |
Yes | Knowledge domain |
key |
Yes | Entry key within the domain |
version |
No | An edition to pin. Absent or "current", the ref follows the newest edition |
as |
No | The name the form reads it under, when the key alone is ambiguous |
Refs are stored in envelope.lookups, beside the other form context, and never in the indexed metadata. A malformed ref throws before the row is written, and the HTTP create surface answers 400.
Current or pinned
Section titled “Current or pinned”| Ref | Pending row | Closed row |
|---|---|---|
Current (no version) |
Reads the edition in force when the form renders, so a row open for days shows today’s list | Reads the edition that was current when it closed, so history shows the list the work was done against |
Pinned (version: N) |
Always reads edition N | Always reads edition N |
A value picked from a current list is validated against the edition in force at submit. An option removed while the form was open is refused with the canonical 422, and the person picks again.
Form schemas stay pinned with schemaVersion. The lists they read follow the knowledge entry.
What the form can do with a lookup
Section titled “What the form can do with a lookup”The whole x-lt vocabulary reads the lookup domain:
{ "properties": { "defect": { "type": "string", "x-lt-options": "lookup.defects.items" }, "checks": { "type": "object", "x-lt-widget": "checklist", "x-lt-source": "lookup.checks.items", "x-lt-require-all": true } }}Option entries are scalars or { value, label } objects ({ id, label } also works). The select shows the label and submits the value, so a pick list can show text and store a foreign key.
Cascading selects
Section titled “Cascading selects”An option path can interpolate another answer with {{domain.path}}, so one choice drives the next field’s legal set:
{ "properties": { "country": { "type": "string", "x-lt-options": "lookup.geo.countries" }, "region": { "type": "string", "x-lt-options": "lookup.geo.regions.{{resolver.country}}" } }}{ "countries": ["US", "EU"], "regions": { "US": ["CA", "NY", "TX"], "EU": ["DE", "FR", "ES"] }}Region stays disabled until Country has an answer, then fills with that country’s list. Changing the country resets a region that is no longer legal, and a stale value fails validation on submit. Cascades resolve in memory from one fetch.
Move a field to a lookup without stranding rows
Section titled “Move a field to a lookup without stranding rows”x-lt-options and x-lt-source accept an ordered array of paths, and the first that resolves wins:
"reason": { "type": "string", "x-lt-options": ["lookup.reasons.items", "envelope.reasonItems"] }New rows carry the ref and render from the edition. Rows parked before the change have no ref and fall through to the envelope list they were written with.
Who can read a lookup
Section titled “Who can read a lookup”The refs on a row are the grant. Anyone who may read the row may fetch exactly the entries it names, at the editions those refs resolve to:
GET /api/escalations/:id/lookups# { "lookups": [{ "domain": "catalog", "key": "defects", "version": 4, "current": true, "data": { "items": [...] } }] }The general knowledge API (/api/knowledge/*) is a builder surface for superadmins and engineers. Members read lookups only through the rows they work. A ref whose edition does not exist answers missing: true for that ref, and the rest of the batch still resolves.
Invoke forms read lookups too
Section titled “Invoke forms read lookups too”A worker config declares inputLookups beside inputSchema, and the Invoke Tool form reads them the same way. Each config ref is checked on save: a malformed ref, or one that names no edition, answers 400 naming the editions that exist.
const config: LTWorkerConfig = { invocable: true, inputSchema: INSPECT_INPUT_SCHEMA, inputLookups: [{ domain: 'catalog', key: 'serial-numbers', version: 1, as: 'serials' }],};Versions
Section titled “Versions”Inspect an entry’s editions with GET /api/knowledge/entry/versions?domain=catalog&key=defects, ltc kb versions catalog defects or the list_knowledge_versions MCP tool. Fetch one edition with ?version=N on the entry endpoint.
