ADR 266 · Accepted

Move, copy, delete and translate act in the workspace the run is in

Context

ADR-260 and ADR-262 let the writers that change fields write a workspace version in the draft workspace a run was started in. The writers that change the structure — move_content_element, move_page, copy_record, delete_record and create_translation_draft — still refused a draft workspace, after the card.

Core's commands do different things in a workspace. Measured on 2026-11-05 with ToolDataHandler and an administrator put into a draft workspace, on TYPO3 14.3.10 and 13.4.37 (SQLite) and 14.3.10 (MariaDB), every row of pages and tt_content dumped after each command, with the workspace aspect of TYPO3's Context set to that workspace as the tools now pin it (an earlier round measured with the aspect left at live, see Consequences):

  • move (DataHandler::moveRecord()): a version of the record with t3ver_state 4 carrying the new pid, sorting and, from the paste update, the new colPos; its translations get a move version too. "After element X" resolves to X's place in the workspace, a target page new in the workspace is accepted, and a record new in the workspace is moved in place. Live is unchanged.

  • copy (copyRecord(), copyPages()): every copied row is new in the workspace (t3ver_state 1), copied from the workspace's version of the source; a page's content is copied as the workspace shows it, with the translations of its elements.

  • delete (deleteAction()): a live record becomes a delete version (t3ver_state 2), and so do its translations; a record changed in the workspace has its version turned into one. A page is marked with its translation only: its subpages and content get no version, and publishing the workspace deletes the live branch. Two cases are not a deletion: a record new in the workspace is discarded — removed from the database, beyond the recycler — and a record moved there has its move version discarded: it is back at its live place and nothing is deleted.

  • localize (localize()): the translation is new in the workspace, made from the workspace's version of the source and placed where the workspace shows it. Deleting an existing translation and localizing again — this tool's overwrite — differs between the cores: 14.3.10 marks the old translation and creates the new one; 13.4.37 marks the old one and then refuses the localization ("There already are localizations").

Core resolves the version of a moved or deleted translation against the parent's version: its l18n_parent names the parent's version uid.

Decision

The five writers act in the run's draft workspace, as the field writers do under ADR-260: they refuse a draft workspace only where the run did not record it, and their DataHandler, started with the pinned user, produces what core produces there. Live is not touched.

They run in that workspace. execute(), the card and the refusal before approval set the workspace aspect of TYPO3's Context and the workspace of the global backend user to the acting user's, and put both back (AsksItsRefusalFirstTrait). Core reads both: the site lookup reads the rootline through the aspect, which decides whether a page new in the workspace has a site and so whether a copy brings its elements' translations and a record there can be translated; workspaceOL() and, in TYPO3 13.4, the page translations a copied element's translation needs read the global user's workspace. Left ambient, these are the approving editor's when the resume runs in a backend request and whatever the worker holds there, so the same call would copy and translate differently by where it runs — on 13.4, a copy approved from live was taken back. The global user itself stays the request's: its session is where an installation's hooks put their messages, and the acting user, loaded for the run, has none. A process without a global user keeps its refusal.

They read the structure as that workspace shows it. The record, the source and target pages and an anchor are read with fetchRowAsTheUserSees(); lists by parent — siblings, subpages, the content on a page, a record's translations — with the predicate of ADR-263, each row under its live uid (ReadsTheRunsWorkspaceTrait::rowsShownIn(), ReadsVersionsInTheWorkspaceTrait::translationsInWorkspace(), which also matches a translation pointing at the parent's version). A page or element moved or new in the workspace is where the workspace puts it; another workspace's draft is not there.

The read-back reads the workspace: the move version's page and column, the copy and its translations new in the workspace and hidden, the translation new there, and for a delete the record and its translations gone from the workspace's view. A delete in a draft workspace does not ask for the subpages and the content of a page, which core deletes live only when the workspace is published.

The card and the answer name the workspace. Move, copy and translation cards carry the line of ADR-260; a delete card replaces "Recoverable: yes, from the recycler" with "Workspace: … — marked for deletion there; live stays unchanged until the workspace is published … Until then the mark can be discarded in the Workspaces module". The answer to the model ends with the draft note of ADR-262; a delete says "Marked … for deletion in the workspace …".

