The reusable field registry, data type vs editor, field sets and namespaces, options vs content.
The single-line string family — one stored contract, three variants that differ only in what the boundary promises.
The paragraphs family — one text data type, two variants differing in what the value promises.
One yes-or-no fact, stored true/false — three affordances, and the three-state smell that outgrows them.
Declared legal values — four methods, two stored shapes, cardinality is the contract.
Select · Radio · Checkboxes · Multi-select
One temporal data type, three granularities — the method fixes which form a field stores.
Date & Time · Date · Time
The non-image kind — documents and uploaded video; linked or embedded, never rendered.
An ordered list of media, where the order is content.
Point at another asset — identity, not a copy; one authoritative source, followed at render.
Fields that travel together — declared once, nested, judged where they live.
Rows of the same shape — order is content, bounds are rules, conditions gate per row.
Fields are the data schema of everything authored. They are site-level,
reusable definitions: a field is created once, in the site's registry, and
assembled into content types — the same start_date can serve an Event, a
Class, and a Special without being declared three times. A content type
(see chapter 1) is, at its center, a list of fields.
Every field carries three names, doing three jobs:
| Name | What it is |
|---|---|
| Label | The display name editors see. Leads the create form — type "Start Date" and the rest follows. |
| Key | The public data identifier (start_date) — what templates and the API read. Minted from the label; letters, numbers, hyphens, underscores. |
| id | The stable internal identity (fld_…). Storage keys on it, so a field can be relabeled — or rekeyed — without orphaning stored values. |
The registry lives at Manage Site → Fields (the surface is covered in the admin guide), organized by the same folder primitive as templates and types.
A field declares two separable things: what is stored and how it is edited. The data type is a contract; the editor is a choice — and most of what feels flexible about the field system comes from refusing to fuse them.
string, text, number, boolean, date, json, media,
reference, collection.text_input, textarea,
rich_text, date_picker, date_time_picker, toggle,
media_picker, image_picker, file_picker, a select with declared
options, a reference picker, raw JSON.This is ProcessWire's insight, and the credit belongs there. ProcessWire separates the Fieldtype (what a field stores, how it queries) from the Inputfield (the form control that collects it), and lets a Fieldtype offer a choice of Inputfields — their Textarea can be edited as a plain textarea or a rich-text editor, and both are modules a third party can add to. Twenty years of that architecture holding up is most of the argument for it; ours is the same idea with the names data type and editor.
The alternative is the common one: a flat list where each field type IS its editing widget. It reads simpler until the first time one stored shape needs a second control — then either the widget grows modes, or a parallel field type appears whose values are the same but whose name is different, and templates start caring which control the author picked. Templates should never know that.
The same stored shape, edited three ways, chosen by the shape of the set and never changing what a template reads:
| Stored (data type) | Editors | Choose by |
|---|---|---|
text |
textarea · rich_text |
whether authors may format |
string + options |
select |
one from a closed set |
json (list of option values) |
checkbox_group · multi_select |
set size — boxes to about seven, the dropdown past that |
media |
image_picker · file_picker · media_picker |
what the field accepts |
date |
date_picker · date_time_picker |
whether the hour matters |
Switching the multi-choice field from checkboxes to the dropdown is the same move as switching a bio from textarea to rich text: an editing decision, made per field, with the stored value untouched. The options page walks both cardinalities of the worked example.
Editors are modules. Each one registers which editors it renders and which data types it can serve — that pairing is the compatibility contract, and the registry derives the choices the admin offers from it. The core set covers the table above; a module can add an editor (or deliberately replace one) without touching the data types, which is also ProcessWire's arrangement: Fieldtypes and Inputfields ship as plugins, and most third-party ones arrive in pairs.
Two consequences worth naming:
content.audiences
is a list of option values whichever control collected it. Nothing
downstream — templates, the API, queries — can tell the editors apart,
which is the test the split has to pass.Two systems worth reading beside this chapter, one for each side of the argument:
text → textarea | rich_text,
twenty years older.complex field
is also the named-groups precedent this system's field conditions were
measured against.A field's editor can change after content exists. Storage keys on the
field's identity, values are validated against the data type, so
flipping audiences from checkbox_group to multi_select re-renders
the form and re-reads the same stored list. The reverse — changing the
data type — is a migration, not a preference, which is exactly the
asymmetry the two names are there to keep visible.
A content type declares one field list, and every entry carries a
namespace. The sets emerge from the tagging — "the content fields" are
simply the entries tagged content:
| Namespace | What it holds |
|---|---|
content |
The asset's own data — what the thing says. |
meta |
Title/SEO/social fields, attachable as schemas. |
options |
Render configuration — what templates and layouts read. |
Values follow the same shape, nested under one container:
data.content, data.meta, data.options. A type with no options
declares no options set — absence costs nothing, in storage, in
projections, and in the editing UI.
Options deserve their own sentence because they are structurally fenced: they are render config (width, alignment, variant…), read by templates and layouts, and excluded from every SEO, sitemap, and social surface. They also have their own transport: when a fragment output is mounted with per-mount options (see rendering), the call-site values merge over the asset's stored options — never into its content. Content is what the asset says; options are how this rendering of it behaves.
Templates read keys, never storage ids:
<time datetime="<%= cms.asset.content.start_date %>">
<%= cms.asset.content.start_date %>
</time>
The same address works on every projection of an asset — the current page
(cms.asset.content.*), a query row (row.content.* from
cms.assets.find()), and a fragment render all decode the stored bag
through the type's field definitions, so one partial can read any of them.
The namespaced bag is also addressable directly: cms.asset.data.content.*,
data.meta.*, data.options.*.
A field can declare two rule bags, and they are two different mechanisms:
sanitize transforms. It canonicalizes the value — trim,
collapse_whitespace, lowercase/uppercase, strip_html — and what it
produces is what gets stored. Rules apply in a fixed canonical order and
every rule is idempotent, so a re-save changes nothing.validate judges. It checks the sanitized value — required,
min/max, max_length, pattern (with an optional message), email
(the WHATWG definition — what the author's browser would say), url —
and reports issues without changing anything.Sanitize runs first, so validation and storage always see the same value. Both bags are plain data on the field definition, enforced per data type at the write boundary, whichever lane the write arrives through (the admin, the web API, or an agent). Issues come back with the write response — path, code, and a human message per problem.