Fill — where values come from
Every output field has a fill: where its value comes from. It defaults to user.
{ "name": "verdict", "type": "text" } // fill: user{ "name": "id", "type": "text", "fill": { "kind": "copy" } } // copiedFill is independent of the type and of the widget. Type says what the value is, widget says what it looks like, fill says who provides it.
The four kinds
Section titled “The four kinds”The labeler answers it on every record. This is the default and the common case.
session
Section titled “session”The labeler answers it once, on a setup screen before labeling starts, and the answer is written onto every exported row.
{ "name": "annotator", "type": "text", "fill": { "kind": "session" } },{ "name": "guidelineVersion", "type": "enum", "fill": { "kind": "session" }, "choices": [{ "name": "v3" }, { "name": "v4" }],},Use it for facts about the run rather than the record. A session field renders a widget like any other — just on a different screen.
Carried over from an input column. Renders no widget — nobody is asked.
// Same name on both sides.{ "name": "id", "type": "text", "fill": { "kind": "copy" } },
// Renamed on the way out: input `id` becomes output `sourceId`.{ "name": "sourceId", "type": "text", "fill": { "kind": "copy", "from": "id" } },
// One input column copied into two output columns is fine.{ "name": "auditId", "type": "text", "fill": { "kind": "copy", "from": "id" } },from defaults to the field’s own name.
timestamp
Section titled “timestamp”Stamped by the app when the record becomes complete, and re-stamped on every later edit to it. Renders no widget.
{ "name": "labeledAt", "type": "date", "fill": { "kind": "timestamp" } }The value reflects when the record was finished, not when it was first opened.
Interactive vs derived
Section titled “Interactive vs derived”| Fill | Renders a widget? | Required by default? |
|---|---|---|
user |
yes | yes |
session |
yes | yes |
copy |
no | no |
timestamp |
no | no |
The default is the sensible one: something a person is asked for is required; something
the app derives is not. An explicit required always wins.
{ "name": "notes", "type": "text", "required": false }, // optional{ "name": "id", "type": "text", "fill": { "kind": "copy" }, "required": true }, // must be presentBecause a derived field renders nothing, naming a widget or a shortcut on one is an
error — there would be nothing to render or focus:
{ "name": "id", "type": "text", "fill": { "kind": "copy" }, "widget": "text" }// ✗ A "copy" field renders no widget.Why fill is explicit
Section titled “Why fill is explicit”Earlier versions inferred it: an output field was copied when its name happened to match
an input column, and control: "hidden" conflated rendering with sourcing. That made three
reasonable things impossible — renaming a copied column, capturing into a field whose name
collides with an input column, and copying one input into two outputs.
Stating it outright costs one line and removes the whole class of surprise.
What actually gets written
Section titled “What actually gets written”A row’s exported values come from three places, merged in one place so that the progress bar, the form and the export can never disagree:
per-record labels ──┐session answers ──┼──▶ the exported rowtimestamp ──┘Copied values are seeded when the record loads, so they are present from the start.