Skip to content

Appointment Profile

Canonical../StructureDefinition/nexus-emr-core-appointment
Statusdraft · 1.26.0
BaseAppointment (constraint)
SourceFSH · JSON

Minimal Nexus EMR profile for Appointment resources, primarily for mapping status, start date (Last Visit Date), and required patient/practitioner participants. Assumes filtering for status='fulfilled' happens during data retrieval/processing. Contains embedded aliases and invariants.

FSH

The authored source of truth: resources/StructureDefinition-nexus-emr-core-appointment.fsh (edit on GitHub). One FSH file may define several related artifacts.

// NexusEmrCoreAppointment.fsh

Alias: $NexusEmrCoreAppointment = ../StructureDefinition/nexus-emr-core-appointment




// --- Profile Definition ---

Invariant:     appointment-participant-location
Description:   "Must have exactly one participant whose actor references a Location"
Severity:      #error
Expression:    "participant.where(actor.reference.matches('(^|/)Location/')).count() = 1"

Invariant:     appointment-participant-patient
Description:   "At most one participant whose actor references a Patient"
Severity:      #error
Expression:    "participant.where(actor.reference.matches('(^|/)Patient/')).count() <= 1"

// The provider participant is a PRACTITIONER by default. Practitioner is the primary
// provider identity in this IG (see the Practitioner profile: it is the stable clinical
// identity an EMR user login resolves to, and it carries the user-account identifier).
// PractitionerRole remains permitted for the cases where the clinic/role binding is the
// thing being asserted, so this invariant counts the provider participant WITHOUT
// privileging either spelling: at most one provider, whichever resource names them.
//
// NAMING: the key names the resource type this IG treats as the primary provider identity
// (`Practitioner`), matching every sibling key in this file -- they are named for the
// referenced RESOURCE TYPE (`-patient`, `-location`), not for a role or a job title. The
// Description, not the key, carries the permitted set. Do not rename it to a role word:
// `-doctor` would be wrong twice over (nurses and other non-physician providers hold this
// participant, and no sibling key names a role), and `-provider` -- the 1.12.0 spelling --
// named an abstraction with no resource type behind it.
//
// HISTORY -- consumers matching on the invariant key need this. The key has been renamed
// twice: `appointment-participant-practitionerrole` (pre-1.12.0, when the rule counted only
// PractitionerRole actors) -> `appointment-participant-provider` (1.12.0, when the rule was
// widened to count either spelling) -> `appointment-participant-practitioner` (1.15.0, for
// the naming reason above). The RULE has not changed since 1.12.0; only the key has.
Invariant:     appointment-participant-practitioner
Description:   "At most one participant whose actor references a provider (Practitioner or PractitionerRole)"
Severity:      #error
Expression:    "participant.where(actor.reference.matches('(^|/)Practitioner/') or actor.reference.matches('(^|/)PractitionerRole/')).count() <= 1"

Profile:        NexusEmrCoreAppointment
Parent:         Appointment
Id:             nexus-emr-core-appointment
Title:          "Appointment Profile"
Description:    "Minimal Nexus EMR profile for Appointment resources, primarily for mapping status, start date (Last Visit Date), and required patient/practitioner participants. Assumes filtering for status='fulfilled' happens during data retrieval/processing. Contains embedded aliases and invariants."
* ^url = $NexusEmrCoreAppointment // Canonical URL from embedded alias
* ^status = #draft

// THE effective date for this profile (see the EffectiveDate / NoEffectiveDate
// RuleSets): one date concept per resource, declared here rather than guessed.
* insert EffectiveDate([[start]])

// Salient fields — the reader/render surface (see SalientField + the Salient RuleSet).
// Scoped to what a day-sheet / inter-visit summary reads off a booking: when it is, what
// kind of visit it is, why, and whether it actually happened. Participants (patient,
// practitioner role, location) are deliberately absent — the chart is already
// patient-scoped, and a mixed actor list renders as mush rather than as an answer.
* insert Salient(status, status, code)
* insert Salient(start, start, dateTime)
* insert Salient(end, end, dateTime)
* insert Salient(appointment_type, appointmentType, coded)
* insert Salient(service_type, serviceType, coded)
* insert Salient(reason_code, reasonCode, coded)
* insert Salient(description, description, string)
* insert Salient(cancelation_reason, cancelationReason, coded)

// SMART on FHIR exposure -- this resource is reachable by an app, and these are the
// permissions each operation costs (see SmartExposed + the SmartExposed RuleSet).
* insert SmartExposed(appointment)

* ^publisher = "WELLSTAR Technologies Corp."

