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 deployed into a namespace, the system automatically initializes a data namespace and prepares tables. Canvas Platform holds the namespace’s access keys and sends them to each plugin entitled to the namespace, so other plugins join it with consent rather than a copied key (see Sharing Data).
{
"sdk_version": "0.1.4",
"plugin_version": "1.0.0",
"name": "acme_corp__my_plugin",
"custom_data": {
"namespace": "acme_corp__shared_data",
"access": "read_write"
}
}
Step by Step #
- Sign in with
canvas login, then runcanvas init. These steps assume your organization’s plugin prefix ismy_org. - When prompted for a name, enter
Hello Custom Data cd hello-custom-data/my_org__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" } - 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 my_org__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}") - Deploy the plugin to your development instance:
canvas deploy . --instance <instance> - 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 deployed 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
- Simple API - Simple API for sharing data between plugins
- Secrets - Managing API keys and sensitive configuration