ui-hints.yaml
ui-hints.yaml is an optional file for presentation overrides that
have no natural home in the database. Your DDL and your COMMENT
text already give Kozou enough to render a usable Admin UI, expose a
REST API, and emit MCP context — ui-hints.yaml exists only for the
handful of cosmetic decisions that are not properties of the schema
itself, such as forcing a particular input widget on a column or
giving a table a friendlier label.
Because the database is the single source of truth (see
Postgres as source of truth),
this file is deliberately small. The system works without it. Reach
for it only when a default is wrong and there is no good way to fix
it in COMMENT.
It is optional
Section titled “It is optional”Kozou runs with no ui-hints.yaml at all. When the file is absent —
or present but empty — every label, widget, and display field comes
from the schema plus sensible defaults. create-kozou scaffolds a
near-empty ui-hints.yaml (an tables: {} placeholder with
commented examples) precisely so you can ignore it until you have a
reason not to.
The path is configured in kozou.config.yaml under uiHints.path,
and missing files are tolerated:
uiHints: path: ./ui-hints.yaml # the file need not existSee kozou.config.yaml for the rest of the configuration surface.
Precedence
Section titled “Precedence”For each property Kozou resolves a value from up to three sources, in order. The first source that supplies a value wins:
| Property | Resolution order |
|---|---|
| Column widget | ui-hints.yaml → @widget COMMENT tag → heuristic default |
| Column label | ui-hints.yaml → first line of the column COMMENT → column name |
| Table label | ui-hints.yaml → first line of the table COMMENT → table name |
| Display field | ui-hints.yaml → heuristic default |
A ui-hints.yaml entry therefore overrides both the
@widget COMMENT tag and the
heuristic Kozou would otherwise infer. It sits at the top of the
chain: if you set a widget here, the @widget tag in the column’s
COMMENT is ignored for that column.
The heuristics it overrides are the same defaults documented for
emitted surfaces: foreign keys default
to a relation-select, columns with a detectable enum to an
enum-select, uuid columns to uuid, numeric types to number,
and so on. The display field falls back to the first column that
exists among name, title, label, display_name, name_ja,
name_en, then the primary key.
What it can override
Section titled “What it can override”The file has two top-level keys, tables and views, each a map
keyed by object name. Every field below is optional — include only
what you want to change.
- Per-table
label— the display name shown for a table or view. - Per-table
displayField— the column used to represent a row when it is referenced elsewhere (for example, the text shown for a foreign-key relation instead of the raw id). - Per-column
label— the display name shown for a column. - Per-column
widget— the input/display widget for a column. One of:text,textarea,number,boolean,date,datetime,enum-select,relation-select,json,image-url,uuid,currency. - Per-column
readonly— mark a column as read-only in the Admin UI. - Per-column
relation— for arelation-selectcolumn, alabelFieldandsearchFields[]controlling how the referenced rows are labelled and searched.
Column ordering follows the order in which you list columns under a table or view: entries are written as a map, and the keys are presented in source order. Columns you do not mention keep their schema position.
There is no per-column description or @ai equivalent here — that
text belongs in the database COMMENT, where every emitted surface
can read it.
tables: <table_name>: label: <string> displayField: <column_name> columns: <column_name>: label: <string> widget: <widget> readonly: <bool> relation: labelField: <column_name> searchFields: [<column_name>, ...]
views: <view_name>: label: <string> columns: <column_name>: label: <string> widget: <widget>Example
Section titled “Example”Suppose products has a status column constrained to
draft / published / archived. Kozou already detects the enum
and renders an enum-select, but you want a clearer label, and you
want the long description column to use a multi-line editor:
tables: products: label: Catalog displayField: name columns: status: label: Publication status widget: enum-select description: widget: textarea sku: readonly: trueHere the status label and widget, the description widget, and the
sku read-only flag override whatever the COMMENT tags or
heuristics would have produced — everything else on products is
left to the defaults.
A relation override looks like this, for an orders table whose
author_id points at an authors table:
tables: orders: columns: author_id: label: Author widget: relation-select relation: labelField: full_name searchFields: [full_name, email]Keep it minimal
Section titled “Keep it minimal”Every entry in ui-hints.yaml is a fact about your data expressed
outside your database — which means it can drift out of sync with the
schema, and it is one more file to keep in agreement with the
COMMENT text. Treat that as a cost.
- Prefer fixing presentation at the source. A wrong widget is often
better corrected with a
@widgettag in the columnCOMMENT, and a wrong label with a clearer first line in theCOMMENT— both travel with the schema and are visible to AI agents reading MCP context. - Use
ui-hints.yamlfor the residue: choices that are genuinely about presentation and have no home in SQL, or one-off overrides where editing the databaseCOMMENTis not worth it. - Validation runs on load. If you reference a table, view, or column that does not exist, Kozou warns; an unparseable or schema-invalid file is reported as an error. Keeping the file small keeps those references easy to verify against the schema.
The goal is a ui-hints.yaml that is mostly empty, holding only the
overrides the database cannot express.
See also
Section titled “See also”- Postgres as source of truth — why the schema, not this file, is authoritative.
- COMMENT conventions — the
@ai,@widget,@policy, and@exampletags, including the@widgettag this file overrides. - kozou.config.yaml — where
uiHints.pathis set.