// --- Element Constraints ---

* . ^short      = "Nexus EMR Appointment Core"
* . ^definition = """
  Represents scheduled appointments and booking information, capturing key details
  like status, start/end time, and participants using standard Nexus EMR references.
  Uses instance-specific identifiers for tracking appointments across systems.
  """

* meta.source 0..1 MS
* meta.source ^short = "Identifies EMR instance & pipeline version the resource came from"
* meta.source ^definition = "A URI that identifies the EMR pipeline and version from which this resource originated.  This tells you which EMR instance (recommend using the instance identifier), and the version of the pipeline code/transformations."

* text 0..1 MS // Narrative element with human-readable summary
* text ^short = "Narrative"
* text ^definition = "A human-readable narrative summary of the NexusEmrCoreAppointment resource for human interpretation."

// Identifier: A business identifier is recommended but not required (any system).
* identifier 0..* MS
* identifier.system 1..1
* identifier.value 1..1

* identifier ^short = "Business identifier(s) for appointment"
* identifier ^definition = """
Unique identifiers for the appointment.

A business identifier is recommended but not required (identifier is 0..*). Where available, carry the source-system identifier using the most appropriate system URI. No specific canonical system is mandated.
"""
* insert SourceIdentifier(identifier)


// Status: booked, arrived, fulfilled, etc. Mandatory and MS.
// Filtering for 'fulfilled' status is expected during query/processing, not enforced here.
* status 1..1 MS
* status ^short = "Where the appointment is in its lifecycle"
* status ^definition = "The overall status of the Appointment. Indicates confirmation status and attendance."
* status from http://hl7.org/fhir/ValueSet/appointmentstatus (required)
* status ^comment = "Not all EMRs support a 'fulfilled' status.  In those cases we will only have a 'booked', 'arrived' or 'checked in' status.  For those consumers of FHIR data that are depending on this (including CPAR), a reasonable workaround is checking for `fulfilled || ((booked || arrived || checked in) && before yesterday)`"

// Start Date/Time: When the appointment is scheduled to begin. Mandatory and MS.
* start 1..1 MS
* start ^short = "When appointment is to take place - Maps to A12_Last Visit Date"
* start ^definition = "Date/Time that the appointment is scheduled to start."
* start ^comment = "This is the scheduled start time, not necessarily when the patient was actually seen."

// Participant: exactly one Location, at most one Patient, at most one provider. MS.
* participant 1..* MS // Must have at least the location
* participant ^short = "Participants involved in appointment"
* participant ^definition = """
List of participants involved in the appointment: the patient, the provider, and the location.

The provider participant references a **Practitioner** -- the primary provider identity in this
IG. A **PractitionerRole** reference is also permitted for the cases where the clinic/role
binding is what is being asserted (multi-org providers, role-scoped booking), but Practitioner
is the expected default and the shape primary-care writers should emit.

An actor may also be identifier-only (no `reference`), per the Nexus EMR logical-reference model.
Consumers must handle that: see the `-example` and `-telephone-example` instances.
"""
* participant ^comment = "Must include exactly one location. Can contain zero or one patient and zero or one provider (Practitioner, or PractitionerRole where the role binding matters)."
// Participant composition (exactly one Location; at most one Patient and one
// provider) is enforced by reference-type invariants rather than slicing:
// profile-discriminated slicing on actor.resolve() is unenforceable for Nexus EMR's
// logical (non-resolvable) references — see meta/history 2026-07-06 evaluable-slicing.
* participant.actor 1..1 MS

// Participation type — the role the actor plays in the booking. Documented as MS
// because third-party consumers read it to decide what a participant IS: Ocean
// skips a patient actor that carries no type, so an untyped booking never
// reaches its reminders list. Both codes sit inside the base extensible binding,
// so nothing here widens it.
* participant.type 0..* MS
* participant.type ^short = "Role the actor plays in this appointment"
* participant.type ^definition = """
The role the participant plays in the appointment, from
`http://terminology.hl7.org/CodeSystem/v3-ParticipationType`. SHOULD be populated
on the actors a writer controls: `PART` (Participation) on the patient, `PPRF`
(primary performer) on the provider. Both are in the bound
`encounter-participant-type` value set.

Consumers outside this ecosystem use the type rather than the referenced resource
type to tell participants apart, and at least one (Ocean) drops a patient
participant that carries none — so an untyped actor is readable here and
invisible there.
"""

* obeys appointment-participant-location and appointment-participant-patient and appointment-participant-practitioner


