Cross-app lookups

The lookup block in schema.json — the one declaration that lets another app's admin UI resolve your ids to a label, and the privacy boundary it draws.

lookup is the app saying: other apps' admin UIs may resolve my ids to this label, search me by these columns, and see these extras.

schema.json
"lookup": {
  "label": "serial",
  "search": ["serial"],
  "columns": ["status", "sold_at"]
}
FieldRequiredDescription
labelYesA declared text column rendered as the human label. Must be text — the label is shown to a human and searched case-insensitively.
searchNoDeclared text columns the lookup searches. Defaults to [label].
columnsNoExtra declared scalar columns returned alongside id and label.

Requires a uuid id column.

Why it exists

An app stores another app's identifier as a plain uuidforeign keys stop at the app boundary. But a form asking an operator to type a product's UUID is not a form anybody can use. The relation field in a cockpit.json form needs to show names and let the operator search them.

lookup is what makes that possible without either app knowing about the other:

cockpit.json
{ "name": "product_id", "label": { "en": "Product" }, "type": "relation",
  "required": true, "relation": { "entity": "products", "label": "name" } }

The products app declared a lookup on its products entity; your form resolves ids through it. You declared nothing but the field.

This declaration is the privacy boundary

lookup is the only thing that ever leaves the app through the cross-app lookup API. The platform grants access to exactly these columns, and nothing else on the table is reachable from another app — not by guessing a column name, not by a filter, not at all.

That is why the allowed types are narrow. jsonb, json, bytea and the array types are deliberately not permitted in columns, because those are what would smuggle a whole document through what is supposed to be a label API.

So treat the block as a published interface, and think about it the way you would think about a public response schema:

  • Do not put anything in label that a neighbouring app's operator should not see. A customer app's contact label is a person's name, which is exactly what a picker needs — and also exactly the sort of thing worth a moment's thought before it appears in an unrelated app's dropdown.
  • Keep columns to what disambiguates. A status, a code, a date — the fields that tell two similar rows apart in a list. Not the whole record.
  • Omit lookup entirely on an entity nobody should pick from. An audit log, a job queue, an internal mapping table: no lookup, no exposure.

What it is not

It is not an API for your own app. Inside your function you read your tables with the typed client, which sees every column.

It is not how one app reads another app's data. That is a capability grant and a call through the gateway. lookup resolves an id to a label for a UI; a capability answers a question.

It is not a search index. search names the columns the picker's type-ahead matches, which is a narrow substring search over a handful of text columns. Full-text search over your entity is search.json.

Next steps

Was this page helpful?