Upcoming Change - Clinical Note Body Structure
Canvas is changing how a clinical note stores its body, from a single block of content to a set of individually addressable lines.
Nothing looks or behaves differently. Charting, commands, signing, locking, printing, and the note’s appearance are exactly as they are today. This is a change to the storage underneath, and what matters about it is what it made possible:
- Two clinicians can work in the same note at once, with their edits merging.
- A command that is originated always lands in the note, or is rolled back completely.
- Changes made elsewhere appear without a reload.
- A long note stays responsive as it grows.
None of that was reachable while the body was a single block, because nothing could refer to one line of it.
When your instance gets it #
Canvas turns the new structure on in two waves:
- September 21, 2026 for instances without a read-only replica.
- September 28, 2026 for instances with one.
What happens on your instance #
- New notes use the new structure, and your existing notes are migrated onto it in batches outside business hours.
- A note looks the same before and after it migrates.
- The end state is that every note on your instance uses the new structure.
Breaking change: reading a note body from the read-only replica #
An integration that reads note bodies from the read-only replica needs a query change in place by September 28, 2026. This is the one place the structure is visible, because SQL reads the stored columns directly rather than going through the SDK.
- A note’s
versionisNULLbefore it is migrated and2after. - On a migrated note the legacy body column is empty.
- The lines move to two columns:
body_content, an object keyed by line identifier, andbody_order, an array of those identifiers in order. checksumis empty and stays empty, because the new structure controls concurrency per line rather than across the whole note. Use themodifiedtimestamp to find notes that changed since your last run.
{
"body_order": [
"0d5f2c81-4a19-4e77-b3c2-7e1a9f480b6d",
"5f3b1a90-7c42-4e18-9a6d-2b81cc4f0e77",
"86da9457-c9d3-429c-9dfb-5b33a12934da",
"c71e4d38-9b05-42af-8e13-6f9a0d2b5c84"
],
"body_content": {
"0d5f2c81-4a19-4e77-b3c2-7e1a9f480b6d": { "type": "text", "value": "Patient reports feeling better" },
"86da9457-c9d3-429c-9dfb-5b33a12934da": { "type": "command", "value": "diagnose" }
}
}
- That is four lines: the text, a blank, the Diagnose command, and another blank.
- An identifier with no
body_contententry is a blank line. Most lines are blank, because Canvas puts one on each side of a command. - A command line’s identifier is its key, which is how you resolve it to a row in the command table.
This query reads a body in the new structure, one row per line, in order:
SELECT n.id AS note_id,
ord.idx AS line_number,
COALESCE(n.body_content -> ord.line_uuid::text ->> 'type', 'text') AS line_type,
COALESCE(n.body_content -> ord.line_uuid::text ->> 'value', '') AS line_value,
CASE WHEN n.body_content -> ord.line_uuid::text ->> 'type' = 'command'
THEN ord.line_uuid::text END AS command_uuid
FROM canvas_sdk_data_api_note_001 n
CROSS JOIN LATERAL unnest(n.body_order) WITH ORDINALITY AS ord(line_uuid, idx)
- The same query runs against
api_noteby changing the table name, sincebody_contentandbody_orderare named the same there. - On that table the legacy body column is
_bodyrather thanbody, and the note identifier is an integer rather than a UUID.
Plugins #
Two changes to the Note data model shipped in the September 8, 2026 release, and both raise rather than failing quietly:
bodycannot be selected withvalues()orvalues_list().bodycannot be named through a relation, such asnote__body.
One case does fail quietly, so it is worth checking before your instance migrates:
Note.objects.filter(body=...)still works, but on a migrated note it matches againstbody_contentrather than the legacy list of lines. A filter written for the old shape returns nothing instead of raising.
Everything else a plugin does with a note body is unchanged:
- The note body structure read through the SDK is the same, including each command line’s
command_uuid. Note.checksumis the one attribute to move off, for the reason above.