AuditEvent Profile¶
| Canonical | ../StructureDefinition/nexus-emr-core-auditevent |
|---|---|
| Status | draft (experimental) · 1.26.0 |
| Base | AuditEvent (constraint) |
| Source | FSH · JSON |
Nexus EMR profile for AuditEvent -- the access and activity log: who acted, on which record, when, through what interaction, and with what outcome.
TypedFhir API / Pydantic schema
The typed accessor surface AuditEvent exposes for AuditEvent — generated from this IG's compiled profile; each typed accessor has a validating raw-FHIR twin (xRaw) where marked. The three ports are parity-enforced.
The AI-facing shape: from typed_fhir.facade.generated.schemas import AuditEventSchema — model_json_schema() is a structured-output / tool definition; instantiating validates an extraction. Generated source.
class AuditEventSchema(BaseModel):
"""AI-facing shape of the AuditEvent profile: the facade's typed accessor
surface (curated + salient fields) as a pydantic model. `model_json_schema()` is a
structured-output / tool schema; instantiating validates an extraction."""
model_config = ConfigDict(extra="forbid")
type: dict | None = Field(
None,
description=(
"What kind of event this is. Identifier for a family of the event. For example, a "
"menu item, program, rule, policy, function code, application name or URL. It "
"identifies the performed function. Note: Required by R4 (1..1) and NOT yet fixed by "
"this guide, though two values are recommended and both come from the standard "
"vocabulary rather than from a local one. "
"`http://terminology.hl7.org/CodeSystem/audit-event-type#rest` for an entry "
"describing a FHIR API interaction, which is what a projection of the platform's "
"access log writes for the great majority of entries. "
"`http://dicom.nema.org/resources/ontology/DCM#110106` (Export) for an entry "
"describing information LEAVING the estate -- a packet released, a report downloaded, "
"a referral transmitted -- because `action` cannot express that distinction and a "
"disclosure report has to be able to find those entries. Recommended rather than "
"required: nothing writes an AuditEvent through this API today, so fixing a value "
"would constrain a producer that does not exist. Do not invent a local vocabulary "
"here. AuditEvent.type — 1..1. codes (extensible): 110100 | 110101 | 110102 | 110103 "
"| 110104 | 110105 | 110106 | 110107 | …"
),
)
subtype: dict | None = Field(
None,
description=(
"The narrower activity, where the event is not simply a REST interaction. Identifier "
"for the category of event. Note: Where an entry describes a named platform activity "
"rather than an interaction with a chart record -- assembling a document packet, "
"launching a billing handoff, running an eligibility check, exchanging credentials, "
"transmitting a referral -- the activity belongs here rather than on `entity`. NO "
"VOCABULARY IS PUBLISHED FOR THAT SET. The platform's log carries its own activity "
"names, and this guide does not yet declare a code system for them: the majority of "
"the values name FHIR resource types, which already have a published code system, and "
"minting a second one that restates them would be a permanent commitment to a "
"duplicate. What would settle it is the residue -- the values that name a platform "
"activity rather than a resource type -- published as a code system in its own right. "
"Until then, read `subtype` liberally and do not assume a system URI. "
"AuditEvent.subtype — 0..*. codes (extensible): 110120 | 110121 | 110122 | 110123 | "
"110124 | 110125 | 110126 | 110127 | …"
),
)
action: Literal["C", "D", "E", "R", "U"] | None = Field(
None,
description=(
"create | read | update | delete | execute. Indicator for type of action performed "
"during the event that generated the audit. Note: R4 binds this to five codes (`C`, "
"`R`, `U`, `D`, `E`) and this guide adds none. TWO THINGS A READER MUST KNOW. First, "
"every interaction that is not a create, an update or a delete is recorded as a READ "
"-- searches, version reads and history reads included. `R` therefore means \"the "
"record was disclosed to someone\", which is what a privacy report wants, and does not "
"mean a single-resource GET. A search that returned twelve records yields TWELVE read "
"entries, one per record returned, rather than one entry for the search: the question "
"an access log has to answer is which records were disclosed, not how many queries "
"were run. Count records, not entries, and expect a busy chart view to produce many. "
"Second, the platform distinguishes a TRANSMISSION -- a referral sent to an external "
"party, a report exported or downloaded -- from an ordinary read, and R4's five codes "
"do not express that distinction. Until it is settled, a transmission projected here "
"appears as `R`, and the fact that it left the estate is carried on `type`/`subtype` "
"rather than on `action`. A consumer answering \"what was disclosed outside the "
"clinic\" must therefore not read `action` alone. Settling it means either assigning "
"the standard export event type on `type` as a required convention, or an extension; "
"this guide has done neither yet. AuditEvent.action — 0..1"
),
)
period: dict | None = Field(
None,
description=(
"When the activity itself occurred. The period during which the activity occurred. "
"Note: Preferred over `recorded` as THE moment where it is present, for the reason "
"above. The platform's access log does not populate it today, so it is optional and "
"its absence is expected rather than a defect. AuditEvent.period — 0..1"
),
)
recorded: FuzzyDate | None = Field(
None,
description=(
"When the entry was recorded. The time when the event was recorded. Note: R4 makes "
"this 1..1, and it is the one element every entry carries. It is the moment the entry "
"was PERSISTED, which is not exactly the moment of the interaction: entries reach the "
"log asynchronously and the stamp is applied on arrival. The difference is small and "
"it is not zero. Order interactions by it freely; reconcile it against an external "
"timestamp with care, and prefer `period` where an entry carries one. "
"AuditEvent.recorded — 1..1"
),
)
outcome: Literal["0", "12", "4", "8"] | None = Field(
None,
description=(
"0 | 4 | 8 | 12 -- whether the interaction succeeded. Indicates whether the event "
"succeeded or failed. Note: The platform records three outcomes -- succeeded, failed, "
"and could not be determined -- against R4's four severity-graded codes. The mapping "
"this guide states: a success is `0`; a failure is `4` (minor failure), because the "
"platform does not grade failures and claiming a serious or major failure would "
"assert something it did not observe; an undetermined outcome carries NO code. READ "
"AN ABSENT `outcome` AS UNKNOWN, NEVER AS SUCCESS. It is the single most "
"consequential misread available on this resource: an access whose outcome could not "
"be determined is exactly the access a privacy review wants surfaced, and defaulting "
"it to success buries it. ONE CLASS OF ENTRY CARRIES A SUCCESS OUTCOME THAT IS NOT A "
"CLAIM ABOUT THE RESULT. An entry recorded for an INVOKED OPERATION is written when "
"the invocation is accepted, before the operation has resolved, and records that the "
"attempt was made. Its outcome is not evidence that the operation succeeded. Entries "
"for record interactions are written on the response and their outcome does reflect "
"it. AuditEvent.outcome — 0..1"
),
)
outcome_desc: str | None = Field(
None,
description=(
"Free-text detail about the event. A free text description of the outcome of the "
"event. Note: The platform's log carries an optional free-text detail on any entry, "
"not only on a failure. R4 scopes this element to describing the outcome, which is "
"narrower than what is written; a consumer should treat the text as descriptive of "
"the event and should not assume it is present only on failures, nor that it is "
"machine-readable. AuditEvent.outcomeDesc — 0..1"
),
)
purpose_of_event: Concept | None = Field(
None,
description=(
"Why the interaction took place. The reason the interaction occurred, bound "
"extensibly by R4 to the standard purpose-of-use vocabulary (treatment, payment, "
"operations, emergency treatment, and the rest). **This guide does not yet constrain "
"it, and emergency access is the reason to say so explicitly.** Where a clinician "
"overrides a patient's restriction on their record to obtain information in an "
"emergency, this element -- together with `agent.purposeOfUse` and `agent.policy` -- "
"is where base R4 records that the access was an override and under what authority. "
"That is the shape a reader should expect if and when such a record appears. **No "
"such record is produced in this estate today.** Nothing implements an "
"emergency-access override, and this guide therefore constrains nothing about one: no "
"code is fixed, no element is made mandatory, and no invariant asserts that an "
"override carries a justification. Read the absence of `purposeOfEvent` on an entry "
"as \"not recorded\", not as \"routine access\". What would settle it: the override "
"itself becoming a product behaviour, at which point the question this guide has to "
"answer is whether the override is a coded purpose on the access entry, as here, or a "
"separate record of its own with the access entry pointing at it. The two differ in "
"whether the justification is queryable independently of the access it authorised. "
"Note: Use AuditEvent.agent.purposeOfUse when you know that it is specific to the "
"agent, otherwise use AuditEvent.purposeOfEvent. For example, during a "
"machine-to-machine transfer it might not be obvious to the audit system who caused "
"the event, but it does know why. AuditEvent.purposeOfEvent — 0..*"
),
)
agent: dict | None = Field(
None,
description=(
"Who acted. The actor or actors involved in the interaction. R4 makes this `1..*`. "
"The platform's access log records exactly ONE actor per entry: the identity that "
"made the request. Entries carrying a second agent -- a recipient of a disclosure, a "
"system acting on a user's behalf -- are permitted by R4 and are not produced today. "
"Note: Several agents may be associated (i.e. have some responsibility for an "
"activity) with an event or activity. For example, an activity may be initiated by "
"one user for other users or involve more than one user. However, only one user may "
"be the initiator/requestor for the activity. AuditEvent.agent — 1..*"
),
)
source: dict | None = Field(
None,
description=(
"The system that recorded the entry. The system that is reporting the event. Note: R4 "
"makes `source` 1..1 and `source.observer` 1..1, so any conformant instance must name "
"an observer. The platform's access log does not record which service produced an "
"entry: entries arrive from every service in the estate on one shared channel and "
"carry no producer identity. A projection therefore has to supply the observer from "
"the endpoint doing the projecting rather than read it from the entry. Treat "
"`source.observer` as naming the reporting endpoint, not the service where the "
"interaction occurred. AuditEvent.source — 1..1"
),
)
entity: dict | None = Field(
None,
description=(
"The records the interaction concerned. What the interaction was about. For an "
"interaction with a chart record this is the record itself; an entry may name more "
"than one, and a single interaction that touched several records names each. The "
"platform's log carries the target of the interaction plus a set of correlation "
"identifiers gathered from the enclosing operation, so that the entries written for "
"one transaction can be retrieved together. Projected here, each of those is an "
"`entity`. COVERAGE IS THE WHOLE TYPE SPACE, not the set this guide profiles. Every "
"R4 resource type reaching the FHIR API is classified and logged, including types "
"this specification does not model. A consumer should not assume that the types "
"appearing in an access log are the types described here. Note: Optional (`0..*`) "
"because entries describing an activity rather than a record -- a credential "
"exchange, a report run -- legitimately concern no resource. AuditEvent.entity — 0..*"
),
)
date: FuzzyDate | None = Field(
None,
description=(
"When the activity itself occurred. The period during which the activity occurred. "
"Note: When the entry was recorded, falling back from the activity period where one "
"is given. Not necessarily the instant of the interaction -- see the `recorded` note. "
"Salient field — period.start | recorded"
),
)
performed_by: str | None = Field(
None,
description=(
"The acting user's identity-provider subject. The opaque account identifier the "
"identity provider issues for the acting user (for example `auth0|abc123`). This is "
"the identity the platform actually holds on every entry, and it is the element to "
"match on when correlating an audit entry with the `meta-updated-by` extension on a "
"resource version -- the two carry the same kind of value for the same reason. NOT a "
"FHIR id and NOT a Practitioner reference. Resolving it to a person needs the "
"identity directory, which this guide does not describe. Note: The identity-provider "
"subject of the acting user, as the platform holds it. An opaque account identifier, "
"not a FHIR id and not a Practitioner reference. Resolving it to a person needs the "
"identity directory, which this guide does not describe. Salient field — "
"agent.altId.first()"
),
)