Cards and layout
Cards turn a list of columns into something readable. They work identically on both sides of the config.
Omit them entirely
Section titled “Omit them entirely”"input": { "fields": [ /* … */ ] }With no cards, MLabel renders one implicit card holding every field, one per row. For a
config with three or four fields that is exactly right, and six lines of layout describing
the only possible arrangement would be noise.
Declaring them
Section titled “Declaring them”"cards": [ { "name": "sample", "display": { "title": "Sample", "description": "The prompt/response pair under review." }, "rows": [ { "use": ["prompt"] }, { "use": ["response"] }, { "perRow": 3, "use": ["model", "created", "score"] }, ], }, { "name": "signals", "display": "Automated signals", "rows": [{ "perRow": 2, "use": ["flagged", "tags"] }, { "use": ["checks"] }], },]nameidentifies the card and must be unique within its side.displaytakestitle,descriptionandhelp— see Display.rowsis a list, rendered top to bottom.usenames the fields in that row, in order. Every name must be a declared field on the same side, or the config is rejected.perRowis how many columns to fit before wrapping. It defaults to the number of fields in the row.
perRow
Section titled “perRow”perRow is a target, not a hard cap — the layout still wraps on a narrow window. Setting it
lower than the number of fields is how you get a deliberate grid:
{ "perRow": 2, "use": ["a", "b", "c", "d"] } // two rows of two{ "use": ["a", "b", "c", "d"] } // one row of four, wrapping as neededFields you leave out
Section titled “Fields you leave out”A field that no card names is simply not displayed. That is a legitimate way to declare a column so it can be copied to the output without cluttering the screen — but it is also an easy accident. If a field seems to be missing from the UI, check whether any card lists it.
Session cards
Section titled “Session cards”An output card can render on the setup screen instead of once per record:
"cards": [ { "name": "run", "scope": "session", "display": { "title": "About this run", "description": "Asked once, applied to every row." }, "rows": [{ "perRow": 2, "use": ["annotator", "guidelineVersion"] }], }, { "name": "labels", "display": "Labels", "rows": [{ "use": ["verdict"] }], },]scope defaults to record.
Ordering
Section titled “Ordering”Cards render in declaration order, rows within a card in declaration order, fields within a
row in use order. Layout is entirely positional — there is no sorting or auto-placement to
reason about.
Laying out for reading
Section titled “Laying out for reading”A few things that consistently help:
- Put the subject first, alone, full width. The thing being judged deserves its own row
with
titlePosition: "above". - Group metadata. Three short scalars in one row with
perRow: 3reads as a header strip rather than three separate facts. - Separate provenance from evidence. “What the model said” and “what upstream tooling computed” are different kinds of information; different cards make that obvious.
- Keep the output form short. If the form does not fit on screen beside the record, the labeler is scrolling on every single item.