Note

Introduction #

The Note model represents clinical notes that appear on a patient’s chart. A Note can contain multiple commands.

Basic usage #

Retrieve a specific note #

To get a note by identifier, use the get method on the Note model manager:

from canvas_sdk.v1.data.note import Note

note = Note.objects.get(id="89992c23-c298-4118-864a-26cb3e1ae822")

Find all notes for a patient #

If you have a patient object, the notes for a patient can be found using the notes attribute on the Patient instance:

from canvas_sdk.v1.data.patient import Patient

patient = Patient.objects.get(id="fd2ecd87c26044a6a755287f296dd17f")
patient_notes = patient.notes.all()

Retrieve the content of commands in a note #

If you have a note object, the commands for that note can be found using the commands attribute on the Note instance:

from canvas_sdk.v1.data.note import Note

note = Note.objects.get(id="89992c23-c298-4118-864a-26cb3e1ae822")
note_commands = note.commands.all()

You can also filter commands by their state or other attributes:

from canvas_sdk.v1.data.note import Note

note = Note.objects.get(id="89992c23-c298-4118-864a-26cb3e1ae822")

# Get only committed commands
committed_commands = note.commands.filter(state="committed")

# Get commands by schema_key (e.g., prescriptions)
prescriptions = note.commands.filter(schema_key="prescribe")

To access the content of a command, use the data attribute which contains a JSON object with the command’s data:

import json
from canvas_sdk.v1.data.note import Note

note = Note.objects.get(id="89992c23-c298-4118-864a-26cb3e1ae822")

for command in note.commands.all():
    # Get the command type
    command_type = command.schema_key

    # Get the command data as a dictionary
    command_data = command.data

    # Pretty print the command data
    print(f"Command Type: {command_type}")
    print(json.dumps(command_data, indent=2))

For more information about command types and their data structure, see the Command documentation.

Retrieve educational materials for a note #

Educational material shared through the Educational Material command is recorded on the note. If you have a note object, those records can be found using the education_material reverse relation:

from canvas_sdk.v1.data.note import Note

note = Note.objects.get(id="89992c23-c298-4118-864a-26cb3e1ae822")
educational_materials = note.education_material.all()

Understanding the note body structure #

The body of a note is a JSON array that represents the structure and layout of the note. It intermixes text content with references to commands:

import json
from canvas_sdk.v1.data.note import Note

note = Note.objects.get(id="89992c23-c298-4118-864a-26cb3e1ae822")

# The body is an array of objects
print(json.dumps(note.body, indent=2))

The body array contains objects of two types:

  1. Text objects: Represent free-form text content
    {"type": "text", "value": "Patient reports feeling better"}
    
  2. Command objects: Reference commands with their metadata
    {
      "type": "command",
      "value": "reasonForVisit",
      "data": {
        "command_uuid": "691123c4-6c7d-415b-880b-2beefab9f64a"
      }
    }
    

    command_uuid identifies the command and is present on every command object. It matches the id of the Command model. A command object on a note that has not yet moved to the refactored body structure can also carry an id, holding the integer identifier of the record the command created. A note on the refactored structure never carries one, so read the Command through command_uuid and take anchor_object from it instead.

Querying on the body #

body is computed on each access rather than stored in a column, because Canvas assembles it from more than one column. That does not change the value you read, but it does limit which query operations can name it:

OperationSupportedNotes
Note.objects.filter(body=...)YesAlso exclude() and get(), and lookups nested inside a Q object
Note.objects.only("body")YesLoads every column the property reads, so building a body costs no further queries
Note.objects.defer("body")YesDefers all of them
Note.objects.values("body"), values_list("body")NoRaises a FieldError telling you to use only("body"). No single column holds the value to return
Note.objects.order_by("body")NoRaises a FieldError
body named through a relationNoFor example Appointment.objects.defer("note__body") or filter(note__body=...). Query Note itself instead

