ADR-265: Background work writes into the workspace it was started in 

Status

Accepted

Date

2026-11-05

Amends

ADR-237 (live only: a save in a draft workspace queued nothing), ADR-236 (a batch read its scope in live only)

Authors

Netresearch DTT GmbH

Context 

ADR-260 records the workspace a run was started in on its actor, and the resolver sets the acting user up there. The work that runs in the background did not use that, measured on 2026-11-05 against TYPO3 14.3:

  • The field fill (ADR-237) queued nothing for a save in a draft workspace, and its writer refused a user in a draft. Its write already went through the DataHandler (ToolDataHandler), so nothing wrote raw.
  • The editor-action batches (ADR-236, the module and nrllm:editor-action:batch) recorded the editor's workspace on each run (the module's actor is frozen from the request's user), but read their scope live: the pages of a tree, which records still had an empty field, and the page text a run on a page writes from. A page new in the draft was not in the batch, a text the draft had written or emptied was not seen.
  • The scheduled batch set its user up in the workspace be_users.workspace_id named when the command ran, and recorded that workspace. A schedule would therefore write into whichever draft its user last worked in, once the writers write into a workspace.

Core hands the DataHandler hooks the uid of the version when a save in a workspace changes a live record (DataHandler::process_datamap(), the $id set from autoVersionIdMap or getWorkspaceVersionOfRecord() before processDatamap_afterDatabaseOperations, TYPO3 14.3.10 and 13.4.37), and the uid of the row itself for a record new in the workspace.

Decision 

The field fill runs in the workspace of the save. The planner reads the record as the saving user's workspace shows it, under its live uid — the uid the hook names may be the version's — and queues the run for the live uid. Its actor records the workspace. The page text the model reads is the workspace's: each content element overlaid with its version there, elements new or moved there included, those deleted there left out, never another workspace's draft (the row predicate of ADR-263). A field the workspace already holds a value in is not queued.

The fill writes into that workspace, or nowhere. The writer resolves the user in the workspace the actor recorded (ADR-260) and writes through the DataHandler as that user: core stores a version, or changes a record new in the workspace in place. It reads the field as the workspace shows it before the write and after it. The provenance mark goes on the version (ADR-239). The fill joins the draft the save made, or writes nothing: a draft discarded or published while the run waited is not made again for the fill alone, and a draft sent on to review or publication is not written either — core resets the stage to editing with any write (t3ver_stage 0), and an unattended fill must not pull it back. Live is not touched, wherever the editor works when the worker runs. A run that recorded no workspace writes in live only, as before. A table that keeps no versions (ctrl.versioningWS), or one core edits live from any workspace (versioningWS_alwaysAllowLiveEdit, which TYPO3 13.4 honours even for a versioned table), is neither planned nor written in a draft workspace: core would write it live.

A batch reads its scope in its workspace. The module's batch takes the workspace its actor recorded, where that is the user's (as the read tools of ADR-263 decide it). Its tree is the tree that workspace shows — pages new or moved there included, pages deleted there left out — with page access asked of the live page, as core's page tree asks it. A record counts as empty when its version there is empty, and the page text a run on a page writes from is the workspace's. Each run's actor records the workspace, so the write at approval lands there, whoever approves and wherever the owner works by then; whether a writer writes into a workspace at all is the writer's own decision (ADR-260). The runs of a draft batch are kept per workspace (origin batch@<workspace>:<tool>:<table>:<uid>; a live batch keeps batch:<tool>:<table>:<uid> of ADR-236), so a card decided in a draft does not keep its record out of a live schedule, nor the reverse.

A schedule runs in live. nrllm:editor-action:batch sets its user up in live and records live, whatever be_users.workspace_id names. A user who may not work in live cannot be named.

Consequences 

✓ An editor in a draft workspace gets the empty fields of a save filled there; the fill waits in the workspace module, live stays as it was.

✓ A batch started in a draft proposes for what the draft shows, and its approved writes stay in the draft — for the writers that write into a workspace (ADR-260, ADR-262, among them set_file_alternative_text and update_page_metadata); one that does not refuses at approval, after the model was asked.

✗ A fill is kept by the live uid of its record (fill: origin, which the queue parses). A fill open in live holds the same field in every workspace until the worker has run it, and the reverse.

✗ The fill reads its text when it is queued, as before; a draft rewritten while the run waits is not read.

✗ A field whose draft was sent to review while the run waited stays empty; the next save in the draft queues it again. A field whose draft was discarded stays empty. The stage and the draft are read right before the write; a review sent in the milliseconds between that read and the DataHandler's write is reset to editing, the same window ADR-237 accepts for a text saved meanwhile (the DataHandler has no compare-and-set).

✗ A batch run queued in a draft before this change kept the live form batch:<tool>:<table>:<uid> (the run table stores no workspace of its own), and counts as a live run until it is purged.

✗ A fill of a file's metadata is no case: sys_ tables are not opted in (ADR-237).