ADR-180: The sixth writer creates a page, and the one-record rule holds 

Status

Accepted

Date

2026-08-20

Amends

ADR-146 (its "revisit at the sixth writer" trigger)

Authors

Netresearch DTT GmbH

Context 

Five writing tools exist (ADR-135, ADR-146). Two of them bring a record into being — a hidden content element and a hidden translation — and none of them creates a page. A backend assistant asked to "add a subpage with an introductory text" can therefore draft the text but has nowhere to put it, and answers that it cannot, which is correct and useless in equal measure.

ADR-146 named two triggers for coming back to the writers: a sixth writer, and any writer that needs to write more than one record per call. Both were on the table at once, because the obvious shape of this tool is "page and first element in one call" — one approval instead of two. This record is the review that trigger asked for.

The alternative that was evaluated and rejected is wrapping a third-party MCP server's generic WriteTable tool into the registry. It is one tool over the whole TCA — the shape ADR-135 refuses — it writes into a workspace of its own choosing with no way to publish from here, and as a remote tool it is admin-only by this extension's own rule. The page writer is a purpose-built action on the terms every other writer already meets.

Decision 

create_page_draft is the sixth writing tool, on exactly the terms of the previous five: disabled by default, in the editing group, an explicit ToolEffect (NON_IDEMPOTENT_WRITE — two calls leave two pages), a human approval before every call (ADR-134), a preview at suspend (ADR-136), a write through the DataHandler under the acting user's permissions, a read-after-write verification, and a refusal vocabulary that never confirms a uid exists. It uses the ADR-146 plan() shape, as that record asked a sixth writer to.

What it creates is as small as a page can be:

  • Always hidden. There is no argument to switch it off. Core's own default for a new page happens to be hidden too, but the tool does not rely on that — an installation may set TCAdefaults.pages.hidden = 0, and the read-back catches the page that came out visible and deletes it again.
  • Always a standard page. doktype is fixed. A shortcut, a mount point, a link or a folder carries configuration (a target, a mount, a URL) rather than content, and a model that needs one needs a different conversation with an editor first.
  • Always the default language. A page in another language is a translation of an existing page and has a tool of its own, create_translation_draft.
  • The field set is fixed: title, navigation title, position (parent and an optional after_page_uid that must be a subpage of the same parent). slug is left to the DataHandler's generator; hidden, doktype, fe_group, perms_*, is_siteroot, TSconfig, backend_layout* and every other page field are refused by name.

Authorisation is Permission::PAGE_NEW on the parent, checked by the tool and then again by the DataHandler, which for a new page additionally enforces tables_modify, the page-type grant (pagetypes_select) and the field-level grants. The DataHandler's own permission check on a new page reads the language field from the incoming record, so the tool states sys_language_uid = 0 explicitly — a non-admin is refused when it is missing.

The editor-action declaration (ADR-152) names pages with parent as the required argument that carries the uid: the record an editor selects is the page the new one goes under.

What the fourth plan() writer showed had become common 

ADR-146 asked, at the next writer, whether anything the shared trait had deliberately left per tool had become common. Two things had, identically in all four plan() writers, and a fourth copy of each was what the duplication gate refused: the guard execute() opens with (an acting user, a backend environment, the live workspace — answered as the user to write as, or the refusal) and the creation of one record through the DataHandler with the reading back of its uid. Both moved into PlansOneEditorialWriteTrait as writableActingUser() and createRecord(), and the three ADR-146 writers use them in the same change — a private copy per file would have relocated the duplication, not removed it. What stays per tool is plan() and everything that reads its shape: the record to write, the read-back and its fields, the success line. The two pre-ADR-146 writers are, as before, not retrofitted.

The one-record rule holds 

The tool creates the page and nothing on it. "Page plus first element" was the tempting shape, and it is refused for the reason ADR-146 gave in advance: the one-record rule is what makes every refusal whole and every preview readable. An approver reading a card that says "a page and an element" is judging two records with two permission sets in one click, and the refusal vocabulary — "the whole call is refused rather than partially applied" — would need a second meaning for the case where the page may be created and the element may not.

A model that wants both calls create_page_draft and then create_content_element_draft on the uid it was given; the page tool's success message hands over that uid and names the next tool. Two approvals is the cost, and it is the right cost: each card shows one thing, and the person approving the element can see the page it goes on.

The mechanism that does span records already exists and is not this: ADR-162's batch planner runs one editor action over several records as N ordinary runs — N approvals, N audits — and is the answer to "ten pages under this parent", not a bigger single tool.

Consequences 

✓ Six editorial writes are available where five were; a backend assistant can now build a subpage and its first text, each step approved and each step visible to the approver as one record.

✓ The write fence, the approval pause, the preview and the fail-closed audit apply without the tool arranging any of it (ADR-141).

✓ The read-back's safety net is exercised by a test that sets the installation-level default to visible, so the net is known to hold on the installation where it matters rather than only on stock core.

✕ Two approvals for "page with text". A person who wants one click per page will ask for a combined tool; the answer is this record, not a new one.

✕ The page arrives without content and without properties beyond its title, which is less than the backend's new-page wizard offers. That is the point, not a gap: the wizard is for editors, who may set everything; the tool is for a model, which may set what it can be held to.

Revisit when 

A writer is proposed that genuinely needs more than one record per call and cannot be expressed as two tools in sequence or as a batch over one action. The question then is what the approver agreed to, and it has no answer in this record.