A body filter reads the column holding that note’s body, and the two columns hold different shapes: a legacy note holds an ordered list of lines, while a note on the refactored structure holds an object keyed by line identifier. A filter written against one shape matches no notes on the other, and returns no rows instead of raising.

So read the body from a note you already have, or filter notes by it, rather than trying to select it as a value:

from canvas_sdk.v1.data.note import Note

# Load only the columns the body needs.
notes = Note.objects.only("body").filter(patient__id="b80b1cdc2e6a4aca90ccebc02e683f35")
for note in notes:
    print(note.body)

Reading a note’s commands #

To work with the commands in a note, use the note’s commands relation rather than walking body. It returns the note’s Command records in a single query, the same way for every note:

from canvas_sdk.v1.data.note import Note

note = Note.objects.get(id="89992c23-c298-4118-864a-26cb3e1ae822")

for command in note.commands.all():
    print(f"Command type: {command.schema_key}")
    print(f"Command data: {command.data}")

commands includes commands that were entered in error; leave them out with note.commands.exclude(state="entered_in_error"). Its results are not in the order the commands appear in the note. When order matters, take the order from body, whose command lines carry a command_uuid matching each command’s id, and still load the commands in one query:

from canvas_sdk.v1.data.note import Note

note = Note.objects.get(id="89992c23-c298-4118-864a-26cb3e1ae822")

commands = {str(command.id): command for command in note.commands.all()}
commands_in_order = [
    commands[line["data"]["command_uuid"]]
    for line in note.body
    if line.get("type") == "command" and line["data"]["command_uuid"] in commands
]

Fetch command details from a command lifecycle event #

When a command lifecycle event such as LAB_ORDER_COMMAND__POST_COMMIT fires, self.target contains the command’s id. Use that ID to load the Command data record and read its data JSON — either to act on it inside the listener, or to expose it through a SimpleAPIRoute endpoint your application can query later by command ID:

import json
from http import HTTPStatus

from canvas_sdk.effects import Effect
from canvas_sdk.effects.simple_api import JSONResponse, Response
from canvas_sdk.handlers.simple_api import APIKeyCredentials, SimpleAPIRoute
from canvas_sdk.v1.data.command import Command


class CommandAPI(SimpleAPIRoute):
    PATH = "/routes/commands/<id>"

    def authenticate(self, credentials: APIKeyCredentials) -> bool:
        ...

    def get(self) -> list[Response | Effect]:
        command_id = self.request.path_params["id"]
        command = Command.objects.get(id=command_id)

        return [
            JSONResponse(json.dumps(command.data), status_code=HTTPStatus.OK)
        ]

Your application would then GET https://<your-instance>.canvasmedical.com/plugin-io/api/<plugin_name>/routes/commands/<id>.

Retrieve the audit history for a note #

The audit history for a note can be found using the NoteStateChangeEvent model. You can access this model directly or through the state_history relation on the note object.

from canvas_sdk.v1.data.note import Note, NoteStateChangeEvent

note = Note.objects.first()

# Use the state_history relation
option_1 = note.state_history.all()

# Use the note object to filter the QuerySet
option_2 = NoteStateChangeEvent.objects.filter(note=note)

# Use the note's UUID to filter the QuerySet, which joins to the note table
# where the note's dbid column is equal to the note_id column of the note
# state change event and the note's id column is equal to the note's UUID.
option_3 = NoteStateChangeEvent.objects.filter(note__id=note.id)

# Use the note's auto-increment database id to filter the QuerySet by the
# foreign key column without joining to the notes table.
option_4 = NoteStateChangeEvent.objects.filter(note_id=note.dbid)

In the above code sample, options 1, 2, and 4 produce identical SQL queries.

Determine if a note is locked #

To see if a note is presently locked, you can use the CurrentNoteStateEvent model to check if the current note state is ‘Locked’. (See: NoteState for an explanation of the different note states you might encounter)

