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:
- Text objects: Represent free-form text content
{"type": "text", "value": "Patient reports feeling better"} - Command objects: Reference commands with their metadata
{ "type": "command", "value": "reasonForVisit", "data": { "command_uuid": "691123c4-6c7d-415b-880b-2beefab9f64a" } }command_uuididentifies the command and is present on every command object. It matches theidof the Command model. A command object on a note that has not yet moved to the refactored body structure can also carry anid, holding the integer identifier of the record the command created. A note on the refactored structure never carries one, so read the Command throughcommand_uuidand takeanchor_objectfrom 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:
| Operation | Supported | Notes |
|---|---|---|
Note.objects.filter(body=...) | Yes | Also exclude() and get(), and lookups nested inside a Q object |
Note.objects.only("body") | Yes | Loads every column the property reads, so building a body costs no further queries |
Note.objects.defer("body") | Yes | Defers all of them |
Note.objects.values("body"), values_list("body") | No | Raises a FieldError telling you to use only("body"). No single column holds the value to return |
Note.objects.order_by("body") | No | Raises a FieldError |
body named through a relation | No | For 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 Name | Type | Notes |
|---|---|---|
| id | UUID | |
| dbid | Integer | |
| created | DateTime | |
| modified | DateTime | |
| patient | Patient | |
| note_type_version | NoteType | |
| title | String | |
| body | JSON (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. |
| originator | CanvasUser | |
| provider | Staff | |
| supervising_provider | Staff | The note’s supervising provider, if one has been set |
| last_modified_by_staff | Staff | The staff member who last modified the note |
| checksum | String | |
| billing_note | String | |
| related_data | JSON | Can contain one key, roomNumber, if the Note is an inpatient stay. |
| datetime_of_service | DateTime | |
| place_of_service | String | |
| encounter | Encounter | |
| location | PracticeLocation | The practice location associated with the note |
| commands | QuerySet[Command] | All commands associated with this note |
| clipboards | QuerySet[Clipboard] | All clipboard commands recorded on this note |
| custom_commands | QuerySet[CustomCommand] | All custom commands recorded on this note |
| note_tasks | QuerySet[NoteTask] | All tasks associated with this note |
| metadata | QuerySet[NoteMetadata] | All metadata key-value pairs associated with this note |
| lab_reviews | QuerySet[LabReview] | All lab reviews associated with this note |
| imaging_reviews | QuerySet[ImagingReview] | All imaging reviews associated with this note |
| referral_reviews | QuerySet[ReferralReview] | All referral reviews associated with this note |
| chart_section_reviews | QuerySet[ChartSectionReview] | All chart section reviews associated with this note |
| visual_exam_findings | QuerySet[VisualExamFinding] | All visual exam findings associated with this note |
| state_history | QuerySet[NoteStateChangeEvent] | The note’s state-change audit history |
| action_events | QuerySet[NoteActionEvent] | Faxes of this note, with their delivery status |
| current_state | CurrentNoteStateEvent | The note’s current state event |
| assessments | QuerySet[Assessment] | All assessments associated with this note |
| goals | QuerySet[Goal] | All goals associated with this note |
| updategoals | QuerySet[UpdateGoal] | All goal updates and closures recorded on this note |
| instructions | QuerySet[Instruction] | All instructions associated with this note |
| immunizations | QuerySet[Immunization] | All immunizations associated with this note |
| claims | QuerySet[Claim] | All claims associated with this note (see the get_claim() method) |
| letter | Letter | The letter associated with this note, if any |
| referral_set | QuerySet[Referral] | All referrals associated with this note |
| laborder_set | QuerySet[LabOrder] | All lab orders associated with this note |
| appointment_set | QuerySet[Appointment] | All appointments associated with this note |
| education_material | QuerySet[EducationalMaterial] | All educational materials recorded on this note |
| procedures | QuerySet[Procedure] | All procedures recorded on this note |
| family_histories | QuerySet[FamilyHistory] | All family history records recorded on this note |
| plans | QuerySet[Plan] | All plans recorded on this note |
| follow_ups | QuerySet[FollowUp] | All follow-ups recorded on this note |
| reasons_for_visit | QuerySet[ReasonForVisit] | All reasons for visit recorded on this note |
| assessed_coding_gaps | QuerySet[AssessCodingGapEvent] | All coding gaps assessed on this note |
| assessed_detected_issues | QuerySet[ValidateCodingGapEvent] | All coding gaps validated on this note |
| created_detected_issues | QuerySet[CreateCodingGapEvent] | All coding gaps created on this note |
| deferred_detected_issues | QuerySet[DeferCodingGapEvent] | All coding gaps deferred on this note |
| removed_allergies | QuerySet[RemoveAllergyEvent] | All allergies removed on this note |
| removed_past_medical_history | QuerySet[RemovePastMedicalHistoryEvent] | All past medical history entries removed on this note |
| resolved_conditions | QuerySet[ResolveConditionEvent] | All conditions resolved on this note |
| histories_of_present_illness | QuerySet[HistoryOfPresentIllness] | All histories of present illness recorded on this note |
| vital_sign_readings | QuerySet[VitalSignReading] | All vital sign readings recorded on this note |
| cancel_prescriptions | QuerySet[CancelPrescription] | All prescription cancellations recorded on this note |
| prescription_change_requests | QuerySet[PrescriptionChangeRequest] | All pharmacy change requests recorded on this note |
| prescription_change_responses | QuerySet[PrescriptionChangeResponse] | All responses to change requests recorded on this note |
NoteType #
| Field Name | Type |
|---|---|
| id | UUID |
| dbid | Integer |
| created | DateTime |
| modified | DateTime |
| system | String |
| version | String |
| code | String |
| display | String |
| user_selected | Boolean |
| name | String |
| icon | String |
| category | NoteTypeCategories |
| rank | Integer |
| is_default_appointment_type | Boolean |
| is_scheduleable | Boolean |
| is_telehealth | Boolean |
| is_billable | Boolean |
| defer_place_of_service_to_practice_location | Boolean |
| available_places_of_service | Array[PracticeLocationPOS] |
| default_place_of_service | PracticeLocationPOS |
| is_system_managed | Boolean |
| is_visible | Boolean |
| is_active | Boolean |
| unique_identifier | UUID |
| deprecated_at | DateTime |
| is_patient_required | Boolean |
| allow_custom_title | Boolean |
| is_scheduleable_via_patient_portal | Boolean |
| online_duration | Integer |
| is_sig_required | Boolean |
| notes | QuerySet[Note] |
| appointments | QuerySet[Appointment] |
NoteMetadata #
| Field Name | Type |
|---|---|
| id | UUID |
| dbid | Integer |
| created | DateTime |
| modified | DateTime |
| note | Note |
| key | String |
| value | String |
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 Name | Type |
|---|---|
| id | UUID |
| dbid | Integer |
| created | DateTime |
| modified | DateTime |
| note | Note |
| originator | CanvasUser |
| state | NoteState |
| note_state_document | String |
| note_state_html | String |
CurrentNoteStateEvent #
| Field Name | Type |
|---|---|
| id | UUID |
| dbid | Integer |
| state | NoteState |
| note | Note |
Enumeration types #
NoteStates #
| Value | Description | Notes |
|---|---|---|
| NEW | Created | |
| PSH | Pushed the charges for | |
| LKD | Locked | |
| ULK | Unlocked | |
| DLT | Deleted | |
| RLK | Relocked | |
| RST | Restored | |
| RCL | Recalled | |
| UND | Undeleted | |
| DSC | Discharged | |
| SGN | Signed | Used when the note type’s is_sig_required is True |
| SCH | Scheduling | Used in appointment notes |
| BKD | Booked | Used in appointment notes |
| CVD | Converted | Used in appointment notes |
| CLD | Canceled | Used in appointment notes |
| NSW | No show | Used in appointment notes |
| RVT | Reverted | Used in appointment notes |
| CNF | Confirmed | Used for CCDA import notes |
NoteTypeCategories #
| Value | Description |
|---|---|
| message | Message |
| letter | Letter |
| inpatient | Inpatient Visit Note |
| review | Chart Review Note |
| encounter | Encounter Note |
| appointment | Appointment Note |
| task | Task |
| data | Data |
| ccda | C-CDA |
| schedule_event | Schedule Event |
PracticeLocationPOS #
| Value | Description |
|---|---|
| 01 | Pharmacy |
| 02 | Telehealth |
| 03 | Education Facility |
| 04 | Homeless Shelter |
| 09 | Prison |
| 10 | Telehealth in Patient’s Home |
| 11 | Office |
| 12 | Home |
| 13 | Asssisted Living Facility |
| 14 | Group Home |
| 15 | Mobile Unit |
| 17 | Walk-In Retail Health Clinic |
| 19 | Off-Campus Outpatient Hospital |
| 20 | Urgent Care Facility |
| 21 | Inpatient Hospital |
| 22 | On-Campus Outpatient Hospital |
| 23 | Emergency Room Hospital |
| 24 | Ambulatory Surgery Center |
| 25 | Birthing Center |
| 26 | Military Treatment Facility |
| 27 | Outreach Site / Street |
| 31 | Skilled Nursing Facility |
| 32 | Nursing Facility |
| 33 | Custodial Care Facility |
| 34 | Hospice |
| 41 | Ambulance Land |
| 42 | Ambulance Air or Water |
| 49 | Independent Clinic |
| 50 | Federally Qualified Health Center |
| 51 | Inpatient Psychiatric Facility |
| 52 | Inpatient Psychiatric Facility - Partial Hospitalization |
| 53 | Community Mental Health Center |
| 54 | Intermediate Care Facility for Mentally Retarded |
| 55 | Residential Substance Abuse Treatment Facility |
| 56 | Psychiatric Residential Treatment Center |
| 57 | Non-Residential Substance Abuse Treatment Facility |
| 60 | Mass Immunization Center |
| 61 | Inpatient Rehabilitation Facility |
| 62 | Outpatient Rehabilitation Facility |
| 65 | End-Stage Renal Disease Treatment Facility |
| 71 | State or Local Public Health Clinic |
| 72 | Rural Health Clinic |
| 81 | Independent Laboratory |
| 99 | Other Place of Service |