Refused before the card (RefusesBeforeApprovalInterface), in a draft workspace, whatever the approver decides:

  • a run that did not record its workspace;

  • a table the call writes that core keeps no draft of (refuseUnversionedTableInDraftWorkspace(), ADR-262); for a page copy, every table that holds records on the page — core copies them along, and a record of a table without versioning, such as sys_template, would be written live where the workspace allows live editing, and refused after the approval otherwise;

  • delete_record of a record new in the workspace, or moved there, and of a record whose translation is new or moved there — core's command would discard rather than delete (it deletes each translation the same way, deleteL10nOverlayRecords()); and of a page whose live branch holds a record — a subpage, a content element — the workspace moved away: publishing the mark runs core's live delete on the live branch, which deletes that record from where it still is and discards its move;

  • create_translation_draft with overwrite where a translation exists: refused on both cores, so the tool behaves the same on both and never leaves a marked translation without a new one;

  • on TYPO3 13.4, create_translation_draft where the translation in that language is marked for deletion in the workspace: 13.4 still counts it and refuses the localization (measured on 13.4.37); 14.3 makes the new one.

The refusal is asked after the plan's own checks, so a uid the user may not touch keeps the neutral refusal, and PlansOneEditorialWriteTrait::refusalInItsWorkspace() answers it for all five. execute() and the card ask it first, through the one copy of that opening in AsksItsRefusalFirstTrait.

A call the card showed refused stays refused. Beside a call that needs approval, the turn waits as one, and an earlier call of it can remove the reason before this one runs — it marks the translation an overwrite would replace, or moves a record back into a branch to be deleted. Unlike ADR-262's first refusals, which rest on configuration, these rest on rows. So ToolLoopService::resume() asks every call's refusal once, after the cards are compared and before any call runs, and answers those calls with the refusal the approver read instead of running them.

Another workspace's drafts still stop a delete. In a draft workspace the delete is only a mark; publishing it runs core's live delete, which discards the drafts of every other workspace on what it deletes. The guard of ADR-198 keeps counting those, a translation this workspace already marked included; the run's own workspace's drafts — the record's own change there — are its own work and are not counted.

Provenance. Only create_translation_draft reports a CREATED record; MarkAiWrittenRecord marks the translation new in the workspace (ADR-260). Move, copy and delete report TEXT_UNCHANGED or DELETED and are not marked, in live as before.

Consequences

✓ An editor in a draft workspace can let the chat move, copy, delete and translate pages and content; each change waits in the Workspaces module, live stays as it was.

✓ A delete or an overwrite core would turn into something else in a workspace is refused before the card, with the reason and what to do instead.

✗ A record new in the workspace, or moved there, or one whose translation is, cannot be deleted through the chat in that workspace; it is discarded in the Workspaces module.

✗ A page that holds a TypoScript template, or any record of a table without versioning, cannot be copied through the chat in a draft workspace.

✗ The drafts of other workspaces are counted when the delete is marked. A draft another workspace makes afterwards is not: publishing the mark discards it, as publishing the same mark made by hand in the page tree would.

✗ Inline children are not asked about: a table core does not version that an extension hangs below a content element as inline children would be copied or localized live in a workspace that allows live editing. TYPO3's own tt_content has none; its file references are versioned.

✗ overwrite of a translation is not available in a draft workspace, although TYPO3 14.3 could do it.

✗ The limit ADR-260 names for the card's read gate reaches further here: the gate runs the plan in the viewer's own workspace. A member of the run's workspace who views the card from another workspace gets it withheld where the plan depends on the run's drafts — a record, page or anchor new or moved there, and a delete whose record has a draft there (counted as another workspace's from where the viewer sits). The gate fails closed; the run's own user always sees the card.

✗ The approval card of a page delete counts the subpages and records the workspace shows; publishing deletes the live branch, which a draft of the same workspace may have changed.

✗ The first measurements of this decision ran with the aspect left at live, as a worker has it, and recorded that a page new in the workspace belongs to no site — so a copy left its elements' translations behind and a record on such a page could not be translated. That described the process, not core; with the aspect pinned both work as in live in either process. The field writers of ADR-260 and ADR-262 and the creating writers start their DataHandler with the aspect left ambient; whether that changes what they write is not measured here.

✗ The global backend user stays the request's, so core reads its user TSconfig and its page TSconfig conditions on the user inside the DataHandler — the approving editor's in a backend request, not the run's user's. This holds for every writer and predates this decision.

✗ Core caches a page's TSconfig for the rest of the process by page uid alone (BackendUtility::getPagesTSconfig(), and the rootline under it), whichever user asked. Where the page's TSconfig was read earlier in the same process — by the approving request, or by another run in a worker — the DataHandler gets that answer, not the run's user's; and the run's user's answer is what later readers in the process get. The tools do not flush that cache. It matters only where user TSconfig, a workspace version of page TSconfig, or a page TSconfig condition on the backend user or the workspace differs between the users, for the same page in the same process.

← All ADRs