from canvas_sdk.v1.data.note import Note, CurrentNoteStateEvent, NoteStates

note = Note.objects.first()

# You can retrieve the CurrentNoteStateEvent record for the note and check its
# state attribute.
if CurrentNoteStateEvent.objects.get(note=note).state == NoteStates.LOCKED:
    # This note is locked!
    pass

# You can skip retrieving the record by just checking if a
# CurrentNoteStateEvent record exists for that note with the state 'Locked'.
if CurrentNoteStateEvent.objects.filter(note=note, state=NoteStates.LOCKED).exists():
    # This note is locked!
    pass

Retrieve the PDF of a locked or signed note #

Finalizing a note captures it as a PDF showing the note at that moment. A note type that does not require a signature is captured when the note is locked, and a note type with is_sig_required set is captured when the note is signed. The file is stored on a DocumentReference pointing back at the NoteStateChangeEvent that recorded the lock or signature, so you reach it through the note’s state history rather than from the note itself.

Look up a note’s PDF #

Resolve the ContentType at runtime from its stable app_label and model rather than hardcoding the per-environment dbid, and match object_id against the dbid of the note’s lock and signature events:

from canvas_sdk.v1.data import ContentType, DocumentReference, DocumentReferenceStatus
from canvas_sdk.v1.data.note import Note, NoteStates

note = Note.objects.get(id="d2194110-5c9a-4842-8733-ef09ea5ead11")

finalized_events = note.state_history.filter(state__in=[NoteStates.LOCKED, NoteStates.SIGNED])

content_type = ContentType.objects.filter(
    app_label="api", model="notestatechangeevent"
).first()

document = DocumentReference.objects.filter(
    content_type=content_type,
    object_id__in=[event.dbid for event in finalized_events],
    status=DocumentReferenceStatus.CURRENT,
).first()

url = document.document_url if document else None

React when a PDF is captured #

The PDF is generated in the background after the note is locked or signed, so it does not exist yet when the state change itself fires. Listen for DOCUMENT_REFERENCE_CREATED instead. Canvas creates the note’s DocumentReference once the PDF is ready, with related_object already pointing at the lock or signature event, so document_url can be read straight away:

from canvas_sdk.events import EventType
from canvas_sdk.handlers import BaseHandler
from canvas_sdk.v1.data import DocumentReference
from canvas_sdk.v1.data.note import NoteStateChangeEvent
from logger import log


class NotePdfCaptured(BaseHandler):
    RESPONDS_TO = EventType.Name(EventType.DOCUMENT_REFERENCE_CREATED)

    def compute(self):
        document = DocumentReference.objects.get(id=self.event.target.id)
        state_change = document.related_object
        if not isinstance(state_change, NoteStateChangeEvent):
            return []  # a document reference that is not a note PDF

        log.info(f"Note {state_change.note.id} PDF: {document.document_url}")
        return []

document_url is a presigned link that expires after an hour. To export the PDF to an external system, either fetch the file within that window, or send only the note ID and let your application request a fresh link later through a SimpleAPI endpoint you stand up in Canvas.

Determine if a note is signed #

For note types where is_sig_required is True, the terminal state is SGN (Signed) rather than LKD (Locked). To check whether a note is currently signed, retrieve its CurrentNoteStateEvent and compare against NoteStates.SIGNED:

from canvas_sdk.v1.data.note import Note, CurrentNoteStateEvent, NoteStates

note = Note.objects.get(id="89992c23-c298-4118-864a-26cb3e1ae822")
current_state = CurrentNoteStateEvent.objects.filter(note=note).first()
is_signed = current_state is not None and current_state.state == NoteStates.SIGNED

Because a note can be signed, amended, and re-signed, the full state history lives in NoteStateChangeEvent. To get the signer and the timestamp of the most recent signature, query for the latest SGN event and read its originator (the CanvasUser who signed) and created timestamp:

