Template catalogue
A host that writes its own action strings will eventually write two spellings of the same thing. The audit templates exist to prevent that: each builder owns one canonical action string, one target resource type, and a set of evidence defaults. This page is the mapping, produced from a fixture that calls every builder in the published package.
Templates are pure builders
Section titled “Templates are pure builders”A template takes host inputs and returns an AuditEventInput. It does not hash, sequence, store, or redact. Those happen later, when the recorder turns the input into a record.
host inputs ↓ auditTemplates.<set>.<builder>AuditEventInput { actor, action, target, purpose, lawfulBasis, retention, metadata } ↓ createAuditEvent (id, occurredAt, scope validation, redaction) ↓ AuditStore.append (sequence, previousHash, hash)recordBecause templates are pure, the catalogue below can be generated without a store, a clock, or a tenant. That is also why a template cannot fail closed on a missing tenant: scope validation belongs to createAuditEvent, not to the builder.
Every field a template sets is a default, not a lock — but the two halves of the event resolve in opposite directions, and mixing them up is the most common source of surprise.
evidence fields metadatapurpose, lawfulBasis, caller keysdataCategories, retention ↓ merged first ↑ template-reserved keyscaller value wins ↓ merged last ↓ activityEpisodeId, riskSignalstemplate default fills the gap ↓ stamped last of allbuildTemplate resolves purpose, lawfulBasis, dataCategories, and retention as caller value first, template value second: you can always override an evidence classification. Metadata goes the other way. Template-reserved keys such as sessionId, proposalId, and subjectId are merged after caller metadata, and activityEpisodeId plus normalized riskSignals are stamped after that. A caller cannot shadow the grouping keys read models join on. That asymmetry is intentional: classification is a host policy decision, identity and grouping are not.
Only fields the caller actually supplies appear on the event. A template never emits a present-but-null field, so absent optional metadata leaves no key behind — which matters because the canonical bytes, and therefore the record hash, differ between an omitted key and a key set to null.
The catalogue
Section titled “The catalogue”Twenty-six builders ship in @veritio/core@0.4.7. Twenty-five are reachable through the auditTemplates registry and advertised by auditTemplateSets. One is not; it is listed last and explained below.
| Builder | Action | Target type | Purpose | Lawful basis | Retention | Required inputs |
|---|---|---|---|---|---|---|
auditTemplates.auth.userCreated |
auth.user.created |
user |
access_management |
contract |
security_1y |
userId |
auditTemplates.auth.signedIn |
auth.session.created |
session |
access_management |
contract |
security_1y |
userId, sessionId |
auditTemplates.auth.signedOut |
auth.session.revoked |
session |
access_management |
contract |
security_1y |
userId, sessionId |
auditTemplates.auth.passwordResetRequested |
auth.password.reset.requested |
password_reset_request |
access_management |
contract |
security_1y |
userId, resetRequestId |
auditTemplates.organization.created |
org.created |
organization |
access_management |
contract |
security_1y |
organizationId, actor |
auditTemplates.organization.memberInvited |
org.member.invited |
organization_invitation |
access_management |
contract |
security_1y |
organizationId, invitationId, inviter |
auditTemplates.organization.memberJoined |
org.member.joined |
organization_member |
access_management |
contract |
security_1y |
organizationId, memberId, actor |
auditTemplates.organization.memberRemoved |
org.member.removed |
organization_member |
access_management |
contract |
security_1y |
organizationId, memberId, actor |
auditTemplates.organization.memberRoleChanged |
org.member.role.changed |
organization_member |
access_management |
contract |
security_1y |
organizationId, memberId, actor |
auditTemplates.data.consentGranted |
consent.granted |
consent |
consent_management |
consent |
consent_7y |
actor, consentId |
auditTemplates.data.consentRevoked |
consent.revoked |
consent |
consent_management |
consent |
consent_7y |
actor, consentId |
auditTemplates.data.subjectRequestCreated |
data.subject.request.created |
subject_request |
data_subject_workflow |
legal_obligation |
subject_request_3y |
actor, subjectRequestId, requestType |
auditTemplates.data.exportBundleCreated |
export.bundle.created |
export_bundle |
data_subject_workflow |
legal_obligation |
export_1y |
actor, exportBundleId |
auditTemplates.data.retentionPolicyApplied |
retention.policy.applied |
policy |
retention_management |
legal_obligation |
retention_audit_7y |
actor, policyId |
auditTemplates.agent.sessionStarted |
agent.session.started |
agent_session |
change_provenance |
— | security_1y |
sessionId, agentActor |
auditTemplates.agent.promptRecorded |
agent.prompt.recorded |
agent_session |
change_provenance |
— | security_1y |
sessionId, promptHash, agentActor |
auditTemplates.agent.toolCalled |
agent.tool.called |
tool_call |
change_provenance |
— | security_1y |
sessionId, toolCallId, tool, status, agentActor |
auditTemplates.code.changeProposalCreated |
change.proposal.created |
change_proposal |
change_provenance |
— | security_1y |
proposalId, actor |
auditTemplates.code.filesChanged |
change.files.changed |
source_tree |
change_provenance |
— | security_1y |
sourceTreeId, actor |
auditTemplates.code.reviewApprovalRecorded |
review.approval.recorded |
pull_request |
change_provenance |
— | security_1y |
pullRequestId, reviewer |
auditTemplates.code.reviewFindingCreated |
review.finding.created |
pull_request |
change_provenance |
— | security_1y |
pullRequestId, reviewer |
auditTemplates.code.reviewWaiverRecorded |
review.waiver.recorded |
pull_request |
change_provenance |
— | security_1y |
pullRequestId, reviewer |
auditTemplates.code.ciJobCompleted |
ci.job.completed |
ci_run |
change_provenance |
— | security_1y |
ciRunId, service, status |
auditTemplates.code.deploymentCreated |
deploy.deployed |
deployment |
change_provenance |
— | security_1y |
deploymentId, service |
auditTemplates.code.runtimeObserved |
audit.runtime.observed |
runtime_event |
runtime_observation |
— | security_1y |
runtimeEventId, actor |
episodeStartedTemplate (unregistered) |
activity.episode.started |
activity_episode |
change_provenance |
— | security_1y |
activityEpisodeId, actor |
A dash in the lawful-basis column means the template sets no lawfulBasis at all. The field is then absent from the built event, not present-and-empty. That is deliberate for provenance and agent events: the SDK will not guess a basis on the host’s behalf. Supplying lawfulBasis as a caller input adds it.
Builder names do not predict action strings
Section titled “Builder names do not predict action strings”Six builders in the table emit an action you would not derive from the method name. Treat the action column as authoritative and never reconstruct one from a camel-case identifier.
auditTemplates.code.deploymentCreated → deploy.deployedauditTemplates.code.runtimeObserved → audit.runtime.observedauditTemplates.auth.signedIn → auth.session.createdauditTemplates.auth.signedOut → auth.session.revokedauditTemplates.organization.created → org.created (not organization.created)auditTemplates.data.subjectRequestCreated → data.subject.request.createdThe organization set is the sharpest trap. Its registry key is organization, its input fields are organizationId and organizationDisplay, and its actions are all short org.* strings. A host filter written against organization.member.joined matches nothing.
One purpose default breaks the pattern
Section titled “One purpose default breaks the pattern”Seven of the eight code builders default purpose to change_provenance. runtimeObservedTemplate defaults to runtime_observation instead, because a production observation is not evidence about who changed the software. It is the only builder in any set that uses that purpose. If you group evidence by purpose to answer “what did this release change”, runtime_observation rows are outside that answer by design.
Verified catalogue output
Section titled “Verified catalogue output”The fixture below calls all twenty-five registered builders plus episodeStartedTemplate with pinned ids, a pinned timestamp, and a pinned scope, then flattens each result to the row shape used in the table. It runs in CI and its output is byte-compared.
{ "auditTemplateSets": { "auth": [ "auth.user.created", "auth.session.created", "auth.session.revoked", "auth.password.reset.requested" ], "organization": [ "org.created", "org.member.invited", "org.member.joined", "org.member.removed", "org.member.role.changed" ], "data": [ "consent.granted", "consent.revoked", "data.subject.request.created", "export.bundle.created", "retention.policy.applied" ], "agent": [ "agent.session.started", "agent.prompt.recorded", "agent.tool.called" ], "code": [ "change.proposal.created", "change.files.changed", "review.approval.recorded", "review.finding.created", "review.waiver.recorded", "ci.job.completed", "deploy.deployed", "audit.runtime.observed" ] }, "catalogue": [ { "set": "auth", "declaredActions": [ "auth.user.created", "auth.session.created", "auth.session.revoked", "auth.password.reset.requested" ], "builders": [ { "builder": "auditTemplates.auth.userCreated", "action": "auth.user.created", "actorType": "user", "targetType": "user", "purpose": "access_management", "lawfulBasis": "contract", "retention": "security_1y" }, { "builder": "auditTemplates.auth.signedIn", "action": "auth.session.created", "actorType": "user", "targetType": "session", "purpose": "access_management", "lawfulBasis": "contract", "retention": "security_1y" }, { "builder": "auditTemplates.auth.signedOut", "action": "auth.session.revoked", "actorType": "user", "targetType": "session", "purpose": "access_management", "lawfulBasis": "contract", "retention": "security_1y" }, { "builder": "auditTemplates.auth.passwordResetRequested", "action": "auth.password.reset.requested", "actorType": "user", "targetType": "password_reset_request", "purpose": "access_management", "lawfulBasis": "contract", "retention": "security_1y" } ], "coverage": { "declaredCount": 4, "builderCount": 4, "emittedMatchesDeclared": true, "duplicateActions": false } }, { "set": "organization", "declaredActions": [ "org.created", "org.member.invited", "org.member.joined", "org.member.removed", "org.member.role.changed" ], "builders": [ { "builder": "auditTemplates.organization.created", "action": "org.created", "actorType": "user", "targetType": "organization", "purpose": "access_management", "lawfulBasis": "contract", "retention": "security_1y" }, { "builder": "auditTemplates.organization.memberInvited", "action": "org.member.invited", "actorType": "user", "targetType": "organization_invitation", "purpose": "access_management", "lawfulBasis": "contract", "retention": "security_1y" }, { "builder": "auditTemplates.organization.memberJoined", "action": "org.member.joined", "actorType": "user", "targetType": "organization_member", "purpose": "access_management", "lawfulBasis": "contract", "retention": "security_1y" }, { "builder": "auditTemplates.organization.memberRemoved", "action": "org.member.removed", "actorType": "user", "targetType": "organization_member", "purpose": "access_management", "lawfulBasis": "contract", "retention": "security_1y" }, { "builder": "auditTemplates.organization.memberRoleChanged", "action": "org.member.role.changed", "actorType": "user", "targetType": "organization_member", "purpose": "access_management", "lawfulBasis": "contract", "retention": "security_1y" } ], "coverage": { "declaredCount": 5, "builderCount": 5, "emittedMatchesDeclared": true, "duplicateActions": false } }, { "set": "data", "declaredActions": [ "consent.granted", "consent.revoked", "data.subject.request.created", "export.bundle.created", "retention.policy.applied" ], "builders": [ { "builder": "auditTemplates.data.consentGranted", "action": "consent.granted", "actorType": "user", "targetType": "consent", "purpose": "consent_management", "lawfulBasis": "consent", "retention": "consent_7y" }, { "builder": "auditTemplates.data.consentRevoked", "action": "consent.revoked", "actorType": "user", "targetType": "consent", "purpose": "consent_management", "lawfulBasis": "consent", "retention": "consent_7y" }, { "builder": "auditTemplates.data.subjectRequestCreated", "action": "data.subject.request.created", "actorType": "user", "targetType": "subject_request", "purpose": "data_subject_workflow", "lawfulBasis": "legal_obligation", "retention": "subject_request_3y" }, { "builder": "auditTemplates.data.exportBundleCreated", "action": "export.bundle.created", "actorType": "user", "targetType": "export_bundle", "purpose": "data_subject_workflow", "lawfulBasis": "legal_obligation", "retention": "export_1y" }, { "builder": "auditTemplates.data.retentionPolicyApplied", "action": "retention.policy.applied", "actorType": "service", "targetType": "policy", "purpose": "retention_management", "lawfulBasis": "legal_obligation", "retention": "retention_audit_7y" } ], "coverage": { "declaredCount": 5, "builderCount": 5, "emittedMatchesDeclared": true, "duplicateActions": false } }, { "set": "agent", "declaredActions": [ "agent.session.started", "agent.prompt.recorded", "agent.tool.called" ], "builders": [ { "builder": "auditTemplates.agent.sessionStarted", "action": "agent.session.started", "actorType": "ai_agent", "targetType": "agent_session", "purpose": "change_provenance", "lawfulBasis": null, "retention": "security_1y" }, { "builder": "auditTemplates.agent.promptRecorded", "action": "agent.prompt.recorded", "actorType": "ai_agent", "targetType": "agent_session", "purpose": "change_provenance", "lawfulBasis": null, "retention": "security_1y" }, { "builder": "auditTemplates.agent.toolCalled", "action": "agent.tool.called", "actorType": "ai_agent", "targetType": "tool_call", "purpose": "change_provenance", "lawfulBasis": null, "retention": "security_1y" } ], "coverage": { "declaredCount": 3, "builderCount": 3, "emittedMatchesDeclared": true, "duplicateActions": false } }, { "set": "code", "declaredActions": [ "change.proposal.created", "change.files.changed", "review.approval.recorded", "review.finding.created", "review.waiver.recorded", "ci.job.completed", "deploy.deployed", "audit.runtime.observed" ], "builders": [ { "builder": "auditTemplates.code.changeProposalCreated", "action": "change.proposal.created", "actorType": "ai_agent", "targetType": "change_proposal", "purpose": "change_provenance", "lawfulBasis": null, "retention": "security_1y" }, { "builder": "auditTemplates.code.filesChanged", "action": "change.files.changed", "actorType": "ai_agent", "targetType": "source_tree", "purpose": "change_provenance", "lawfulBasis": null, "retention": "security_1y" }, { "builder": "auditTemplates.code.reviewApprovalRecorded", "action": "review.approval.recorded", "actorType": "user", "targetType": "pull_request", "purpose": "change_provenance", "lawfulBasis": null, "retention": "security_1y" }, { "builder": "auditTemplates.code.reviewFindingCreated", "action": "review.finding.created", "actorType": "user", "targetType": "pull_request", "purpose": "change_provenance", "lawfulBasis": null, "retention": "security_1y" }, { "builder": "auditTemplates.code.reviewWaiverRecorded", "action": "review.waiver.recorded", "actorType": "user", "targetType": "pull_request", "purpose": "change_provenance", "lawfulBasis": null, "retention": "security_1y" }, { "builder": "auditTemplates.code.ciJobCompleted", "action": "ci.job.completed", "actorType": "service", "targetType": "ci_run", "purpose": "change_provenance", "lawfulBasis": null, "retention": "security_1y" }, { "builder": "auditTemplates.code.deploymentCreated", "action": "deploy.deployed", "actorType": "service", "targetType": "deployment", "purpose": "change_provenance", "lawfulBasis": null, "retention": "security_1y" }, { "builder": "auditTemplates.code.runtimeObserved", "action": "audit.runtime.observed", "actorType": "service", "targetType": "runtime_event", "purpose": "runtime_observation", "lawfulBasis": null, "retention": "security_1y" } ], "coverage": { "declaredCount": 8, "builderCount": 8, "emittedMatchesDeclared": true, "duplicateActions": false } } ], "unregisteredBuilders": [ { "builder": "episodeStartedTemplate", "action": "activity.episode.started", "actorType": "user", "targetType": "activity_episode", "purpose": "change_provenance", "lawfulBasis": null, "retention": "security_1y" } ], "totals": { "sets": 5, "declaredActions": 25, "registeredBuilders": 25, "unregisteredBuilders": 1, "everySetFullyCovered": true }}Read the output in three parts.
auditTemplateSets is the vocabulary the package advertises. It is exported data, not derived from the builders, which is exactly why it can drift from them.
catalogue[].coverage is the invariant check. declaredCount and builderCount must agree, emittedMatchesDeclared compares the ordered list of actions the builders actually returned against the ordered list the set declares, and duplicateActions catches two builders claiming the same action. All five sets report emittedMatchesDeclared: true and duplicateActions: false. totals.everySetFullyCovered: true is the roll-up. That is the useful result: in this version, no advertised action is unimplemented and no builder emits an action outside its set.
unregisteredBuilders is the gap. It holds exactly one row.
The builder that no set advertises
Section titled “The builder that no set advertises”totals reports 25 declared actions, 25 registered builders, and 1 unregistered builder. The unregistered one is activity.episode.started.
It is a first-class, documented, exported builder in all three SDKs. It is absent from every auditTemplateSets list in all three SDKs. So a host that validates its outbound actions against auditTemplateSets — the obvious thing to do with an exported vocabulary — will reject an action its own SDK emits.
The three SDKs also do not agree on what the builder is called, or on whether it is reachable from a grouped registry.
| SDK | Builder name | In the grouped registry? | In the advertised set? |
|---|---|---|---|
| TypeScript | episodeStartedTemplate |
No — top-level export only | No |
| Python | activity_episode_started_template |
Yes — audit_templates["agent"]["episode_started"] |
No |
| Go | EpisodeStartedTemplate |
Go ships no grouped registry | No |
Python’s agent registry group therefore holds four builders while audit_template_sets["agent"] declares three actions. TypeScript’s agent group holds three and matches. Go has AuditTemplateSets but no AuditTemplates equivalent, so Go callers must reference the exported functions directly and get no set-to-builder mapping at all.
The practical consequence: auditTemplateSets is a floor, not a closed vocabulary. Validate against it plus activity.episode.started, or validate against the schema’s action pattern instead.
When a template is the wrong tool
Section titled “When a template is the wrong tool”Templates cover lifecycle events with a stable, single target: an account, a session, a consent, a deployment. Three situations fall outside that shape.
A governed create/update/delete on host domain data needs changed-path derivation, a tenant-scoped idempotency hash, and revision evidence. createGovernedActionDraft produces those; no template does. Reaching for auditTemplates.code.changeProposalCreated to record a row edit produces an event that looks right and carries none of the diff evidence.
An event with no canonical action here should use a host-defined action through createAuditEvent directly. Do not repurpose the closest template — an org.member.role.changed event that actually records a billing plan change is worse than an honest custom action, because it silently poisons every filter and export built on the canonical vocabulary.
High-volume request logging is out of scope for all of them. These builders are for events a reviewer would want to see individually, and each one costs a sequence number in the tenant chain.
Failure modes
Section titled “Failure modes”Raw content in agent and code metadata. Every agent.* and code.* builder runs its caller metadata through a raw-content guard before building. Key names ending in prompt, diff, patch, path, stdout, stderr, output, args, log, token, secret, password, or apikey throw. So do string values containing a diff --git line, a @@ -1,2 +1,2 @@ hunk header, or a Bearer token shape. Keys ending in hash, hashes, id, ids, count, or status are explicitly allowed — that is the intended shape of the metadata these templates want.
This is a throw, not a redaction. The event is never built.
The guard is not applied consistently to the episode builder. Go’s EpisodeStartedTemplate passes blockRawContent: true. TypeScript’s episodeStartedTemplate and Python’s activity_episode_started_template do not. Metadata that throws in Go is accepted in TypeScript and Python, where it then falls back to key-name redaction in createAuditEvent. Do not rely on the template guard as your only minimization step.
Scope. organizationCreatedTemplate and the three member builders default scope to { tenantId: organizationId } when the caller omits it. No other builder defaults scope. Omit scope elsewhere and the missing tenant surfaces later, at event creation.
Role normalization. Passing role as an array deduplicates and sorts it before it reaches metadata, so ['admin','viewer'] and ['viewer','admin','viewer'] produce identical bytes and identical hashes. Passing a whitespace-only string drops the key entirely.
What this does not prove
Section titled “What this does not prove”The verified output covers the TypeScript package only. There is no fixture under spec/conformance pinning template output across languages — the conformance suite covers canonical JSON, hashing, redaction, risk scoring, governed-action drafts, and export bundles, but not templates. Python and Go template output is not byte-compared against TypeScript by any check in the repository. The action strings and defaults in the table match across the three files as read at the pinned revision; nothing enforces that they stay matched.
everySetFullyCovered: true proves the registry is internally consistent. It does not prove the vocabulary is complete, correct for your domain, or stable across versions.
The action vocabulary is not enforced by the protocol. spec/event.schema.json accepts any dotted action string, so a typo’d custom action is a valid event. Templates are a convention that keeps hosts on one spelling; they are not a validator.
Finally, retention: 'consent_7y' is a named policy label carried on the event. It records which retention class the host assigned. It does not delete anything, and it does not establish that the assignment is the right one — enforcement lives in your storage and retention jobs. These builders produce compliance evidence; they are not legal advice and do not establish that any regulatory obligation has been met.
Continue with Audit templates for how to call these builders in a host boundary, Event schema for the fields a template fills in, and Retention for what the retention labels above are supposed to drive.