// Slot linkage — populated by booking clients when creating an Appointment from a
// free Slot. The booking system (not the converter, not the IG) owns the
// Slot.status transition from 'free' to 'busy'. Cardinality is 0..* (not 1..*)
// for backward compatibility — historical and walk-in appointments lack a Slot.
* slot 0..* MS
* slot only Reference(NexusEmrCoreSlot)
* slot ^short = "Slot(s) this appointment was booked into"
* slot ^definition = """
The Slot resource(s) that this appointment was created from. When a booking client
creates an Appointment in response to a user selecting a free Slot, it SHOULD
populate this reference. The booking system is responsible for transitioning the
referenced Slot's status from 'free' to 'busy'.

Walk-in and historical appointments (imported from EMRs predating the Slot model)
will not have a Slot reference — `.slot` is therefore `0..*`, not `1..*`.
"""


// requested start date/time for the appointment (may be different than actual start date/time)
* requestedPeriod.start 0..1 MS
* requestedPeriod.start ^short = "Requested date/time for appointment"
* requestedPeriod.start ^definition = "The period of time that was originally requested for the appointment."
* requestedPeriod.start ^comment = "May differ from the actual scheduled time due to availability."

// ServiceCategory: Broad categorization of the service
* serviceCategory 0..* MS
* serviceCategory ^short = "Service category with raw code support"
* insert SourceCoding(serviceCategory)
* serviceCategory ^definition = """
A broad categorization of the service that is to be performed during this appointment.

RECOMMENDED: Standard terminology codes when available
OPTIONAL: Raw EMR codes for source system preservation
"""
* serviceCategory.coding.system 1..1
* serviceCategory.coding.code 1..1


// ServiceType: Specific service to be performed
* serviceType 0..* MS
* serviceType ^short = "Service type with raw code support"
* serviceType ^definition = """
The specific service that is to be performed during this appointment.

RECOMMENDED: Standard terminology codes when available
OPTIONAL: Raw EMR codes for source system preservation

Where this code was mapped from a source system's own vocabulary, carry the raw coding alongside the mapped one, flagged `userSelected = true`. See [Carrying the raw code](../guide/patterns/raw-codes.md).
"""
* serviceType.coding.system 1..1
* serviceType.coding.code 1..1


// AppointmentType: Style of appointment or patient
* appointmentType 0..1 MS
* appointmentType ^short = "Appointment type with raw code support"
* insert SourceCoding(appointmentType)
* appointmentType ^definition = """
The style of appointment or patient that has been booked (e.g., routine, walk-in, checkup).

RECOMMENDED: v2 Appointment Reason Codes or other standard terminology when available
OPTIONAL: Raw EMR codes for source system preservation
"""


// Modality: delivery channel (in-person / video / telephone / secure message).
// A DISTINCT axis from appointmentType (booking style) — see the extension's own
// docs for why this is an extension and not a coding on appointmentType.
* extension contains AppointmentModalityExtension named modality 0..1 MS
* extension[modality] ^short = "How the appointment is delivered"


// CreatedBy / created: who and when the appointment record was authored (FHIR-19).
// R4 Appointment has no native author element and participants model ATTENDANCE, not
// record authorship (the creator -- e.g. front-desk staff -- is often not an attendee).
// The "who" is the createdBy extension (target mirrors Composition.author to cover
// user / self-booking patient / system-device cases); the "when" is native
// Appointment.created, marked must-support here.
* extension contains AppointmentCreatedByExtension named createdBy 0..1 MS
* extension[createdBy] ^short = "Who/what created the appointment (authoring user / system / self-booking patient)"
* created 0..1 MS
* created ^short = "When the appointment record was initially created"
* created ^definition = "The date/time the appointment record was initially created (native R4 Appointment.created). Pairs with the createdBy extension, which carries the authoring agent."


// ReasonCode: Reason the appointment is scheduled
* reasonCode 0..* MS
* reasonCode ^short = "Appointment reason with raw code support"
* insert SourceCoding(reasonCode)
* reasonCode ^definition = """
Coded reason this appointment is scheduled.

RECOMMENDED: Encounter Reason Codes or other standard terminology when available
OPTIONAL: Raw EMR codes for source system preservation
"""

// Invariant-style (evaluable-slicing convention): no coding slices — hoisted base
// constraints apply to EVERY coding; the raw-code SHAPE rule is a portable invariant.
// Raw codings are OPTIONAL — the invariant constrains their shape only when present.
* reasonCode.coding.system 1..1
* reasonCode.coding.system ^short = "Standard terminology URI, or `../NamingSystem/<instance-id>-appointment-reasoncode-raw-code`"
* reasonCode.coding.code 1..1