from canvas_sdk.v1.data.note import NoteStateChangeEvent, NoteStates

# `note` is the Note from the previous example
sign_event = NoteStateChangeEvent.objects.filter(
    note=note,
    state=NoteStates.SIGNED,
).order_by("-created").first()

if sign_event:
    signed_at = sign_event.created
    signed_by = sign_event.originator  # CanvasUser who signed

Find all open notes #

You can find all open notes by retrieving the note records with a current state which indicates it can be edited. (See list below)

from canvas_sdk.v1.data.note import Note, CurrentNoteStateEvent, NoteStates

open_note_states = [
    NoteStates.NEW,
    NoteStates.PUSHED,
    NoteStates.CONVERTED,
    NoteStates.UNLOCKED,
    NoteStates.RESTORED,
    NoteStates.UNDELETED,
]

# This will execute one query per CurrentNoteStateEvent object returned
open_notes_via_list_comprehension = [event.note for event in CurrentNoteStateEvent.objects.filter(state__in=open_note_states)]

# This will always execute two queries: one to find the note ids of open
# notes, and a second query to fetch the note records by the ids returned in the
# first query
open_note_ids = CurrentNoteStateEvent.objects.filter(state__in=open_note_states).values_list('note_id', flat=True)
open_notes_via_multiple_queries = Note.objects.filter(dbid__in=open_note_ids)

Get the current state of a given note #

To get a note’s current state, retrieve its CurrentNoteStateEvent and check the state attribute. If you are trying to assess if the current note state represents that note as being editable, you can call the editable() method on the CurrentNoteStateEvent object.

from canvas_sdk.v1.data.note import Note, CurrentNoteStateEvent

note = Note.objects.first()

current_note_state = CurrentNoteStateEvent.objects.get(note=note).state
is_editable = current_note_state.editable()

Get the current claim of a given note #

You can retrieve the current claim using the method get_claim() presented in the Note object.

from canvas_sdk.v1.data.note import Note

note = Note.objects.first()
claim = note.get_claim()

Get the NoteType of a given note #

A note’s type is available through its note_type_version attribute, which returns the related NoteType object:

from canvas_sdk.v1.data.note import Note

note = Note.objects.get(id="89992c23-c298-4118-864a-26cb3e1ae822")

# Get the note type name (e.g., "Office Visit")
note_type_name = note.note_type_version.name

# Access other note type attributes
note_type_display = note.note_type_version.display
note_type_code = note.note_type_version.code
note_type_system = note.note_type_version.system

Filtering #

By attribute #

Notes can also be filtered by attribute. For example, to get all notes for a patient where the datetime_of_service is after a certain date, the following code can be used:

import arrow

from canvas_sdk.v1.data.note import Note
from canvas_sdk.v1.data.patient import Patient

patient = Patient.objects.get(id="fd2ecd87c26044a6a755287f296dd17f")
recent_notes = Note.objects.filter(
    patient=patient,
    datetime_of_service__gte=arrow.now().shift(weeks=-3).datetime
)

The NoteType model can also be used to find notes by type.

from canvas_sdk.v1.data.note import Note
from canvas_sdk.v1.data.note import NoteType
from canvas_sdk.v1.data.patient import Patient

note_type = NoteType.objects.get(name="Office visit")
patient = Patient.objects.get(id="fd2ecd87c26044a6a755287f296dd17f")
patient_office_visits = Note.objects.filter(patient=patient, note_type_version=note_type)

Attributes #

Note #

