Skip to content

AuditEvent Profile

Canonical../StructureDefinition/nexus-emr-core-auditevent
Statusdraft (experimental) · 1.26.0
BaseAuditEvent (constraint)
SourceFSH · 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 AuditEventSchemamodel_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()"
        ),
    )