Quick Start

Getting Started #

To use custom data in your plugin, declare a custom_data section in your CANVAS_MANIFEST.json with a namespace and access level. The namespace is a unique identifier scoped to your organization (formatted as organization__name with a double underscore), and the access level controls whether the plugin can read only or read and write data. When the first read_write plugin is installed into a namespace, the system automatically initializes a data namespace, prepares tables, and generates namespace_read_access_key and namespace_read_write_access_key secrets that control access for other plugins joining the same namespace.

{
  "sdk_version": "0.1.4",
  "plugin_version": "1.0.0",
  "name": "my_plugin",
  "secrets": ["namespace_read_write_access_key"],
  "custom_data": {
    "namespace": "acme_corp__shared_data",
    "access": "read_write"
  }
}

Step by Step #

  1. canvas init
  2. When prompted for a name, enter Hello Custom Data
  3. cd hello-custom-data/hello_custom_data
  4. Open CANVAS_MANIFEST.json in your preferred editor.
  5. Create a custom_data block:
    "custom_data": {
      "namespace": "my_org__hello_custom_data",
      "access": "read_write"
    }
    
  6. Add namespace_read_write_access_key to the secrets array (the key will be generated by the system for you)
  7. Next, create a models directory under the root of your plugin hierarchy, sibling to CANVAS_MANIFEST.json and handlers
  8. Create an __init__.py file inside of models and open it in your editor.
  9. Declare the following classes within the __init__.py file:
    from canvas_sdk.v1.data import Note, ModelExtension
    from canvas_sdk.v1.data.base import CustomModel
    from django.db.models import DO_NOTHING, OneToOneField, TextField
    
    
    class CustomNote(Note, ModelExtension):
        """Proxy model — see Extending SDK Models for why this exists."""
        pass
    
    
    class NoteTag(CustomModel):
        """Stores a plugin-assigned tag on a note."""
        note = OneToOneField(
            CustomNote, to_field="dbid", on_delete=DO_NOTHING,
            related_name="tag", primary_key=True
        )
        tagged_by = TextField()
    
  10. Open handlers/event_handlers.py in your editor. Update the imports:
    from hello_custom_data.models import CustomNote, NoteTag
    
  11. In the code, replace uses of Note with CustomNote. These objects behave the same as the SDK model. (See Extending SDK Models for why proxy models are used.)
  12. Add the following lines after the note reference has been initialized in the code:
    tag, created = NoteTag.objects.get_or_create(
        note=note,
        defaults={"tagged_by": "hello-custom-data"}
    )
    log.info(f"Note tagged by: {tag.tagged_by}")
    
  13. Install the plugin to your development environment
  14. Tail the logs with canvas logs
  15. Log into Canvas, navigate to a patient chart, and create a new note

In the logs you will see our message: Note tagged by: hello-custom-data

What just happened? When you installed the plugin, a new database namespace called my_org__hello_custom_data was created. Within the namespace are tables that hold information owned by, and managed by, the my_org plugins. The NoteTag model you defined turned into a PostgreSQL table with the following structure:

create table my_org__hello_custom_data.notetag
(
    note_id   bigint not null primary key,
    tagged_by text
);

Creating the NoteTag record caused a new row to be inserted into the notetag table in the my_org__hello_custom_data namespace. This table is private to the namespace. The Note itself is unmodified — the NoteTag CustomModel stores the additional data in its own table and links back to the note via a OneToOneField.

CustomModels let you define fully structured tables with typed fields and relationships — including linking to SDK models like Note, Patient, and Staff via OneToOneField or ForeignKey.

AttributeHub Alternative #

If you don’t need a structured model and just want to store a simple key-value pair, you can use an AttributeHub instead. Replace the NoteTag creation in event_handlers.py with:

from canvas_sdk.v1.data import AttributeHub
from logger import log

note_id = "89992c23-c298-4118-864a-26cb3e1ae822"
hub = AttributeHub.objects.create(
    type="note_tag",
    id=f"note:{note_id}"
)
hub.set_attribute("tagged_by", "hello-custom-data")
log.info(f"Note tagged by: {hub.get_attribute('tagged_by')}")

AttributeHubs are standalone key-value stores — they don’t require a model definition or a models directory. They’re a good fit for one-off state, configuration, and data that doesn’t have a natural schema. See Design Considerations for help choosing between the two approaches.

See Also #