Field NameTypeNotes
idUUID 
dbidInteger 
createdDateTime 
modifiedDateTime 
patientPatient 
note_type_versionNoteType 
titleString 
bodyJSON (computed)Array of objects representing the note structure. Each object has a type (either "text" or "command") and a value. Command objects also carry a data field holding command_uuid (matching the Command id); older notes may additionally include an integer id. See Understanding the note body structure.
originatorCanvasUser 
providerStaff 
supervising_providerStaffThe note’s supervising provider, if one has been set
last_modified_by_staffStaffThe staff member who last modified the note
checksumString 
billing_noteString 
related_dataJSONCan contain one key, roomNumber, if the Note is an inpatient stay.
datetime_of_serviceDateTime 
place_of_serviceString 
encounterEncounter 
locationPracticeLocationThe practice location associated with the note
commandsQuerySet[Command]All commands associated with this note
clipboardsQuerySet[Clipboard]All clipboard commands recorded on this note
custom_commandsQuerySet[CustomCommand]All custom commands recorded on this note
note_tasksQuerySet[NoteTask]All tasks associated with this note
metadataQuerySet[NoteMetadata]All metadata key-value pairs associated with this note
lab_reviewsQuerySet[LabReview]All lab reviews associated with this note
imaging_reviewsQuerySet[ImagingReview]All imaging reviews associated with this note
referral_reviewsQuerySet[ReferralReview]All referral reviews associated with this note
chart_section_reviewsQuerySet[ChartSectionReview]All chart section reviews associated with this note
visual_exam_findingsQuerySet[VisualExamFinding]All visual exam findings associated with this note
state_historyQuerySet[NoteStateChangeEvent]The note’s state-change audit history
action_eventsQuerySet[NoteActionEvent]Faxes of this note, with their delivery status
current_stateCurrentNoteStateEventThe note’s current state event
assessmentsQuerySet[Assessment]All assessments associated with this note
goalsQuerySet[Goal]All goals associated with this note
updategoalsQuerySet[UpdateGoal]All goal updates and closures recorded on this note
instructionsQuerySet[Instruction]All instructions associated with this note
immunizationsQuerySet[Immunization]All immunizations associated with this note
claimsQuerySet[Claim]All claims associated with this note (see the get_claim() method)
letterLetterThe letter associated with this note, if any
referral_setQuerySet[Referral]All referrals associated with this note
laborder_setQuerySet[LabOrder]All lab orders associated with this note
appointment_setQuerySet[Appointment]All appointments associated with this note
education_materialQuerySet[EducationalMaterial]All educational materials recorded on this note
proceduresQuerySet[Procedure]All procedures recorded on this note
family_historiesQuerySet[FamilyHistory]All family history records recorded on this note
plansQuerySet[Plan]All plans recorded on this note
follow_upsQuerySet[FollowUp]All follow-ups recorded on this note
reasons_for_visitQuerySet[ReasonForVisit]All reasons for visit recorded on this note
assessed_coding_gapsQuerySet[AssessCodingGapEvent]All coding gaps assessed on this note
assessed_detected_issuesQuerySet[ValidateCodingGapEvent]All coding gaps validated on this note
created_detected_issuesQuerySet[CreateCodingGapEvent]All coding gaps created on this note
deferred_detected_issuesQuerySet[DeferCodingGapEvent]All coding gaps deferred on this note
removed_allergiesQuerySet[RemoveAllergyEvent]All allergies removed on this note
removed_past_medical_historyQuerySet[RemovePastMedicalHistoryEvent]All past medical history entries removed on this note
resolved_conditionsQuerySet[ResolveConditionEvent]All conditions resolved on this note
histories_of_present_illnessQuerySet[HistoryOfPresentIllness]All histories of present illness recorded on this note
vital_sign_readingsQuerySet[VitalSignReading]All vital sign readings recorded on this note
cancel_prescriptionsQuerySet[CancelPrescription]All prescription cancellations recorded on this note
prescription_change_requestsQuerySet[PrescriptionChangeRequest]All pharmacy change requests recorded on this note
prescription_change_responsesQuerySet[PrescriptionChangeResponse]All responses to change requests recorded on this note

NoteType #

