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/.
AfterAiRecordWrittenEvent
Netresearch
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.
-
- type
-
string
The agent run's uuid, which is the correlation id of everything that run did. It is what
tx_holds, so a listener that needs the run — its actor, its configuration, its trace — resolves it by this value. Never empty.nrllm_ agentrun. uuid
-
- type
-
NetresearchNr Llm Domain Value Object Record Reference
The row the write produced or changed: a table name and a uid, and
__renders them asto String () pages:.42
-
- type
-
NetresearchNr Llm Domain Enum Write Kind
Writewhen the record did not exist before the call, andKind:: CREATED Writewhen it did. There is no deletion case, because no builtin tool deletes.Kind:: UPDATED
A listener
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,
);
}
}
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 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).
No actor. Which backend user the run acted for is persisted on the run; resolve it by correlationId 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 and record. 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.