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 #
canvas init- When prompted for a name, enter
Hello Custom Data cd hello-custom-data/hello_custom_data- Open
CANVAS_MANIFEST.jsonin your preferred editor. - Create a
custom_datablock:"custom_data": { "namespace": "my_org__hello_custom_data", "access": "read_write" } - Add
namespace_read_write_access_keyto thesecretsarray (the key will be generated by the system for you) - Next, create a
modelsdirectory under the root of your plugin hierarchy, sibling toCANVAS_MANIFEST.jsonandhandlers - Create an
__init__.pyfile inside ofmodelsand open it in your editor. - Declare the following classes within the
__init__.pyfile: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() - Open
handlers/event_handlers.pyin your editor. Update the imports:from hello_custom_data.models import CustomNote, NoteTag - In the code, replace uses of
NotewithCustomNote. These objects behave the same as the SDK model. (See Extending SDK Models for why proxy models are used.) - Add the following lines after the
notereference 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}") - Install the plugin to your development environment
- Tail the logs with
canvas logs - 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 #
- Extending SDK Models - Why proxy models exist and how
related_namenamespacing works - Transactions - All-or-nothing writes with
transaction.atomic() - Testing Custom Data - Testing utilities and examples
- Sharing Data - Sharing data with other plugins and external services
- Data Models - Core SDK data models
- Caching API - Auto-expiring transient data
- Canvas CLI - Simple API for sharing data between plugins
- Secrets - Managing API keys and sensitive configuration