Field NameType
idUUID
dbidInteger
createdDateTime
modifiedDateTime
systemString
versionString
codeString
displayString
user_selectedBoolean
nameString
iconString
categoryNoteTypeCategories
rankInteger
is_default_appointment_typeBoolean
is_scheduleableBoolean
is_telehealthBoolean
is_billableBoolean
defer_place_of_service_to_practice_locationBoolean
available_places_of_serviceArray[PracticeLocationPOS]
default_place_of_servicePracticeLocationPOS
is_system_managedBoolean
is_visibleBoolean
is_activeBoolean
unique_identifierUUID
deprecated_atDateTime
is_patient_requiredBoolean
allow_custom_titleBoolean
is_scheduleable_via_patient_portalBoolean
online_durationInteger
is_sig_requiredBoolean
notesQuerySet[Note]
appointmentsQuerySet[Appointment]

NoteMetadata #

Field NameType
idUUID
dbidInteger
createdDateTime
modifiedDateTime
noteNote
keyString
valueString
from canvas_sdk.v1.data.note import Note
from logger import log

note_id = "89992c23-c298-4118-864a-26cb3e1ae822"
note = Note.objects.get(id=note_id)
note_metadata = note.metadata.all()

for metadata in note_metadata:
   log.info(f"Note metadata: {metadata.key}, {metadata.value}")

NoteStateChangeEvent #

Field NameType
idUUID
dbidInteger
createdDateTime
modifiedDateTime
noteNote
originatorCanvasUser
stateNoteState
note_state_documentString
note_state_htmlString

CurrentNoteStateEvent #

Field NameType
idUUID
dbidInteger
stateNoteState
noteNote

Enumeration types #

NoteStates #

ValueDescriptionNotes
NEWCreated 
PSHPushed the charges for 
LKDLocked 
ULKUnlocked 
DLTDeleted 
RLKRelocked 
RSTRestored 
RCLRecalled 
UNDUndeleted 
DSCDischarged 
SGNSignedUsed when the note type’s is_sig_required is True
SCHSchedulingUsed in appointment notes
BKDBookedUsed in appointment notes
CVDConvertedUsed in appointment notes
CLDCanceledUsed in appointment notes
NSWNo showUsed in appointment notes
RVTRevertedUsed in appointment notes
CNFConfirmedUsed for CCDA import notes

NoteTypeCategories #

ValueDescription
messageMessage
letterLetter
inpatientInpatient Visit Note
reviewChart Review Note
encounterEncounter Note
appointmentAppointment Note
taskTask
dataData
ccdaC-CDA
schedule_eventSchedule Event

PracticeLocationPOS #

ValueDescription
01Pharmacy
02Telehealth
03Education Facility
04Homeless Shelter
09Prison
10Telehealth in Patient’s Home
11Office
12Home
13Asssisted Living Facility
14Group Home
15Mobile Unit
17Walk-In Retail Health Clinic
19Off-Campus Outpatient Hospital
20Urgent Care Facility
21Inpatient Hospital
22On-Campus Outpatient Hospital
23Emergency Room Hospital
24Ambulatory Surgery Center
25Birthing Center
26Military Treatment Facility
27Outreach Site / Street
31Skilled Nursing Facility
32Nursing Facility
33Custodial Care Facility
34Hospice
41Ambulance Land
42Ambulance Air or Water
49Independent Clinic
50Federally Qualified Health Center
51Inpatient Psychiatric Facility
52Inpatient Psychiatric Facility - Partial Hospitalization
53Community Mental Health Center
54Intermediate Care Facility for Mentally Retarded
55Residential Substance Abuse Treatment Facility
56Psychiatric Residential Treatment Center
57Non-Residential Substance Abuse Treatment Facility
60Mass Immunization Center
61Inpatient Rehabilitation Facility
62Outpatient Rehabilitation Facility
65End-Stage Renal Disease Treatment Facility
71State or Local Public Health Clinic
72Rural Health Clinic
81Independent Laboratory
99Other Place of Service