What Article 12 requires, and what it does not
Article 12(1) of the AI Act: high-risk AI systems “shall technically allow for the automatic recording of events (logs) over the lifetime of the system”. The load-bearing word is automatic. A process where an engineer can export a transcript on request is not a logging capability; it is a person. A record that exists only because someone chose to produce it does not satisfy 12(1).
Article 12(2) then gives you a purpose test rather than a field list. Logging must ensure “a level of traceability of the functioning of a high-risk AI system that is appropriate to the intended purpose”, and must enable recording of events relevant for: “identifying situations that may result in the high-risk AI system presenting a risk within the meaning of Article 79(1) or in a substantial modification”; facilitating post-market monitoring under Article 72; and monitoring the operation of the system by the deployer under Article 26(5). Note the reach of that first limb — situations that may result in a risk, not only situations that are one.
That is the whole instruction. There is one concrete field list in Article 12 — paragraph 3: the period of each use, the reference database checked against, the input data that produced a match, and the natural persons involved in verification. It applies only to point 1(a) of Annex III, remote biometric identification. If you run a claims-triage assistant or a document-review RAG system, none of it binds you, and copying it anyway produces a schema that fits a face-recognition system and not yours.
Retention has actual numbers. Providers keep the Article 12(1) logs under Article 19, deployers under Article 26(6), and both say the same: a period appropriate to the intended purpose, at least six months, unless other Union or national law says otherwise — with data protection law named explicitly as the thing that might. Both also qualify the duty “to the extent such logs are under their control”, which is the statutory hook under the ownership question I come back to in “Where you write it”: control, not proximity, is what the Act attaches to. The duties run in parallel, so build a system and also run it and you wear both hats. Whether your system is high-risk at all is a prior question, covered in the plain-terms overview.
Standards are being drafted for exactly this, and none is finished. The CEN-CENELEC work supporting Article 12 — the track that would eventually give you a presumption of conformity — is still in development and unpublished. I am deliberately not citing a work-item number for it: the scope and the numbering have both moved more than once, and a stale identifier is worse than none. ISO/IEC 24970 (“AI system logging”) is stable enough to name: it reached FDIS and was registered for formal approval in May 2026; also unpublished. Until a harmonised standard is cited in the Official Journal there is nothing to lean on: you define the schema yourself and write down why it satisfies 12(2). Between now and December 2027, that is the job.
Why prompt plus response explains nothing
Here is the part the field lists get wrong. For a plain deterministic function, input and output are enough to check the work. An LLM in a RAG system is not that function: its answer is a function of the corpus at that instant, and the corpus is not in your prompt.
Re-index on Tuesday night with the same code, the same model and the same seed. On Wednesday the identical prompt retrieves different chunks and produces a different answer, and nothing in a prompt-and-response log records that anything changed. When a regulator, a customer or your own incident review asks why the system said what it said in March, you hand over a prompt, a response, and no way to demonstrate the link between them.
index_snapshot_id. Everything else on the usual list — prompt, response, timestamp, user, model name — can be identical across two runs that produced contradictory answers. Without it you cannot establish which corpus answered — and that, not any question of re-running the call, is where I read the 12(2) purpose test as bottoming out for a RAG system. That reading is mine: the Act sets a purpose test and names no field. Pin the snapshot, or accept that your record is a receipt rather than an explanation.The same logic promotes three more fields from nice-to-have to load-bearing. embedder_version, because re-embedding the same text with a new model reorders every neighbour. system_prompt_version, because the system prompt is code that ships without a deploy and changes behaviour silently. And the decoding parameters including the seed, because temperature above zero makes the output a sample rather than a value. Corpus drift is the failure mode underneath all of this, and I have written separately about how it shows up in production.
Verification is not replay
Two things people want from an audit record are quietly incompatible: a record that keeps no cleartext, and a record that can re-derive the output. You cannot have both, so here is the split.
Verification and traceability is what a hashed record genuinely delivers. With the fields below you can establish which corpus, which model weights, which prompt version and which chunks produced a given answer; prove that a prompt someone hands you later is the one that actually ran, by recomputing its HMAC and comparing; and show the decision path, including whether a human accepted the output or overrode it. That is what the purpose test in 12(2) asks for, and a record of this shape answers it.
Replay — actually re-deriving the output — is a much stronger claim, and this schema does not deliver it. It cannot, by construction. A prompt_hmac whose cleartext sits in a short-TTL vault lets you test a candidate prompt that someone supplies; once the vault expires, it cannot give you the prompt back. A hash is one-way, and that is the entire reason to use one. Same for the corpus: chunk_id plus index_snapshot_id tells you which chunks were retrieved, but showing what the model actually read requires that the snapshot, the source documents, the preprocessing and the chunk content all still exist.
So choose deliberately, and write the choice down. Replay means retaining the cleartext inputs and the corpus behind them, and holding a body of prompts for the whole retention period — which runs directly into the minimisation duty in “The conflict with GDPR”. That is defensible for some systems and a bad trade for most. This design buys verification and pays for it by not keeping the content.
The seed is evidence, not a guarantee
And even if you retain every input, replay is not a solved problem. PyTorch's documentation is blunt: “Completely reproducible results are not guaranteed across PyTorch releases, individual commits, or different platforms”, and results “may not be reproducible between CPU and GPU executions, even when using identical seeds”. vLLM states that it “does not guarantee the reproducibility of the results by default, for the sake of performance”, and its reproducibility notes spell out the conditions: scheduling under multiprocessing and batching both perturb results, so offline runs need deterministic scheduling or batch invariance, and online serving needs batch invariance before a seed buys you much at all. Layer on hardware, drivers, CUDA versions, the tokenizer and quantisation, and token-for-token regeneration of an answer from last March sits between very expensive and fictional.
This costs you less than it sounds, because Article 12(2) never asked for it: it asks for traceability appropriate to the intended purpose, not deterministic regeneration. Chasing bit-exact replay is engineering vanity — a genuinely hard problem no regulator set you, solved at the expense of the one they did. The seed still belongs in the record, but as evidence of intent and configuration: it documents what the system was set up to do when it ran, not a promise that you can make it happen again.
The record schema, field by field
This is one record for one call. Everything in it is either derivable at request time or already sitting in your deployment metadata. The markers are honest about their own authority: required means I consider the field necessary to meet the Article 12(2) purpose test for a RAG system, not that the Act names it — the Act names fields only in 12(3), and only for biometrics. prudent means it costs a few bytes and saves you a week during an incident. It closes with two signatures rather than one, for reasons I come to in “Where you write it”.
{
"record_id": "01J8FQ7M2K9XW3PBN4TRVC6H8D", // required · ULID, write-once
"ts_start": "2026-07-15T09:41:22.118Z", // required · period of use
"ts_end": "2026-07-15T09:41:26.902Z", // required
"system": {
"ai_system_id": "claims-triage", // required
"risk_class": "high_risk", // required · legal & compliance own this call
"role": "deployer", // required · Art. 19 vs Art. 26(6)
"instructions_version": "2.4.0" // required · Art. 26(1) conformance
},
"actor": {
"subject_pseudonym": "hmac:4f9c1a...e7", // required · keyed, never the name
"operator_id": "u-4471", // required · who ran it
"session_id": "s-9d20d1" // prudent · groups a conversation
},
"input": {
"prompt_hmac": "hmac:9f2c8b...31", // required · tests a supplied prompt, cannot recover one
"prompt_ref": "vault://p/2026-07-15/01J8FQ7M", // prudent · short-TTL cleartext
"prompt_chars": 812, // prudent
"pii_classes": ["name", "policy_number"] // prudent · drives your Art. 30 entry
},
"provenance": { // app-attested · what produced the answer — not a replay bundle
"model_id": "llama-3.3-70b-instruct", // required
"weights_sha256": "b71e0c...aa", // required · the name is not the weights
"serving_stack": "vllm-0.8.2", // prudent · kernels change numerics
"decoding": {
"temperature": 0.2, // required
"top_p": 0.9, // required
"seed": 42, // required · config evidence, not a replay guarantee
"max_tokens": 800 // prudent
},
"system_prompt_version": "sp-2026-06-30-r3", // required · code that ships silently
"system_prompt_sha256": "3ac910...5f", // required · version strings lie
"index_snapshot_id": "idx-2026-07-14T02:00Z-a91f", // required · THE omitted field
"embedder_version": "bge-m3@2024-06", // required
"retrieval": {
"top_k": 8, // required
"reranker": "bge-reranker-v2@2025-01", // required if present
"filter": "tenant=ACME AND lang=de" // required · scope changes results
},
"retrieved": [ // required · the real context window
{"chunk_id": "doc-8842#c17", "score": 0.8123, "rank": 1},
{"chunk_id": "doc-1190#c03", "score": 0.7440, "rank": 2}
]
},
"output": {
"response_hmac": "hmac:7d10ff...92", // required
"response_ref": "vault://r/2026-07-15/01J8FQ7M", // prudent
"finish_reason": "stop", // prudent · truncation explains a lot
"refusal": false // prudent
},
"oversight": { // app-attested · a decision after ts_end lands as a linked follow-up record
"human_review": "mandatory", // required · Art. 14
"reviewer_id": "u-2210", // required when review happened
"decision": "overridden", // required · accepted | overridden | none
"override_reason_code": "OR-07" // prudent · codes beat free text
},
"attestations": [ // required · two signers, because no one party witnesses all of this
{
"signer": "gateway", // signs only what it terminates first-hand
"key_id": "gw-signing-2026-q3",
"covers": ["record_id", "ts_start", "ts_end", "system", "actor", "input", "output"],
"payload_sha256": "9c41d7...a8", // JCS digest over exactly the fields named in "covers"
"signature": "ed25519:9a01...bb"
},
{
"signer": "app:claims-triage", // the gateway cannot witness these — the application asserts them
"key_id": "app-triage-2026-q3", // its own key, its own trust domain
"covers": ["record_id", "provenance", "oversight"],
"payload_sha256": "3fb0ac...5d", // record_id is in both, so neither half can be spliced onto another call
"signature": "ed25519:4c88...02"
}
],
"integrity": { // the collector writes this; it signs neither claim
"canon": "JCS/RFC8785", // required · pins key order and float form, or no two parties agree
"seq": 88213, // required · collector-assigned; gaps are evidence
"prev_sha256": "5c2ab8...0d", // required · the previous entry_sha256 — the chain
"entry_sha256": "e1907a...c4" // required · JCS digest over every field above this block
}
}
What I would cut, and what I would not
If you are tempted to trim, cut prompt_chars, session_id and finish_reason — diagnostics, not evidence. Do not cut the scores from retrieved: the difference between “answered from a 0.81 match” and “answered from a 0.31 match” is the difference between a system working and a system guessing, which is exactly what Article 12(2)(a) — identifying situations that may present a risk — asks you to be able to see. And do not add a natural-language “explanation” generated by a second LLM call: it is evidence of nothing except that a second model produced plausible text, it cannot be validated, and in an audit record it invites a reader to treat it as a finding.
Where you write it
Two candidate placements, and neither is clean. The case for the gateway is independence: the application is the thing under audit, and if the code path that produces the decision also writes the record of it, one deploy changes both and the system is marking its own homework. A gateway gives you a separate trust domain — the resolved request, the response, its own signing key — and it is where you already terminate auth and enforce input controls, the same chokepoint I argue for when securing LLM applications.
But look at what a gateway cannot see, and notice that it is most of this article: which documents your application retrieved, which index snapshot was actually queried, how the prompt was assembled before it arrived, what a human reviewer decided ten minutes later. Those are the centerpiece fields — index_snapshot_id above all — and the application has to assert them. For precisely the fields that carry the argument, the gateway is not an independent witness. It is a courier.
So the realistic design is split, and that is the attestations array in the schema above rather than a single writer's signature: the gateway signs what it observes first-hand, the application signs its own attestation of the provenance and oversight fields with its own key, and both go to an append-only collector that signs neither and cannot rewrite either. Each covers list names exactly what that key vouches for; record_id sits in both, so neither half can be lifted onto a different call. You get attribution rather than independence — a tamper-evident record of who claimed what. “Always log at the gateway” is architecture opinion, mine included, not a requirement of the Act. Article 12 says nothing about where the writer sits.
seq plus prev_sha256 binds each record to the last, and WORM storage stops even the writing account deleting early. First make the digests reproducible at all: JSON keys have no inherent order and 0.8123 has more than one valid encoding, so unless every party serialises the same way — hence canon, and JCS/RFC 8785 is the boring choice — your auditor recomputes a different hash and learns nothing. But a chain proves nothing against an attacker who controls the writer: they recompute it from the tampered record forwards. It becomes evidence only when the head hash goes somewhere you cannot quietly rewrite — a daily digest countersigned by your auditor, or mailed to a second organisation. Publish the head or admit the chain is decoration.The conflict with GDPR, and how it resolves
Now the part that makes lawyers nervous, and rightly. Article 19 and Article 26(6) tell you to keep logs for at least six months, and keeping six months of prompts is itself a processing activity — prompts to a claims system are full of names, health details and grievances. Article 5(1)(c) GDPR requires data minimisation. Retaining cleartext prompts because “the AI Act said to log” is how you comply with one regulation by breaching another.
The resolution is in the schema above, and it has to be stated precisely or it is not a resolution at all: the audit record must let you verify the decision without retaining the personal data. Store identity, not content. The record holds prompt_hmac, not the prompt; subject_pseudonym, not the name; chunk_id, not the chunk text. That is exactly the trade-off from “Verification is not replay”, and this is the regulation that makes it the right one.
Use an HMAC with a secret key, not a bare SHA-256. A plain hash of a short prompt is brute-forceable: if you know the template, you can enumerate candidates and match the digest, and the hash is then the prompt in a costume. Neither form is anonymisation — keyed or not, these remain pseudonymised personal data under Article 4(5) GDPR. The reason to use a key is leverage: destroy it and the digests become far harder to tie back to an individual.
But do not overclaim for that. Destroying the key is not automatically erasure and not automatically anonymisation. The EDPB's Guidelines 01/2025 on Pseudonymisation are explicit that deleting the additional information does not by itself make pseudonymised data anonymous — that yields anonymity only if the overall anonymisation test is actually met — and your record still carries operator_id, session_id, timestamps, chunk references and a distinctive shape of interaction, any of which, alone or combined, may still permit singling someone out. Key destruction is a strong mitigation, not a trick that converts personal data into non-personal data, and whether it lands is a question for your DPIA.
The Article 30 record, and the erasure question
The log is a processing activity, so it has to be reflected in your record of processing activities under Article 30 GDPR — though not necessarily as its own line. GDPR does not require a separate entry per technical log stream, and many controllers document audit logging inside the processing activity it serves. What matters is that the substance is there: purpose — traceability of a high-risk AI system under Art. 19 / Art. 26(6) AI Act; categories — pseudonymised identifiers, technical metadata; retention — the defined period, with the cleartext vault on a shorter clock; recipients — your auditor and the market surveillance authority.
Legal basis is where the shorthand gets dangerous. Article 6(1)(c) — compliance with a legal obligation — is the natural fit once the AI Act duty applies to you, but it is not the whole analysis. A claims-triage assistant processes health data, and special-category data needs a condition under Article 9 in addition to the Article 6 basis. If nobody has identified which Article 9 condition you rely on, your legal basis analysis is unfinished, however tidy the Article 30 entry looks.
Then the question everyone asks last: what happens on an erasure request under Article 17? The cleartext vault is deleted; that part is easy. The audit record is harder. Article 17(3)(b) disapplies erasure only to the extent that processing is necessary for compliance with a legal obligation under Union law. It is not a blanket exemption for anything labelled “audit”, it does not license indefinite retention, and it does not automatically cover every field. The test is field by field and it bites: Art. 19 / Art. 26(6) plausibly carries the traceability fields for the retention period, and rather obviously does not carry prompt_chars or a session ID you kept because it was convenient. The two-store design makes that analysis winnable — it is not a reason to skip it.
Where Tippel fits
December 2027 reads like a long way off. It is one procurement cycle and one ingestion rebuild away, and that is the trap. Most teams meet this problem late: a system is live, someone asks for the audit trail, and the logs cannot explain anything. Retrofitting a snapshot ID into a running RAG pipeline means versioning an index that was never designed to be versioned — a rebuild of the ingestion path, not a logging ticket. Decided at build time, it costs a few bytes per record.
If you are designing a system that will fall under Article 12, the AI Readiness Check is where that gets settled: one use case, your real data, and a fixed window that ends with an architecture you can defend rather than a slide deck. If you would rather just talk it through, get in touch.