Skip to content

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,
});
inspector form (excerpt)
"defect": { "type": "string", "title": "Defect", "x-lt-options": "lookup.defects.items" }
  1. 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/entry takes the same body. A write that changes the data mints the next edition. A write that leaves the data unchanged mints nothing.

  2. Reference it from the wait with lookups.

  3. Point a form field at it with lookup.<as ?? key>.<path>.

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

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.

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.

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}}" }
}
}
catalog/geo
{
"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.

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:

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

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' }],
};

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.