---
title: "Events"
manual: "TYPO3 LLM Extension"
version: "0.35"
permalink: "https://docs.typo3.org/permalink/netresearch/nr-llm:api-events@0.35"
source: "Api/Events.rst"
modified: "2026-09-16T22:09:16+00:00"
---

# Events

PSR-14 events this extension dispatches, and what you may rely on about them.
Each is `@api`: within a major version it is not removed, its properties do
not disappear and their types do not change. Listeners are registered the
ordinary TYPO3 way, in your own extension's `Configuration/Services.yaml`.

## AfterAiRecordWrittenEvent

`NetresearchNrLlmEventAfterAiRecordWrittenEvent`

Dispatched once per successful editorial write, after the write has landed and
been read back by the tool that made it. It answers three questions and no
fourth.

-   **correlationId**

    -   *type:* string

    The agent run's uuid, which is the correlation id of everything that run did.
    It is what `tx_nrllm_agentrun.uuid` holds, so a listener that needs the
    run — its actor, its configuration, its trace — resolves it by this value.
    Never empty.

-   **record**

    -   *type:* `NetresearchNrLlmDomainValueObjectRecordReference`

    The row the write produced or changed: a table name and a uid, and
    `__toString()` renders them as `pages:42`.

-   **kind**

    -   *type:* `NetresearchNrLlmDomainEnumWriteKind`

    `WriteKind::CREATED` when the record did not exist before the call, and
    `WriteKind::UPDATED` when it did. There is no deletion case, because no
    builtin tool deletes.

### A listener

```php
use Netresearch\NrLlm\Domain\Enum\WriteKind;
use Netresearch\NrLlm\Event\AfterAiRecordWrittenEvent;

final readonly class LabelAiWrittenPages
{
    public function __construct(
        // Your own service. Nothing of the sort ships in nr_llm — what a
        // site says about an AI-written record is the site's decision.
        private AiTransparencyLabelRepository $labels,
    ) {}

    public function __invoke(AfterAiRecordWrittenEvent $event): void
    {
        if ($event->record->table !== 'pages') {
            return;
        }

        $this->labels->record(
            $event->record->uid,
            $event->kind === WriteKind::CREATED ? 'ai-generated' : 'ai-assisted',
            $event->correlationId,
        );
    }
}
```

```yaml
services:
  Vendor\Extension\EventListener\LabelAiWrittenPages:
    tags:
      - name: event.listener
        identifier: 'vendor/label-ai-written-pages'
```

### What it deliberately does not carry

**No record payload.** Not the field values, not a before/after, not a rendered
excerpt, not the tool's name. A payload is a copy, and a copy of editorial
content is a second place for it to leak from and a second place for it to go
stale. [record](https://docs.typo3.org/permalink/netresearch/nr-llm:confval-record@0.35) names the row; read it yourself, under your own
permissions, at the moment you need it.

**No listener of ours.** Nothing in this extension listens to this event, and
nothing is planned to. An Article 50 transparency label, a badge in the page
module, a line in an existing editorial audit trail and a nightly compliance
report are four different artefacts with four different owners; which of them
your site wants is your decision ([ADR-187](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-187@0.35)).

**No actor.** Which backend user the run acted for is persisted on the run;
resolve it by [correlationId](https://docs.typo3.org/permalink/netresearch/nr-llm:confval-correlationid@0.35) rather than expecting a second copy of an
identity on every event.

### Two properties worth knowing before you write a listener

**It fires before the run trace persists its step for the same call.** Do not
try to join the run's trace for this write — it is not there yet. Everything the
event promises is on the event.

**A listener that throws is logged, not propagated.** The write has already
landed and a human has already approved it, so a broken listener does not fail
the run — it produces an `error`-level log entry naming the record. Do not
rely on an exception to signal anything back to the extension.

**It can fire twice for one record.** The agent queue is at-least-once: a run
whose lease is lost may be re-executed, and an idempotent write that runs twice
dispatches twice with the same [correlationId](https://docs.typo3.org/permalink/netresearch/nr-llm:confval-correlationid@0.35) and [record](https://docs.typo3.org/permalink/netresearch/nr-llm:confval-record@0.35).
Deduplicate on that pair if your listener must act once per record. A
non-idempotent write is never auto-retried, so it cannot double this way.
