Skip to content

Forms

Give a role a JSON Schema and every row on that role gets a resolve form: fields, validation, conditional sections, layout and a help panel, all rendered from the schema. The schema is versioned, rows pin the version they were written for, and the server can enforce it on every surface.

inspector form
{
"title": "Inspect part",
"type": "object",
"x-lt-layout": "two-column",
"x-lt-help": "### {{metadata.serialNumber}}\n\nCheck the part against the drawing in the envelope.",
"required": ["result"],
"properties": {
"result": {
"type": "string",
"title": "Result",
"x-lt-options": [
{ "value": "pass", "label": "Pass" },
{ "value": "rework", "label": "Send to rework" }
]
},
"defect": {
"type": "string",
"title": "Defect",
"x-lt-showIf": "resolver.result=rework",
"x-lt-options": "lookup.defects.items"
},
"measurement": { "type": "number", "title": "Measured length (mm)", "minimum": 0, "x-lt-maximum": "envelope.maxLength" },
"notes": { "type": "string", "format": "textarea", "title": "Notes", "x-lt-span": 2 }
}
}

The role is the unit of UI: the role decides who sees a row, its schema decides what they see, and the version decides which edition renders. A workflow can park rows on many roles, and each role owns its own form, so declare every resolve form on its role.

Declare the form in code with start():

src/index.ts
await start({
database: { connectionString: process.env.DATABASE_URL },
roles: [
{ role: 'inspector', title: 'Inspector', form_schema: INSPECTOR_FORM, enforce_schema: true },
],
workers: [/* ... */],
});

Or save it at runtime:

Surface Save a new version
HTTP PATCH /api/roles/:role with form_schema and an optional change_summary
SDK lt.roles.update
CLI ltc roles save-schema inspector --file schema.json --summary "Added defect"
MCP update_role
Dashboard The schema editor at /admin/roles/:role/schema

Every save that changes a role’s form appends an immutable snapshot to lt_role_schemas and advances the role’s current version. A role’s first form is version 1.

const decision = await conditional<InspectionV3>(signalId, {
role: 'inspector',
metadata: { serialNumber },
schemaVersion: 3, // renders and validates v3, whatever comes later
});
Behavior Detail
Pinned row Renders and validates against its snapshot forever. The pin is stored as metadata.schema_version
Unpinned row Uses the role’s latest form when the row is fetched
Missing version A pin naming a version that does not exist fails at creation
Evolve Change the schema, bump the pin and the payload type together. Open rows keep their edition

Inspect versions with GET /api/roles/:role/schema?version=N, GET /api/roles/:role/schema/versions, lt.roles.getSchema, lt.roles.listSchemaVersions, ltc roles schema <role> --version N, ltc roles schema-versions <role>, or the get_role_schema and list_role_schema_versions MCP tools.

  1. metadata.form_schema, a full schema embedded on the row, when present.
  2. The role’s snapshot at metadata.schema_version, when the row is pinned.
  3. The role’s latest form.

Tokens in the form read from context domains with domain.path (in conditions and option paths) or {{domain.path}} (in text templates):

Domain Reads
escalation The row’s own columns, such as {{escalation.id}}
metadata The row’s facets
envelope The row’s unindexed context
payload The data the workflow attached on the legacy path
resolver The live form values as the person edits
lookup Knowledge entries the row references. See Lookups

Two envelope keys shape the form. envelope.formDefaults prefills fields. x-lt-bind on a field maps the flat form value into a nested resolver payload ("x-lt-bind": "customer.email"), and prefill reverses the same map.

Declared type Renders as
boolean Checkbox
boolean with x-lt-options A select emitting true or false
number, integer Number input
string Text input. format adds date, date-time, email, uri, password, textarea
scalar with enum or x-lt-options Select
array with x-lt-options Multi-select
object Nested fields, or a widget

A password field is replaced by a short-lived ephemeral token before the payload is stored. Plain JSON Schema keywords (required, minimum, maximum, minLength, maxLength, pattern, readOnly, title, description) work as written.

The tokens group by job. Each links to the reference.

Group Tokens Use it to
Layout x-lt-layout, x-lt-order, x-lt-section, x-lt-span, x-lt-bind, x-lt-help Arrange fields in sections and columns, map them to the payload, write side-panel help
Display x-lt-display Render read-only facts as a dense label and value list
Show if x-lt-showIf, x-lt-hide-if-empty, x-lt-hidden Show a field only when another value says so
Validation x-lt-options, x-lt-minimum, x-lt-maximum, x-lt-min-length, x-lt-max-length, x-lt-pattern-error, x-lt-require-any, x-lt-require-sum, x-lt-nullable Constrain choices and bounds, including bounds read from the row
Widgets x-lt-widget (file-upload, code-editor, json, signature, rich-text, markdown, checklist, attachment), x-lt-source, x-lt-require-all, x-lt-default-checked Rich inputs, and checklists driven by runtime data
Embeds x-lt-widget (link, escalation, escalation-list), x-lt-href, x-lt-query, x-lt-columns, x-lt-actions, x-lt-submit-guard Show related rows in the form, and hold submit until they drain
Footer x-lt-labels, x-lt-submit-on-claim Rename the footer controls, or claim and submit in one gesture
Transition x-lt-transition, x-lt-transition-message, x-lt-transition-done Wait after submit and open the follow-on row for the same person
Roster x-lt-widget: "roster", x-lt-roster List every member a container expects and check each off as it arrives
Scan x-lt-scan, x-lt-scan-submit Fill a field by scanning, checked against what the row expects
Invoke x-lt-widget: "invoke", x-lt-invoke Start another workflow from the form with a payload mapped from the row
List schema list_schema with x-lt-layout (active-history, facet-table, facet-board), x-lt-card, x-lt-row-action Format the role’s whole list page
Viewport x-lt-viewport Replace the generated form with your own app in an iframe

x-lt-showIf takes a domain.path. The field shows when the value is truthy. ! inverts, =VALUE and !=VALUE compare, and an array requires every condition. resolver. conditions react live as the person edits:

"removal_reason": { "type": "string", "x-lt-showIf": "resolver.decision=remove" }

A field hidden at submit time never blocks submission, on the client or on the server.

The dashboard runs one validation pass before submit and lists every error in a side panel. With enforce_schema: true on the role, the server runs the same module on every resolve surface and answers violations with one 422 body. See Schema enforcement.