---
title: "ADR-236: A batch over a folder is a draft per record"
manual: "LLM Foundation"
version: "0.43"
permalink: "https://docs.typo3.org/permalink/netresearch/nr-llm:adr-236@0.43"
source: "Adr/Adr236ABatchOverAFolderIsADraftPerRecord.rst"
rendered: "2026-11-04T08:34:12+00:00"
---

# ADR-236: A batch over a folder is a draft per record {#adr-236}

-   *Status:* Accepted
-   *Date:* 2026-11-01
-   *Amends:* [ADR-162](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-162@0.43) (a second bound, for a batch that does not run in the request)
-   *Authors:* Netresearch DTT GmbH

## Context {#adr-236-context}

[ADR-162](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-162@0.43) runs a bulk editor action as N ordinary runs inside one
backend request and stops at 20 records, because a request can time out. It named
the way past the cap: move the loop onto the queue, a separate decision.
[ADR-234](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-234@0.43) made a queue that does not run in the caller, and
[ADR-235](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-235@0.43) made the alternative text come from the image. What is
left is the scope: a folder of two hundred images, a section of a site, and a
scheduler that goes through them without anybody opening a page.

The guided tours of this project hold to the rule that nothing is saved without
confirmation. A scheduled batch has nobody present, so it has to say where it
stands on that rule.

## Decision {#adr-236-decision}

**A batch is still N ordinary runs.** Everything ADR-162 derives from that holds:
N turns, N digests, N verdicts, one catalogue call per record. Only the loop
moves, from `run()` to `defer()`.

**It is a draft, per record.** A batch writes nothing. Each run ends at its tool
call, which suspends for approval ([ADR-134](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-134@0.43)), so the generated text
stands on an approval card and the field keeps its value until somebody approves
that card. That is the rule of the tours, kept: nothing is saved without
confirmation. There is no direct-write switch. A declared write is approved
whatever the configuration says ([ADR-202](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-202@0.43)), and a batch verdict is
what [ADR-133](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-133@0.43) refuses, so a switch would have to reverse both for
a surface that writes to many records at once.

**Whose rights.** A writing tool acts for a person and refuses a service account.
The page queues under the editor's own frozen actor, as every background run does
([ADR-234](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-234@0.43)). `nrllm:editor-action:batch` needs `--user`, an
active backend user whose rights the runs have and who finds the cards in their
inbox. It is never `_cli_`, whose admin flag says nothing about who asked. A
user disabled after the batch was queued yields no acting user, and the run fails.

The catalogue asks whether the actor may use the default configuration through
`ConfigurationResolver::actorMayUse()`, not through the ambient user: on the
command line that would be `_cli_`, and everyone would pass.

**The image is read in the worker.** A queued request names its file in
`imageFileUid`. Scaling two hundred images inside a request is the timeout this
record leaves behind, and two hundred data URIs in the run table are a queue full
of copies. The worker reads the image as the acting user, through the same gate
([ADR-235](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-235@0.43)), so a file that moved or a right that went away is
decided when the run starts. A run whose image cannot be sent fails, because no
model should describe an image it does not have.

**A repeated batch is harmless.** The origin of a run is
`batch:<tool>:<table>:<uid>`, per record. A record is skipped when it has a run
that is open or ended in a decision: approved, rejected, or declined by the model.
A person who said no must not get the same card on every schedule, and an image
approved as decorative holds an empty text on purpose. A run that failed, was
cancelled, ran out of budget or was cut short says nothing about the record, so
the record is tried again. A decision lasts as long as its run is kept.

A record is also skipped when it holds a value in any field a vision action fills:
the prompt asks for all of them, so a file with one written would get a proposal to
replace it. A file with a description and no title therefore stays as it is. A
field switched off is not counted. A daily schedule picks up what arrived since,
and a batch never overwrites a text. The `--only-empty` option adds a column to
that check. A decision covers the record for that action: a second batch of the
same action with another `--only-empty` column skips what the first one decided.

**A queued run on a page reads the page.** A run has only the writing tool it was
started for, so the prompt carries the text of the page and its content elements,
read when the batch is queued. A vision run has its image instead.

**A second bound.** `EditorActionBatchPlanner::MAX_BACKGROUND_RECORDS` is 100
open runs of one action. It is not about timeouts. It bounds the cards an action
hands an approver, the second constraint ADR-162 named, and it is the number of
cards the inbox shows, oldest first. Open runs count, so a daily schedule does not
stack cards faster than somebody decides them. What the scope holds beyond it is
reported and picked up by the next batch. A record the catalogue refuses takes no
place, or a folder that starts with documents would never reach its images.

## Consequences {#adr-236-consequences}

-   A folder of 100 images is 100 cards, each decided on its own. That is the price
    of ADR-133, and it is the reason the bound exists. With a full inbox a later
    card of another action stays out of view until older ones are decided. An
    installation that wants
    this to be less work needs a different approval unit, which is its own decision.
-   A scheduled batch needs a user who reads an inbox, and something that works the
    queue: `nrllm:agent:work` or a consumer.
-   A run in the queue carries a request, not an image. A run waiting for approval
    does carry it, in `suspended_state`, until it settles ([ADR-235](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-235@0.43)).
-   The cost estimate of the batch page does not count the image, as for the
    synchronous page.
-   The budget stops a batch per user in the sweep, and the runs stay queued
    ([ADR-234](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-234@0.43)).
-   A page tree is read with the acting user's page permissions. A folder is listed
    with the rights of the process and filtered per file as the acting user.

## Alternatives considered {#adr-236-alternatives}

**Direct write behind an explicit opt-in.** It would make a scheduled run finish a
folder unattended, and it is what a large archive would want. It needs a write
path outside the tool loop, since a tool write always suspends, and it would save
generated text under an editor's name without that editor seeing it. Left to a
decision of its own, with an approval unit to match.

**A proposal table holding the generated texts.** A list an editor works through
in one view is a better surface for 100 texts than 100 cards. It is a second place
that holds a pending write, with its own lifecycle, next to the suspended run that
already does.

## Revisit when {#adr-236-revisit}

-   Measured use shows approvers giving up on the cards.
-   [ADR-133](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-133@0.43) is reopened for a verdict over several runs.
