ADR-196: The element draft offers the TCA's content types
- Status
-
Accepted (columns of the shared form no longer exclude a type — see ADR-205)
- Date
-
2026-09-21
- Amends
-
ADR-146 (its fixed type list and field set)
- Amended
-
2026-09-24 by ADR-205
- Authors
-
Netresearch DTT GmbH
Context
ADR-146 shipped create_ with a
static allow-list of four content types — header, text, textmedia,
bullets — intersected with the live TCA, and a fixed field set: headline,
body, column, language, position. Both were deliberate: the tool was the first
writer that brings a record into being, and the smallest commitment was the
right first shape.
A live editorial run on the Netresearch demo showed the cost. Its site package declares twenty-six content types; the tool could create four of them, and an assistant asked for a table, a quote or a card element had to answer that it could not. Eighteen of the twenty-six carry relations or a FlexForm and are out of reach for a draft tool whatever the list says; the remaining ones are prose elements with a few scalar options — a select, a check, an input — that the fixed field set could not fill either.
ADR-135 argued against a generic record editor because a narrow tool moves the decision from runtime to review time: what the tool can do wrong is bounded by its allow-list, and the allow-list is in the diff. That reasoning is not in question here. The question is whether the bound has to be a list of names, or whether a rule that is reviewed once can bound the tool just as well while following the installation's TCA.
Decision
The table stays fixed: tt_, one hidden element per call, through
the DataHandler as the acting user, with the approval pause, the preview and
the read-back of ADR-146 unchanged. What changes is where
the type list and the field list come from.
The offered types are read from the live TCA at call time. Every item of
tt_ is offered unless one of three things excludes it:
- it is on the deny-list —
list(the legacy plugin element),html(raw output),shortcut(a record reference),div(a divider) and everymenu_*type. They reference records or pages, or run code, and the deny-list is asked by name before the form is read, sohtmlstays out of reach on an installation where its form happens to be scalar; - it is a plugin. Since v13 a plugin is a content type of its own,
registered through
Extension, which puts the item in the group its caller names and, where no FlexForm is given, copiesManagement Utility:: add Plugin () header's scalar form onto the type — so the form alone would offer it.registerdefaults to thePlugin () pluginsgroup, but core's own indexed_search, felogin and form register intoforms, and indexed_search'sindexedsearch_has no FlexForm. A type is therefore a plugin when its item sits in thepi2 pluginsor theformsgroup, or when Extbase registered it:Extensionrecords every plugin underUtility:: configure Plugin () $GLOBALSand derives the content type as['TYPO3_ CONF_ VARS'] ['EXTCONF'] ['extbase'] ['extensions'] [<Extension>] ['plugins'] [<Plugin>] strtolower— the same keys and derivation in(<Extension> . '_' . <Plugin>) typo3/14.3.7 and 13.4.35;cms- extbase - its form holds an excluding column. The form is the type's
showitemwith every palette expanded — after core'sTcahas added the general, language, hidden and access palettes — and each column is classified by its TCA type, with the type'sPreparation columnsapplied.Overrides
Three kinds of column, and the kind decides:
fillableinput,text,check,number,datetime,radio,color,email, andselectwith static items and noforeign_. A model may set these throughtable fields.unfilledfile,category,linkandlanguage. These are the relations core attaches to prose —categoriessits on every content type,header_in the header palette every type uses,link assetsontextmedia— and they do not exclude the type. The draft leaves them empty;attach_or a human fills them later. This is the rule that keepsfile_ to_ content_ element textmediaoffered exactly as before.excluding- Everything else:
flex(a FlexForm is configuration, not prose),inline(child records),groupandfolder(record and folder references), aselectbacked by aforeign_,table slug,password,uuid,json,passthrough,user,none,image— and every TCA type this tool does not know, so a new type fails closed rather than open. One excluding column excludes the type. A type without a form is excluded too.Manipulation
The argument set keeps its seven arguments and gains ``fields``, an object
of column names to values. A key is accepted when it is a fillable column of
the chosen type's form and not one of the columns the tool refuses by name:
identity (uid, pid, CType, sorting, tstamp, crdate,
deleted, t3ver_*), position (col), visibility (hidden),
publication and audience (starttime, endtime, fe_,
editlock) and translation topology (sys_,
l18n_, l10n_, l18n_) — the exclusions
ADR-135 made for pages, by analogy — plus header and bodytext, which
are arguments of their own. The bodytext argument follows the same rule
as a key: it is refused for a type whose form does not show the column, where
the DataHandler would still write it under the column's base config.
Three kinds of fillable column stay in the form and are still refused as keys,
because the DataHandler bends them by a rule the draft cannot vouch for: a
check with several items is a bitmask, and 1 would set only its first
bit; a check with eval maximum or
maximum is unchecked again once enough other records
carry it; an input or email with eval unique or
unique is rewritten to a value no other record holds. They do not
exclude the type; the draft leaves them at their default. A column the type's
TCA declares read is refused the same way: the backend form shows it
without letting an editor change it, and the DataHandler does not read the
flag. A read-only header or bodytext refuses the call.
A value is validated against the column's TCA type, the way the DataHandler
reads it: a select against its static items, and against the acting user's
explicit_ where the column declares auth — for any
select, not only CType; a check as a boolean; a number as a whole number
(or a decimal where the column says so, with at most the two places the
DataHandler stores) within the TCA range, compared as the DataHandler
compares it — a decimal first rounded to two places, then rounded up against
the upper bound and rounded down against the lower — so a decimal inside a
fractional bound that the DataHandler would clamp is refused; a datetime as
anything PHP reads; an
input or text within the TCA max or the tool's own bound, and an input or
a text without the RTE not below the TCA min, a bound given as a numeric
string counting as the integer; a colour with an alpha pair only where the
column declares opacity; an email as a valid address. The first wrong key
or value refuses the whole call before anything is written, and the refusal
names the columns the type does offer.
Page TSconfig narrows the form per page, and the tool honours it.
TCEFORM. is applied by FormEngine only — the DataHandler stores
a type or an item the form would not have offered. After the page is
authorised, and so after the neutral refusal, the tool reads the page's
TSconfig as FormEngine reads it (Backend, with
<column>. merged over <column>.) and refuses a type that
CType. or CType. takes out of the selector, a
fields key, a body or a header whose column disabled hides or
config. makes read-only (not for a radio, where FormEngine's
override matrix does not take the key) — the header is written on every
call, so a hidden or read-only header refuses every call of that type on
that page — and a select value, a column or a
language that the keep or remove of its column (for the
last two col and sys_, which FormEngine filters in
Tca and Tca) removes; the refusal names the rule.
add is not read. Like FormEngine, it does not apply the item rules to
radio or check. Core itself ships one such rule —
TCEFORM..
The type list in the tool description is the TCA-level set and does not
depend on a page; TCEFORM narrows it per page at call time.
A ``datetime`` is handed to the DataHandler as the integer both supported
cores store verbatim — a Unix timestamp, or seconds of the day on a time
column. A string is not that shape: 13.4's Data reads it as UTC
wall time and subtracts the server's offset, so a day given as 2026-
would land on the evening before on any server outside UTC, while 14.3 reads
an offset correctly. A column with a native db takes unqualified local
wall time, which both cores store in the column's own format — the date, the
time, or both — without shifting it. The approval card is the human gate of
ADR-136, and a timestamp is not readable, so the card shows
the moment the integer stands for in the server's zone — `2026-` for seconds of the day — not the integer.
The exclude-field grant is asked before the write, per column, the way
ADR-192 asks it: the DataHandler drops an exclude column
the acting user holds no non_ grant for in silence and
creates the element without it, which is half of an approved draft. It is
asked for the columns the tool writes itself as well — the hidden column,
header, bodytext, and col and sys_ where they
differ from the 0 the silence would leave; core marks the hidden and the
language column exclude. exclude is read as a boolean cast, as core's
schema reads it, so an extension's integer 1 counts. Before the grant, the
chosen type is checked against the acting user's explicit_ where
CType declares auth: the DataHandler drops a disallowed type in
silence and creates the element as the default type. The read-back then
checks every column the call set, position and language included; a column
that still did not take is reported and the element is deleted again, exactly
as a visible element is. The report names both causes it cannot tell apart: a
value rewritten by TYPO3 under a rule this tool does not check, and a missing
grant. Three kinds are checked for presence rather than
equality, because the DataHandler rewrites them on purpose — a datetime is
normalised to its format and clamped to its range, a text column
with enable passes through the RTE transformation, and an
input with an eval beyond trim is transformed by it; a decimal is
compared as a number, because the database renders 12. as it likes.
The wire shape stays small. The description lists the types that pass
the exclusion rule, as before, says that the page's TSconfig may narrow them
and their options, and that fields takes further scalar columns of the
type.
The JSON schema declares fields as an object with free keys, in the
boolean additional form read_ ships — a type array
is a union type, which Gemini's schema dialect does not express — and its
description names the scalar kinds a value may take and where the allowed
keys come from. It does not enumerate the columns of every type: a model
that wants them reads the TCA through the structure tools, or sends a wrong
key once and is told.
Why a rule bounds the tool as well as a list did
ADR-135's argument was never that the list had to be short; it was that what
the tool can do wrong must be readable in the diff and reviewed once. A rule
satisfies that when its exclusions are explicit and finite, and here they are:
four type names, one prefix, two item groups and the Extbase plugin
registration; twelve column types, the record-backed select and every
type the tool does not know; three fillable kinds refused as keys; seventeen
column names and one prefix, the disabled column under whatever name
ctrl. gives it, plus the two arguments of their own; and the
page's TCEFORM rules. Everything a model can reach
is a scalar column of a prose element on one table, with the element hidden.
The blast radius is bounded by the exclusions, and the exclusions are what a
reviewer reads.
What the rule buys over the list is that it follows the installation. The demo's site package, a client's own elements, the next core version's new type: each is offered or excluded by the same rule, without a release of this extension, and an installation can read the rule's outcome in the tool's description.
What it costs is stated below.
What stays excluded, and why
- Plugins, raw HTML (
html), shortcuts, dividers and menus - Their payload references records or pages, or runs code. An assistant that
can place a plugin can place any plugin; an assistant that can write raw
HTML can write a script. The legacy
listelement,html,shortcut,divand the menus are denied by name, whatever the form says. A plugin content type is excluded by its Extbase registration or by thepluginsorformsitem group: without a FlexForm its form isheader's and would pass the column rule. - FlexForm columns
- Configuration, not prose, and its schema depends on a pointer field the tool would have to resolve. A type carrying one is excluded.
- Inline children, group and folder references, record-backed selects
- They create or reference other records. The one-record rule of ADR-146 is what makes every preview readable and every refusal whole; a type whose form needs children is a batch by another name.
- File references as arguments
- A
filecolumn does not exclude a type, and it is never an argument: the relation is a different risk class than a scalar, andattach_exists for it, with its own authorisation on the file side.file_ to_ content_ element - Links and categories as arguments
- Same shape as files: they do not exclude, and they are not filled. A link decides where a visitor is sent; a category is a reference to a record.
- Slugs and passwords
- A slug decides routing; a password is a credential. A content type carrying either is not a prose element.
- Access and publication columns
hidden,starttime,endtime,fe_,group editlock. Publication and audience are not editorial, andhiddenis the one value this tool exists to set itself.
Consequences
✓ Every scalar content type an installation declares can be drafted, with its options, through one tool — where four could be.
✓ The exclusions are in one place and are read once: the constants at the top of the tool, quoted above.
✓ textmedia is offered as before, media attached later; textpic,
image and core's table become reachable on a stock installation.
ADR-146's list had header, text, textmedia and bullets only.
✕ The description a model receives is now installation-specific, and so is the set of columns a call may carry. Two installations running the same extension version offer different things, and a prompt written against one may not transfer.
✕ A select with an items is validated against its static
items only; a value the processor would have added is refused.
✕ A check with several items is a bitmask the tool does not set; it is
refused as a key, and the draft leaves it at its default.
✕ A datetime, an RTE-enabled text column and an input with an
eval beyond trim are verified for presence, not for value. A column
the DataHandler clamped to its range reads back as present.
✕ A plugin that is not an Extbase plugin — registered through
Extension directly — and whose item sits in
a group other than plugins or forms is judged by its form alone:
excluded when it carries a FlexForm, offered when it does not. Nothing in the
TCA marks such a type as a plugin.
✕ category and link columns were meant to exclude a type and cannot:
core's own TCA puts categories on every content type and header_
in the header palette every type uses. Treating them as unfilled is the only
rule under which the four original types remain offered.
Revisit when
An installation asks for a type the rule excludes and the request is
legitimate — a select with a foreign_ that is a configuration
table rather than content, or a check with several items. The answer is
then a narrower classification, not a wider one.
Also revisit if a fields column is reported as not taken in practice after
the grant check passed. The read-back is the backstop for a silence the grant
check is meant to end; a case that reaches it names a second silence.
And when a second table wants the same shape. The rule is written for
tt_ and its system columns; a generic record editor is still the
question ADR-135 declined, and this record does not reopen
it.